我有一些代码,需要是线程安全的,异常安全。 下面的代码是我的问题的一个非常简化的版本:
#include <mutex>
#include <thread>
std::mutex mutex;
int n=0;
class Counter{
public:
Counter(){
std::lock_guard<std::mutex>guard(mutex);
n++;}
~Counter(){
std::lock_guard<std::mutex>guard(mutex);//How can I protect here the underlying code to mutex.lock() ?
n--;}
};
void doSomething(){
Counter counter;
//Here I could do something meaningful
}
int numberOfThreadInDoSomething(){
std::lock_guard<std::mutex>guard(mutex);
return n;}
我有一个互斥体,我需要在一个对象的析构函数来锁定。 问题是,我的析构函数不应该抛出异常。
我能做什么 ?
0),我不能没有n
与原子变量(当然它会在这里做的伎俩,但不是我的问题点)
1)我可以用自旋锁代替我的互斥
2)我可以尝试捕捉锁定陷入无限循环,直到我eventualy收购也不例外提出的锁
这些解决方案都没有,似乎非常有吸引力。 你是否也有同样的问题? 你是如何解决的呢?
正如亚当·彼得森H.建议,我终于决定写一个不抛互斥:
class NoThrowMutex{
private:
std::mutex mutex;
std::atomic_flag flag;
bool both;
public:
NoThrowMutex();
~NoThrowMutex();
void lock();
void unlock();
};
NoThrowMutex::NoThrowMutex():mutex(),flag(),both(false){
flag.clear(std::memory_order_release);}
NoThrowMutex::~NoThrowMutex(){}
void NoThrowMutex::lock(){
try{
mutex.lock();
while(flag.test_and_set(std::memory_order_acquire));
both=true;}
catch(...){
while(flag.test_and_set(std::memory_order_acquire));
both=false;}}
void NoThrowMutex::unlock(){
if(both){mutex.unlock();}
flag.clear(std::memory_order_release);}
这个想法是有两个互斥的,而不是只有一个。 真正的互斥与实现自旋互斥std::atomic_flag
。 这种自旋互斥,由保护std::mutex
这可能抛出。
在正常情况下,标准的互斥被获取并设置了标志只有一个原子操作的成本。 如果标准互斥不能被立即锁定,该线程进入休眠状态。
如果由于某种原因,标准的互斥抛出,互斥体将进入旋转模式。 当发生异常会然后循环,直到它可以设置标志的线程。 由于没有其他线程知道,这个线程bybassed完全标准的互斥体,它们可以旋转了。
在最坏的情况下,这种锁定机制降低到一个自旋锁。 大多数时候,它的反应就像一个正常的互斥。
这是一个糟糕的情况是在你的析构函数做的事情可能会失败。 如果未能及时更新这个计数器将无可挽回地破坏你的应用程序,你可能想干脆让析构函数抛出。 这将调用你的程序崩溃terminate
,但如果您的应用程序已损坏,它可能是更好杀死进程,并依靠一些更高级别的恢复方案(如后台进程或重试运行另一程序看门狗) 。 如果未能递减计数器是可恢复的,你应该吸收例外与try{}catch()
块和恢复(或潜在的信息保存一些其它的操作,最终恢复)。 如果这是不可能收回的,但它不是致命的,你可能要赶上和吸收异常并记录故障(即一定要登录一个异常安全的方式,当然)。
这将是理想的,如果该代码可以重组使得析构函数并不做任何事情,不能失败。 但是,如果你的代码是正确的,否则,失败而获取锁是除了在有限资源的情况下的可能很少,所以无论是吸收或中止对失败很可能是可以接受的。 对于一些互斥锁()可能是一个不抛的操作(如自旋锁使用atomic_flag),如果你可以使用这样一个互斥体,你可以期望lock_guard永远不会抛出。 你在这种情况下唯一担心的是僵局。