muduo C++ 网络库——线程同步精要(1):互斥锁

互斥器:使用得最多的同步原语

互斥锁的详细介绍在这一篇博文中:

互斥锁

 

概念补充:RAII——资源的有效期与持有资源的对象的生命期严格绑定,即由对象的构造函数完成资源的分配(获取),同时由析构函数完成资源的释放。只要对象能正确地析构,就不会出现资源泄漏问题。

 

互斥器保护了临界区,任何一个时刻最多只能有一个线程在此mutex划出的临界区内活动。单独使用mutex时,主要为了保护共享数据。原则:

-用RAII手法封装mutex的创建,销毁,加锁,解锁四个操作。

-只用非递归的mutex(即不可重入的mutex)

-不手工调用lock()和unlock()函数,一切交给Guard对象的构造和析构函数负责。Guard对象的生命期正好等于临界区。

-每次构造Guard对象的时候,思考已持有的锁,防止因加锁顺序不同而导致死锁。

 

 

1.只使用非递归的mutex

 mutex分为递归(recursive)非递归(non-recursive)两种。另一种叫法是:可重入非可重入

 

在同一线程里多次对non-recursive mutex加锁会立刻导致死锁,而recursive mutex不用考虑线程自己把自己锁死

但recursive mutex可能会隐藏代码里的一些问题,例如:

拿到一个锁就开始修改对象,但是没想到外层代码也拿到了锁,正在修改同一个对象。

[cpp]  view plain  copy
  1. MutexLock mutex;  
  2. std::vector<Foo> foos;  
  3.   
  4. void post(const Foo& f)  
  5. {  
  6.     MutexLockGuard lock(mutex);  
  7.     foos.push_back(f);  
  8. }  
  9.   
  10. void traverse()  
  11. {  
  12.     MutexLockGuard lock(mutex);  
  13.     for (std::vector<Foo>::const_iterator it = foos.begin(); it != foos.end(); ++it)  
  14.     {  
  15.         it->doit();  
  16.     }  
  17. }  

post加锁,然后修改foos对象;traverse()加锁,然后遍历foos向量。这都是正确的。

但是,如果Foo::doit()间接调用了post(),那么就会出现戏剧性的后果:
1.mutex是非递归的,于是就死锁了。

2.mutex是递归的,由于push_back()可能导致vector迭代器失效,程序偶尔会crash。

 

这种情况non-recursive就能暴露出程序的逻辑错误。死锁比较容易debug,把各个线程的调用栈打出来,很容易看出来是怎么死的。(gdb中使用thread apply all bt命令)

 

如果一个函数既可能在已加锁的情况下调用,又可能在未加锁的情况下调用,那么拆成两个函数:

1.跟原来的函数同名,函数加锁,转而调用第2个函数

2.给函数名加上后缀WithLockHold,不加锁,把原来的函数体搬过来。

[cpp]  view plain  copy
  1. void post(const Foo& f)  
  2. {  
  3.     MutexLockGuard lock(mutex);  
  4.     postWithLockHold(f);  
  5. }  
  6.   
  7. void postWithLockHold(const Foo& f)  
  8. {  
  9.     foos.push_back(f);  
  10. }  

加锁情况下调用postWithLockHold,未加锁情况就调用post。

 

2.死锁

考虑下面这个线程与自己死锁的例子:

[cpp]  view plain  copy
  1. #include "../Mutex.h"  
  2.   
  3. class Request  
  4. {  
  5.  public:  
  6.   void process() // __attribute__ ((noinline))  
  7.   {  
  8.     muduo::MutexLockGuard lock(mutex_);  
  9.     print();  
  10.   }  
  11.   
  12.   void print() const // __attribute__ ((noinline))  
  13.   {  
  14.     muduo::MutexLockGuard lock(mutex_);  
  15.   }  
  16.   
  17.  private:  
  18.   mutable muduo::MutexLock mutex_;  
  19. };  
  20.   
  21. int main()  
  22. {  
  23.   Request req;  
  24.   req.process();  
  25. }   

line 8 和 line 14造成了死锁。可以按照之前的方法从Request::print()抽取出Request::printWithLockHold()。并让print和process都调用它即可。

 

两个线程死锁的例子:

Inventory(清单)class,记录当前的Request对象:成员函数都是线程安全的

[cpp]  view plain  copy
  1. class Inventory  
  2. {  
  3.  public:  
  4.   void add(Request* req)  
  5.   {  
  6.     muduo::MutexLockGuard lock(mutex_);  
  7.     requests_.insert(req);  
  8.   }  
  9.   
  10.   void remove(Request* req) __attribute__ ((noinline))  
  11.   {  
  12.     muduo::MutexLockGuard lock(mutex_);  
  13.     requests_.erase(req);  
  14.   }  
  15.   
  16.   void printAll() const;  
  17.   
  18.  private:  
  19.   mutable muduo::MutexLock mutex_;  
  20.   std::set<Request*> requests_;  
  21. };  
  22.   
  23. Inventory g_inventory;  

Request class 与 Inventory class 的交互逻辑很简单,在处理(process)请求的时候,往g_inventory中添加自己。析构的时候从g_inventory中移除自己。 

[cpp]  view plain  copy
  1. class Request  
  2. {  
  3.  public:  
  4.   void process()            // __attribute__ ((noinline))  
  5.   {  
  6.     muduo::MutexLockGuard lock(mutex_);  
  7.     g_inventory.add(this);  
  8.     // ...  
  9.   }  
  10.   
  11.   ~Request() __attribute__ ((noinline))  
  12.   {  
  13.     muduo::MutexLockGuard lock(mutex_);  
  14.     sleep(1);  
  15.     g_inventory.remove(this);  
  16.   }  
  17.   
  18.   void print() const __attribute__ ((noinline))  
  19.   {  
  20.     muduo::MutexLockGuard lock(mutex_);  
  21.     // ...  
  22.   }  
  23.   
  24.  private:  
  25.   mutable muduo::MutexLock mutex_;  
  26. };  

 

Inventory class 还有一个功能是打印全部的Request对象:printAll()

[cpp]  view plain  copy
  1. void Inventory::printAll() const  
  2. {  
  3.   muduo::MutexLockGuard lock(mutex_);  
  4.   sleep(1);  
  5.   for (std::set<Request*>::const_iterator it = requests_.begin(); it != requests_.end(); ++it)  
  6.   {  
  7.     (*it)->print();  
  8.   }  
  9.   printf("Inventory::printAll() unlocked\n");  
  10. }  

 运行下面这个程序会产生死锁:

[cpp]  view plain  copy
  1. void threadFunc()  
  2. {  
  3.   Request* req = new Request;  
  4.   req->process();  
  5.   delete req;       //~Request()  
  6. }  
  7.   
  8. int main()  
  9. {  
  10.   muduo::Thread thread(threadFunc);  
  11.   thread.start();  
  12.   usleep(500 * 1000);  
  13.   g_inventory.printAll();  
  14.   thread.join();  
  15. }  

main()线程先调用 Inventory::printAll() 再调用 Request::print() ;

而threadFunc()线程,先调用 Request::~Request(), 再调用 Inventory::remove(),这两个调用序列对两个mutex的加锁顺序正好相反,于是造成了经典的死锁。

 

解决方案很简单,要么把print()移出printAll()的临界区;要么把remove()移出~Request()的临界区,比如交换Request类中13行和15行的代码。

猜你喜欢

转载自blog.csdn.net/amoscykl/article/details/80750690