C++11线程库使用

从C++11开始,在STL中提供了thread模块对操作系统的C线程库进行了封装,避免了跨平台问题。但是,很多人觉得这个库封装的不够好。

线程同步

互斥量

C++11中提供了如下4种语义的互斥量(mutex):

  • std::mutex:独占的互斥量,不能递归使用。
  • std::timed_mutex:带超时的独占互斥量,不能递归使用。
  • std::recursive_mutex:递归互斥量,不带超时功能。
  • std::recursive_timed_mutex:带超时的递归互斥量。

这些互斥量的基本接口很相似,一般用法是通过lock()方法来阻塞线程,直到获得互斥量的所有权为止。在线程获得互斥量并完成任务之后,就必须使用unlock()来解除对互斥量的占用,lock()和unlock()必须成对出现。try_lock()尝试锁定互斥量,如果成功则返回true,如果失败则返回false,它是非阻塞的。

独占互斥量std::mutex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <mutex>
#include <thread>

using namespace std;

mutex m;

void thread1_func(int i)
{
m.lock();
// 在这里写上你需要的代码
m.unlock();
}

int main()
{
thread t1(thread_func, 1);
thread t2(thread_func, 2);
t1.join();
t2.join();
return 0;
}

使用lock_guard可以简化lock/unlock的写法,同时也更安全,因为lock_guard在构造时会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而保证了互斥量的正确操作,避免忘记unlock操作,因此,应尽量用lokc_guard。lock_guard用到了RAII技术,这种技术在类的构造函数中分配资源,在析构函数中释放资源,保证资源在出了作用域之后就释放。上面的例子使用lock_guard后会更简洁,代码如下:

1
2
3
4
5
6
7
void func()
{
lock_guard<std::mutex> locker(m); // 出作用域之后自动解锁
cout <<"entered thread " <<this_thread::get_id() <<endl;
this_thread::sleep_for(std::chrono::seconds(1));
cout <<"leaving thread " <<std::this_thread::get_id() <<endl;
}

递归的独占互斥量std::recursive_mutex

递归锁允许同一线程多次获得该互斥锁,可以用来解决同一线程需要多次获取互斥量时死锁的问题。一个线程多次获取同一个互斥量时会发生死锁。要解决这个死锁的问题,一个简单的办法就是用递归锁:std::recursive_mutex,它允许同一线程多次获得互斥量。

代码示例:

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
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;


struct Complex {
std::recursive_mutex mutex;
int i;

Complex():i(0){}

void mul(int x) {
std::lock_guard<std::recursive_mutex> lock(mutex);
i*=x;
}
void div(int x) {
std::lock_guard<std::recursive_mutex> lock(mutex);
i/=x;

}
void both(int x,int y) {
std::lock_guard<std::recursive_mutex> lock(mutex);
mul(x);
div(y);
}

};

int main() {
Complex complex;
complex.both(32, 23); // 因为同一线程可以多次获取同一互斥量,不会发生死锁

return 0;
}

需要注意的是尽量不要使用递归锁好,主要原因如下:

  1. 需要用到递归锁定的多线程互斥处理往往本身就是可以简化的,允许递归互斥很容易放纵复杂逻辑产生,从而导致一些多线程同步引起的晦涩问题。
  2. 递归锁比起非递归锁,效率会低一些。
  3. 递归锁虽然允许同一个线程多次获得同一互斥量,可重复获得的最大次数并未具体说明,一旦超过一定次数,再对lock进行调用就会抛出std::system错误。

带超时的互斥量

std::timed_mutex是超时的独占锁,std::recursive_timed_mutex是超时的递归锁,主要用在获取锁时超时等待功能,因为有时不知道获取锁需要多久,为了不至于一直在等待获取互斥量,就设置一个等待超时时间,在超时后还可以做其他事情。std::timed_mutex比std::mutex多了两个超时获取锁的接口:try_lock_for和try_lock_until,这两个接口是用来设置获取互斥量的超时时间。

std::timed_mutex的基本用法如下代码:

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
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;

std::timed_mutex mutex1;

void work() {
chrono::milliseconds timeout(100);

while(true) {
if(mutex1.try_lock_for(timeout)) {
cout <<this_thread::get_id() <<":do work with the mutex" <<endl;
chrono::milliseconds sleepDuration(250);
this_thread::sleep_for(sleepDuration);

mutex1.unlock();
this_thread::sleep_for(sleepDuration);
} else {
cout <<this_thread::get_id() <<": do work without mutex" <<endl;
chrono::milliseconds sleepDuration(100);
this_thread::sleep_for(sleepDuration);
}
}
}

