【问题标题】:Mistakes/questions in singleton example in book "Cracking the coding interview"?“破解编码面试”一书中单例示例中的错误/问题?
【发布时间】:2010-12-12 00:21:21
【问题描述】:

在《Cracking the coding interview》一书的第 259 页上,给出了 C++ 中的模板化单例(我不想发布所有代码,以防其受到版权保护)。

问题是将单例实现为模板,并假设一个名为 Lock 的类确保其线程安全和异常安全。

答案正如您对使用双重锁定技术的单例所期望的那样,Lock 对象具有一个 acquire()/release() 对。

但是该类没有析构函数。这是一个错误吗?如果它有一个析构函数,因为类实例成员是静态的,析构函数只会在程序终止时被调用,如果程序正在终止,任何分配的内存都会被释放回系统。还是会?是否存在这种情况无法发生并且没有析构函数的单例因此导致泄漏的情况?

其次,问题是让单例异常安全。单例对象是使用未捕获的新对象创建的,而 Lock 对象是静态的,因此这实际上是异常安全的吗?如果没有用于创建单例的内存,那么 new 会抛出异常,但是由于 Lock 对象是静态的,因此无法调用其 release() 方法,因此它永远不会被调用?

【问题讨论】:

  • 如果一个类没有析构函数,编译器会生成一个默认的。
  • 双重锁定的问题在于它以某种方式假定Lock 在第一次尝试获取它之前已正确初始化。但是,如果您尝试在全局构造期间获取它,则情况可能并非如此(在某些编译器上)... C++0x 通过要求编译器使局部静态变量初始化线程安全来解决该问题。
  • 这本书充满了错误……而且不仅仅是错别字。很多解决方案都是错误的。这是一个很好的例子。自从多核出现以来,双重检查锁定技术完全是伪造的。

标签: c++


【解决方案1】:

首先,我要提醒一下,Singleton 被广泛认为是一种反模式——这似乎是一个好主意,但结果往往是一个错误。

其次,如果不看代码就很难确定,但我的直接猜测是,如果你想让它异常安全,你可能最好将它们提供的锁对象包装在释放锁的 RAII 包装器中它的dtor:

class real_lock {
    Lock lock;
public:
    real_lock() { lock.acquire(); }
    ~real_lock() { lock.release(); }
};

这样,异常安全(至少是 Lock 部分)相当简单。 OTOH,双重检查锁定也几乎是一种反模式。您使用它所做的几乎任何事情都至少有可能在某些机器上/在某些情况下出现问题。

【讨论】:

  • 那没用。互斥锁仅在多个线程获取相同互斥锁时才起作用,但此互斥锁Lock lock; 是本地的。
  • 如果 real_lock 在单例的 GetInstance() 函数中被声明为一个堆栈变量,那么我会这样做。但是在书中给出的答案中,锁类被声明为静态类变量,在这种情况下,我无法看到在新抛出的情况下可以释放锁?
  • 如果双重检查锁定是一种反模式,那么如何在 C++ 中实现线程安全的单例?所有 c++ 书籍和设计模式书籍似乎都在使用它。
  • @Percy:Jerry 的代码不能用作堆栈变量,因为每个调用者都在锁定不同的互斥体,所以不会有任何同步。
  • @Percy:正如 Jerry 所说,Singleton 也是一种反模式。
【解决方案2】:

异常安全并不意味着捕获异常。这意味着使用 RAII 和自动析构函数来确保在出现异常时正确进行清理。

至于互斥体,正确的做法是拥有一个静态互斥体对象和一个自动范围保护样式的获取/释放 RAII 对象。由于 RAII 对象具有自动存储功能,因此在发生异常时会释放互斥锁。

编辑:这是 RAII 类的正确形式

class scoped_lock_guard
{
    Lock& m_lock;
public:
    scoped_locK_guard(Lock& lock) : m_lock(lock) { lock.acquire(); }
    ~scoped_lock_guard() { m_lock.release(); }
};

Lock 对象本身必须以某种方式共享。

【讨论】:

  • 我知道异常安全并不意味着使用 catch。我的观点是 Lock 的 Release() 方法没有一个就不能调用,或者除非使用 RAII。因此,此类不是异常安全的,因为如果引发异常,则对 acquire() 和 release() 的调用之间存在不匹配。
  • 我没有代码,但就像我说的那样,获取和释放应该从实例化为自动本地的 RAII 类中完成。当然,它需要与互斥体本身不同,互斥体本身是在实例之间共享的。
  • 解决方案的答案没有使用 RAII 类,因此在我看来这本书给出了一个不正确的答案。我只是想检查一下我没有遗漏什么,但似乎没有。单例中缺少析构函数怎么办,是否存在不显式删除分配给静态变量的内存会导致泄漏的操作系统?
  • RAII 是处理异常安全的 C++ 方式,如果不使用,那么这本书是错误的。缺少用户提供的析构函数并不意味着没有析构函数,编译器会自动生成一个调用类成员子对象和基类子对象的析构函数。
【解决方案3】:

在本书的开头部分,作者建议她将用 Java 编写“几乎所有”解决方案(第 42 页)。

Java GC 将负责清理静态实例。

【讨论】:

  • Java 没有模板,所以如果这个特定示例确实使用了模板,那么它就不是 Java。
  • 我的问题是“在 C++ 中”
  • RTFM .. 手掌对着脸!我相信在 C++ 的情况下,静态分配将在作者的解决方案中泄露。
猜你喜欢
  • 1970-01-01
  • 2018-08-12
  • 1970-01-01
  • 1970-01-01
  • 2019-09-29
  • 2019-10-11
  • 1970-01-01
  • 1970-01-01
  • 2013-09-05
相关资源
最近更新 更多