互斥器:使用得最多的同步原语
互斥锁的详细介绍在这一篇博文中:
互斥锁
概念补充: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可能会隐藏代码里的一些问题,例如:
拿到一个锁就开始修改对象,但是没想到外层代码也拿到了锁,正在修改同一个对象。
- MutexLock mutex;
- std::vector foos;
-
- void post(const Foo& f)
- {
- MutexLockGuard lock(mutex);
- foos.push_back(f);
- }
-
- void traverse()
- {
- MutexLockGuard lock(mutex);
- for (std::vector::const_iterator it = foos.begin(); it != foos.end(); ++it)
- {
- it->doit();
- }
- }
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,不加锁,把原来的函数体搬过来。
- void post(const Foo& f)
- {
- MutexLockGuard lock(mutex);
- postWithLockHold(f);
- }
-
- void postWithLockHold(const Foo& f)
- {
- foos.push_back(f);
- }
加锁情况下调用postWithLockHold,未加锁情况就调用post。
2.死锁
考虑下面这个线程与自己死锁的例子:
- #include "../Mutex.h"
-
- class Request
- {
- public:
- void process()
- {
- muduo::MutexLockGuard lock(mutex_);
- print();
- }
-
- void print() const
- {
- muduo::MutexLockGuard lock(mutex_);
- }
-
- private:
- mutable muduo::MutexLock mutex_;
- };
-
- int main()
- {
- Request req;
- req.process();
- }
line 8 和 line 14造成了死锁。可以按照之前的方法从Request::print()抽取出Request::printWithLockHold()。并让print和process都调用它即可。
两个线程死锁的例子:
Inventory(清单)class,记录当前的Request对象:成员函数都是线程安全的
- class Inventory
- {
- public:
- void add(Request* req)
- {
- muduo::MutexLockGuard lock(mutex_);
- requests_.insert(req);
- }
-
- void remove(Request* req) __attribute__ ((noinline))
- {
- muduo::MutexLockGuard lock(mutex_);
- requests_.erase(req);
- }
-
- void printAll() const;
-
- private:
- mutable muduo::MutexLock mutex_;
- std::set requests_;
- };
-
- Inventory g_inventory;
Request class 与 Inventory class 的交互逻辑很简单,在处理(process)请求的时候,往g_inventory中添加自己。析构的时候从g_inventory中移除自己。
- class Request
- {
- public:
- void process()
- {
- muduo::MutexLockGuard lock(mutex_);
- g_inventory.add(this);
-
- }
-
- ~Request() __attribute__ ((noinline))
- {
- muduo::MutexLockGuard lock(mutex_);
- sleep(1);
- g_inventory.remove(this);
- }
-
- void print() const __attribute__ ((noinline))
- {
- muduo::MutexLockGuard lock(mutex_);
-
- }
-
- private:
- mutable muduo::MutexLock mutex_;
- };
Inventory class 还有一个功能是打印全部的Request对象:printAll()
- void Inventory::printAll() const
- {
- muduo::MutexLockGuard lock(mutex_);
- sleep(1);
- for (std::set::const_iterator it = requests_.begin(); it != requests_.end(); ++it)
- {
- (*it)->print();
- }
- printf("Inventory::printAll() unlocked\n");
- }
运行下面这个程序会产生死锁:
- void threadFunc()
- {
- Request* req = new Request;
- req->process();
- delete req;
- }
-
- int main()
- {
- muduo::Thread thread(threadFunc);
- thread.start();
- usleep(500 * 1000);
- g_inventory.printAll();
- thread.join();
- }
main()线程先调用 Inventory::printAll() 再调用 Request::print() ;
而threadFunc()线程,先调用 Request::~Request(), 再调用 Inventory::remove(),这两个调用序列对两个mutex的加锁顺序正好相反,于是造成了经典的死锁。
解决方案很简单,要么把print()移出printAll()的临界区;要么把remove()移出~Request()的临界区,比如交换Request类中13行和15行的代码。