我遇到了一个让我感到不安的问题。似乎我发现了一种容易解决的情况,但如果a)我在编程时失去了注意力,或者b)其他人开始实现我的界面并且不知道如何处理,这可能会导致问题这种情况。
这是我的基本设置:
我有一个抽象类,我正在使用它作为几种数据类型的通用接口。我采用了非虚拟公共接口范例(Sutter,2001)以及范围锁定来提供一些线程安全性。一个示例接口类看起来像这样(我遗漏了有关作用域锁定和互斥实现的细节,因为我认为它们不相关):
class Foo
{
public:
A( )
{
ScopedLock lock( mutex );
aImp( );
}
B( )
{
ScopedLock lock( mutex );
bImp( );
}
protected:
aImp( ) = 0;
bImp( ) = 0;
}
然后由用户来实现aImp和bImp,这是问题所在。如果aImp执行一些使用bImp的操作,那么执行此操作非常容易(在某种意义上几乎是逻辑的): / p>
class Bar
{
protected:
aImp( )
{
...
B( );
...
}
bImp( )
{
...
}
}
死锁。当然,对此的简单解决方案是始终调用受保护的虚拟函数而不是其公共变体(在上面的代码段中用bImp()替换B())。但是,如果我犯了一个错误,或者更糟糕的是让其他人自己上吊,那么让自己挂起来似乎仍然很容易。
是否有人有办法尝试阻止抽象类的实现者在编译时调用这些公共函数,或者以其他方式帮助避免死锁解决方案?
只是为了解决问题,一些互斥锁允许操作,这将避免死锁问题。例如,如果我使用Windows函数EnterCriticalSection和LeaveCriticalSection实现它,则没有问题。但我宁愿避免使用特定于平台的功能。我目前在我的作用域锁实现中使用boost :: mutex和boost :: shared_mutex,据我所知,它并没有尝试避免死锁(我认为我几乎更喜欢)。
答案 0 :(得分:7)
使用私有继承可能会解决您的问题:
class Foo
{
public:
void A( )
{
ScopedLock lock( mutex );
aImp( );
}
void B( )
{
ScopedLock lock( mutex );
bImp( );
}
protected:
virtual void aImp( ) = 0;
virtual void bImp( ) = 0;
};
class FooMiddle : private Foo
{
public:
using Foo::aImp;
using Foo::bImp;
};
class Bar : public FooMiddle
{
virtual void aImpl ()
{
bImp ();
B (); // Compile error - B is private
}
};
私下从Foo派生,然后使用FooMiddle确保Bar无法访问A或B.但是,bar仍然可以覆盖aImp和bImp,而FooMiddle中的using声明意味着仍然可以调用它们来自Bar。
或者,帮助但未解决问题的选项是使用Pimpl模式。你最终会得到如下内容:
class FooImpl
{
public:
virtual void aImp( ) = 0;
virtual void bImp( ) = 0;
};
class Foo
{
public:
void A( )
{
ScopedLock lock( mutex );
m_impl->aImp( );
}
void B( )
{
ScopedLock lock( mutex );
m_impl->bImp( );
}
private:
FooImpl * m_impl;
}
好处是在源自FooImpl的类中,它们不再具有“Foo”对象,因此不能轻易地调用“A”或“B”。
答案 1 :(得分:6)
您的互斥锁不能是递归互斥锁。如果它不是递归互斥锁,则在同一线程中锁定互斥锁的第二次尝试将导致该线程阻塞。由于该线程锁定了互斥锁,但在该互斥锁上被阻塞,因此会出现死锁。
你可能想看看:
boost::recursive_mutex
http://www.boost.org/doc/libs/1_32_0/doc/html/recursive_mutex.html
它应该实现跨平台的递归互斥行为。注意Win32 CRITICAL_SECTION(通过Enter / LeaveCriticalSection使用)是递归的,这将创建你描述的行为。
答案 2 :(得分:1)
虽然递归锁可以解决你的问题,但我总是觉得,虽然有时是必要的,但在许多情况下,递归锁被用作一种简单的出路,锁定方式太多了。
您的发布代码显然已经过简化以用于演示目的,所以我不确定它是否适用。
举个例子,假设使用资源X不是线程安全的。你有类似的东西。
A() {
ScopedLock
use(x)
aImp()
use(x)
}
aImp() {
ScopedLock
use(x)
}
显然,这会导致死锁。
然后使用更窄的锁,可以消除问题。出于性能原因,在尽可能小的范围内使用锁定总是一个好主意,因为可以避免死锁。
A() {
{
ScopedLock
use(x)
}
aImp()
{
ScopedLock
use(x)
}
}
你明白了。
我知道这并不总是可行的(或者会导致可怕的低效代码),而不知道更多细节,我不知道它是否适用于您的问题。但是无论如何都认为它值得发布。