读写锁

读写锁shared_mutex是一种较一般化的互斥锁,主要用于读操作比较多、写操作少的多线程情况,也就是可以有多个读线程和一个写线程对共享区域操作。读写锁有三种状态:读锁、写锁、不锁。遵循写互斥,读共享,写优先的原则。

常见的有下面几种情况:

  1. 多个线程可以同时占有读锁
  2. 每次只能有一个写线程,一个线程加了写锁,其他线程无论读写都要阻塞,解锁后,优先唤醒占有写锁的线程
  3. 一个线程加读锁,所有试图加读锁的线程都可以访问,试图加写锁的线程阻塞。也就是读共享,写互斥。 解锁后,要加写锁的线程优先。

boost中的shared_mutex默认的实现是写者优先。 读写锁在使用前要初始化,释放底层内存前要销毁。

shared_lockunique_lock

适用场景:对数据的读次数多于写次数的场合,比如数据库,我们允许在数据库上同时执行多个读操作,但是某一时刻只能在数据库上有一个写操作来更新数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <ros/ros.h>
#include <iostream>
#include <boost/ref.hpp>
#include <boost/thread/thread.hpp>
#include <string>
using namespace boost;
using namespace std;

int g_num = 10; // 全局变量,写者改变该全局变量,读者读该全局变量
shared_mutex s_mu; // 全局shared_mutex对象


void read_(std::string &str)
{
const char* c = str.c_str();
while (1)
{
{
shared_lock<shared_mutex> m(s_mu); //读锁定,shared_lock
printf("线程 %s 进入临界区,global_num = %d\n", c, g_num);
boost::this_thread::sleep(boost::posix_time::seconds(2)); //sleep 1秒,给足够的时间看其他线程是否能进入临界区
printf("线程 %s 离开临界区...\n", c);
}
boost::this_thread::sleep(boost::posix_time::seconds(2)); //读线程离开后再sleep 1秒(改变这个值可以看到不一样的效果)
}
cout << std::endl;
}


void writer_(std::string &str)
{
const char* c = str.c_str();
while (1)
{
{
unique_lock<shared_mutex> m(s_mu); //写锁定,unique_lock
++g_num;
printf("线程 %s 进入临界区,global_num = %d\n", c, g_num);
boost::this_thread::sleep(boost::posix_time::seconds(1)); //sleep 1秒,让其他线程有时间进入临界区
printf("线程 %s 离开临界区...\n", c);
}
//写线程离开后再sleep 2秒,如果这里也是1秒,那两个写线程一起请求锁时会让读线程饥饿
boost::this_thread::sleep(boost::posix_time::seconds(2));
}
cout << std::endl;
}

int main()
{
std::string r1 = "read_1";
std::string r2 = "read_2";
std::string w1 = "writer_1";
std::string w2 = "writer_2";

boost::thread_group tg;
tg.create_thread(bind(read_, boost::ref(r1))); //两个读者
tg.create_thread(bind(read_, boost::ref(r2)));

tg.create_thread(bind(writer_, boost::ref(w1))); //两个写者
tg.create_thread(bind(writer_, boost::ref(w2)));
tg.join_all();

return 0;
}

某次的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
线程read_1进入临界区,global_num = 10
线程read_2进入临界区,global_num = 10
线程read_1离开临界区...
线程read_2离开临界区...
线程writer_1进入临界区,global_num = 11
线程writer_1离开临界区...
线程writer_2进入临界区,global_num = 12
线程writer_2离开临界区...
线程read_2进入临界区,global_num = 12
线程read_2离开临界区...
线程read_1进入临界区,global_num = 12
线程read_1离开临界区...
线程writer_2进入临界区,global_num = 13
线程writer_2离开临界区...
线程writer_1进入临界区,global_num = 14
线程writer_1离开临界区...

由此可见,多个读线程可以同时进入临界区;写线程进入后,其他线程都不能进入,只能等它离开。即同时只有一个写线程能进入临界区。

需要避免这样的结果,也就是一个写线程连续执行,轮不到读线程了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
线程read_1进入临界区,global_num = 10
线程read_2进入临界区,global_num = 10
线程read_1离开临界区...
线程read_2离开临界区...

线程writer_1进入临界区,global_num = 11
线程writer_1离开临界区...
线程writer_1进入临界区,global_num = 12
线程writer_1离开临界区...
线程writer_1进入临界区,global_num = 13
线程writer_1离开临界区...

线程writer_2进入临界区,global_num = 14
线程writer_2离开临界区...
线程writer_2进入临界区,global_num = 15
线程writer_2离开临界区...
线程writer_2进入临界区,global_num = 16
线程writer_2离开临界区...
线程writer_2进入临界区,global_num = 17

参考:
读写锁