int main() {
thread t1(work);
thread t2(work);

t1.join();
t2.join();

return 0;
}

在上面的例子中,通过一个while循环不断地去获取超时锁,如果超时还没有获取到锁时就休眠100毫秒,再继续获取超时锁。相比std::timed_mutex,std::recursive_timed_mutex多了递归锁的功能,允许同一线程多次获得互斥量。

条件变量

条件变量是C++11提供的另外一种用于同步线程的机制,它能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时,才会唤醒当前阻塞的线程。条件变量需要和互斥量配合起来用。C++11提供了两种条件变量:

  • condition_variable,配合std::unique_lock<std::mutex>进行wait操作。
  • condition_variable_any,和任意带有lock、unlock语义的mutex搭配使用,比较灵活,但效率比condition_variable差一些。

可以看到condition_variable_any比condition_variable更灵活,因为它更通用,对所有的锁都适用,而condition_variable性能更好。

条件变量的使用过程如下:

  1. 拥有条件变量的线程获取互斥量。
  2. 循环检查某个条件,如果条件不满足,则阻塞直到条件满足;如果条件满足,则向下执行。
  3. 某个线程满足条件执行完之后调用notify_one或notify_all唤醒一个或者所有的等待线程。

我们可以看一个经典的生产者-消费者的例子:

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
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <unistd.h>
using namespace std;


mutex m_mutex; // 互斥量
int s = 0; // 共享资源
condition_variable m_notempty; // 条件变量

void producer() { // 生产者
while(1) {
sleep(1);

m_mutex.lock(); // 上锁
s++;
cout <<"increase one ,s=" <<s <<endl;
m_mutex.unlock();

m_notempty.notify_one();
}
}


void consumer() { // 消费者
while(1) {
sleep(1);

unique_lock<mutex> locker(m_mutex);
while(s==0) m_notempty.wait(locker); // 使用条件变量
// 或
// m_notempty.wait(locker, []{ return s > 0; });
s--;
cout <<"decrease one,s=" <<s <<endl;
}
}

int main() {
// 创建线程
thread thread1(producer);
thread thread2(consumer);
thread1.join();
thread2.join();
}

上面的例子中while(s==0) m_notempty.wait(locker); ,这句代码的意思的是当s等于0的时候,阻塞直到条件满足时被唤醒。我们也可以这么用, m_notempty.wait(locker, []{ return s > 0; });,将判断条件放到函数里面,意思是wait将一直阻塞,知道判断条件满足时,被唤醒。

原子变量

C++11提供了一个原子类型std::atomic<T>,可以使用任意类型作为模板参数,C++11内置了整型的原子变量,可以更方便地使用原子变量,使用原子变量就不需要使用互斥量来保护该变量了,因为对该变量的操作保证其是原子的,是不可中断的。用起来更简洁。

要做一个计时器,使用mutex时,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <mutex>
using namespace std;


struct Counter {
int value;
std::mutex m_mutex;

void increment() {
std::lock_guard<std::mutex> lock(mutex);
++value;
}

void decrement() {
std::lock_guard<std::mutex> lock(mutex);
--value;
}

int get() {
return value;
}
};

如果使用原子变量,就不需要再定义互斥量了,使用更简便。示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <mutex>
using namespace std;


struct Counter {
std::atomic<int> value;

void increment() {
value++;
}

void decrement() {
value--;
}

int get() {
return value;
}
};

其它

call_one/once_flag的使用

为了保证在多线程环境中某个函数仅被调用一次,比如,需要初始化某个对象,而这个对象只能初始化一次时,就可以用std::call_once来保证函数在多线程环境中只被调用一次。使用std::call_once时,需要一个once_flag作为call_one的入参,它的用法比较简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;

std::once_flag flag;

void do_once() {
std::call_once(flag, []{ std::cout <<"Called once" <<endl; });
}

int main() {
std::thread t1(do_once);
std::thread t2(do_once);
std::thread t3(do_once);

t1.join();
t2.join();
t3.join();
}

运行结果:

Called once

参考

------ 本文结束------
赞赏此文?求鼓励,求支持!
  • 本文标题: C++11线程库使用
  • 本文作者: Jiang.G.F
  • 创建于: 2020年02月15日 - 22时02分
  • 更新于: 2020年03月03日 - 11时03分
  • 本文链接: https://gfjiangly.github.io/C++/cpp11_thread.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!
0%