1.前言
比如说我们现在以一个list容器来模仿一个消息队列,当消息来临时插入list的尾部,当读取消息时就把头部的消息读出来并且删除这条消息。在代码中就以两个线程分别实现消息写入和消息读取的功能,如下:
class msgList { private: list<int>mylist; //用list模仿一个消息队列 public: void WriteList() //向消息队列中写入消息(以i作为消息) { for (int i = 0; i<100000; i++) { cout << "Write : " << i <<endl; mylist.push_back(i); } return; } void ReadList() //从消息队列中读取并取出消息 { for(int i=0;i<100000;i++) { if (!mylist.empty()) { cout << "Read : " << mylist.front() << endl; mylist.pop_front(); } else { cout << "Message List is empty!" << endl; } } } }; int main() { msgList mlist; thread pread(&msgList::ReadList, &mlist); //读线程 thread pwrite(&msgList::WriteList, &mlist); //写线程 //等待线程结束 pread.join(); pwrite.join(); return 0; }这段程序在运行过程中,大部分时间是正常的,但是也会出现如下不稳定的情况:
为什么会出现这种情况呢?
这是因为消息队列对于读线程和写线程来说是共享的,这时就会出现两种特殊的情况:读线程的读取操作还没有结束,线程上下文就切换到了写线程中;或者写线程的写入操作还没有结束,线程上下文切换就到了读线程中,这两种情况都反映了读写冲突,从而出现了以上错误。
要想解决这个问题,最显然最直接的方法就是将读写操作分离开来,读的时候不允许写,写的时候不允许读,这样,才能实现线程安全的读和写。说形象一点,就是在进行读操作时,就对共享资源进行加锁,禁止其他线程访问,其他线程要访问就得等到读线程解锁才行,就像上厕所一样,一次只能上一个人,其他人必须得等他上完了再上。这样,就有了互斥锁的概念。
2 互斥锁
在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。比如说,同一个文件,可能一个线程会对其进行写操作,而另一个线程需要对这个文件进行读操作,可想而知,如果写线程还没有写结束,而此时读线程开始了,或者读线程还没有读结束而写线程开始了,那么最终的结果显然会是混乱的。为了保护共享资源,在线程里也有这么一把锁——互斥锁(mutex),互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。
2.1 互斥锁的特点
1. 原子性:把一个互斥量锁定为一个原子操作,这意味着如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量;
2. 唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;
3. 非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。
2.2 互斥锁的使用
根据前面我们可以知道,互斥锁主要就是用来保护共享资源的,在C++ 11中,互斥锁封装在mutex类中,通过调用类成员函数lock()和unlock()来实现加锁和解锁。值得注意的是,加锁和解锁,必须成对使用,这也是比较好理解的。除此之外,互斥量的使用时机,就以开篇程序为例,我们要保护的共享资源当然就是消息队列list了,那么互斥锁应该加在哪里呢?
可能想的比较简单一点:就直接把锁加在函数最前面不就好了么?如下所示:
class msgList { private: list<int>mylist; //用list模仿一个消息队列 mutex mtx; //创建互斥锁对象 public: void WriteList() //向消息队列中写入消息(以i作为消息) { mtx.lock(); for (int i = 0; i<100000; i++) { cout << "Write : " << i <<endl; mylist.push_back(i); } mtx.unlock(); return; } //....... };不过如果这样加锁的话,要等写线程完全执行结束才能开始读线程,读写线程变成了串行执行,这就违背了线程并发性的特点了。正确的加锁方式应当是在执行写操作的具体部分加锁,如下所示:
class msgList { private: list<int>mylist; //用list模仿一个消息队列 mutex mtx; //创建互斥锁对象 public: void WriteList() //向消息队列中写入消息(以i作为消息) { for (int i = 0; i<100000; i++) { mtx.lock(); cout << "Write : " << i <<endl; mylist.push_back(i); mtx.unlock(); } return; } //....... };这样,才能真正的实现读写互不干扰。
下面再举一个更为直观的例子,创建两个线程同时对list进行写操作:
#include <iostream> #include <list> #include <mutex> #include <thread> using namespace std; class msgList { private: list<int> mylist; mutex m; int i = 0; public: void WriteList() { while (true) { int val; { // 临界区:读判断 + 自增 + 插入,三步必须在一个锁内 lock_guard<mutex> lk(m); if (i >= 1000) break; val = i++; mylist.push_back(val); } // 出作用域自动解锁 // 锁外可做其他无需互斥的事 } } void showList() { lock_guard<mutex> lk(m); // 若可能与其他线程并发,读也要加锁 for (auto p = mylist.begin(); p != mylist.end(); ++p) cout << (*p) << " "; cout << endl; cout << "size of list : " << mylist.size() << endl; } }; int main() { msgList mlist; thread pwrite0(&msgList::WriteList, &mlist); thread pwrite1(&msgList::WriteList, &mlist); pwrite0.join(); pwrite1.join(); cout << "threads end!" << endl; mlist.showList(); //子线程结束后主线程打印list return 0; }link