C++多线程2
多线程通信与同步
1 多线程状态
1.1 线程状态说明:
- 初始化 Init: 该进程正在被创建
- 就绪 Ready :该线程在就绪列表中,等待CPU调度。
- 运行 Running : 该新城正在运行。
- 阻塞 Blocked :该线程备注色挂起,Block状态包括:pend(锁,事件,信号量等阻塞),suspend(主动pend),delay(演示阻塞),pendtime(因为所,事件,信号量时间等超时等待)。
- 退出 Exit :该线程运行结束,等待父线程回收其控制块资源(不包含堆资源)。
状态转换图如下:
1.1.2 竞争状态(Race Condition)和临界区(Critical Section)
竞争状态(Race Condition):
多线程同时读写共享数据。
临界区 (Critical Section):
读写共享的代码片段。
避免竞争状态策略: 对临界区进行保护,同时只有一个进程能够进入临界区。
1.2 互斥体和锁mutex
1.2.1 互斥锁 mutex
- lock(),try_lock()
- unlock()
代码演示:
#include<thread>
#include<iostream>
#include <mutex>
using namespace std;
static mutex m;
void thread_main() {
m.lock();//其他线程阻塞等待
cout << "********************" << endl;
cout << "1" << endl;
cout << "1" << endl;
cout << "1" << endl;
cout << "1" << endl;
cout << "********************" << endl;
m.unlock();
}
int main() {
for(int i=0;i<10;i++)
{
thread th(thread_main);
th.detach();
}
return 0;
}
如果不加锁将会出现对临界区资源竞争的情况,无法输出整块内容。而加锁之后,可以完整的输出。
当我们进行加锁操作时,未获得锁的进程将会排队阻塞等待。如果忘记解锁,将会导致死锁问题,另外我们希望临界区代码越小越好,以防止资源的过度浪费。
此外,可以使用try_lock(),尝试对临界区进行加锁。子进程可以定时对所进行访问,当没有获取到锁时,进程不必阻塞等待。在进行try_lock()操作时也需要耗费系统资源。因此使用try_lock()时需要注意系统资源的释放(sleep),防止进程不断询问造成资源的耗尽。
1.2.2 互斥锁的坑_线程抢占不到资源
static mutex m;
void threadtest(int i) {
for(;;)
{
m.lock();
cout << i << "[in]" << endl;
this_thread::sleep_for(1s);
m.unlock();
//this_thread::sleep_for(1s);
}
}
int main() {
for (int i = 0; i < 5; i++) {
thread th(threadtest,i);
th.detach();
}
getchar();
}
代码运行结果如下:
通过图中我们可以看到一个线程抢到锁之后,其他线程就不能抢到锁。这是因为线程在处理循环事务时,在解锁和加锁之间线程没有其他事务,这导致系统还没来的及释放资源,就已经重新加锁。
而当我们在加锁和解锁之间添加其他事务时
static mutex m;
void threadtest(int i) {
for(;;)
{
m.lock();
cout << i << "[in]" << endl;
this_thread::sleep_for(1s);
m.unlock();
this_thread::sleep_for(1s);
}
}
int main() {
for (int i = 0; i < 5; i++) {
thread th(threadtest,i);
th.detach();
}
getchar();
}
代码运行结果如下:
我们在解锁和加锁中间睡眠一秒,这样可以防止解锁之后立马加锁。
1.2.3 超时锁应用 timed_mutex避免长时间死锁
- 可以记录锁获取情况,多次超时,可以记录日志,获取错误情况。
我们在讲mutex的时候,锁是默认没有超时的,也就是说如果,有另外一个线程在占用资源的情况下,这一个线程是一直阻塞的,这种方式的好处就是。代码简洁,但是也带来了一些问题,就是我们后期的这种调试难度就比较大,比如说,我们不小心在代码当中写了一个死锁,写完这个死锁,那么在调试当中,怎么去找到这个死锁;第一个方案是每次锁之前记一下日志,看他有没有进去,但是这样的一个调试成本比较大,不太容易发现死锁,前面也有几种方案,一种方案就是用try_lock(),try_lock这种方案就是尝试去获取锁,就是我们隔一段时间尝试一下,隔一段时间尝试下,如果长时间都没有争取到这个锁资源,那表示我们的代码可能是有问题了,那这样的话我们就去解决它,但try_lock()有点麻烦,在try lock()的时候,如果一直try_lock(),可能造成什么情况呢?就是你的CPU资源会会耗尽。这时就需要,每次try_lock()之后呢,你就要去做一个sleep,要去释放一下资源,就是try_lock()失败之后你就去sleep,然后再次做try_lock(),所以这件事需要自己去控制,当然这里我们,为什么不能直接有这样的一个接口,就把一步把两件事做掉,其实是有类似的做法,那就有一个try_lock_for(),把这两步做了,一个是try_lock(),第二个是sleep,它的一个好处是代码简洁,另外一个就是超时之后就会返回。接下来是代码演示部分:
#include<thread>
#include<iostream>
#include <mutex>
using namespace std;
timed_mutex tmux;
void threadmain(int i) {
for (;;) {
if (!tmux.try_lock_for(1ms)) {
cout <<i<< "time_out" << endl;
continue;
}
cout << i << "thread" << endl;
tmux.unlock();
this_thread::sleep_for(10ms);
}
}
int main() {
for (int i = 0; i < 3; i++) {
thread th(threadmain,i);
th.detach();
}
getchar();
}
三个线程依次打印thread,当某个线程超时等待1ms后,就会输出timeout。结果如下:
1.2.4 递归锁(可重入) recursive_mutex和recursive_timed_mutex用于业务组合
- 同一个线程中的一个锁可以锁多次,避免了一些不必要的死锁
- 组合业务 用到同一个锁
我们有时候会发现同一个锁存在多个函数当中,调用这些函数时,会出现同一个锁被调用多次,也就是同一个锁被锁多次,如果是我们用mutex这种普通锁的方式,在第二次锁的时候会抛出异常。有一个锁叫做递归锁,它是可以重入的,可重入的概念:一个锁被锁住,你第二次再调用同一个lock时不会报错,只会把个当前线程锁的次数加一,而且还是处于锁的状态。
为什么需要可重入锁的,而不是选择解锁之后重新加锁的原因是,当前任务可能需要同一个线程内完成任务,如果先解锁再加锁的话,可能会进入另外一个线程。当前线程的任务还没来得及完成就被释放了。下面是代码演示:
recursive_mutex rmutex;
void task1() {
rmutex.lock();
cout << "task1" << endl;
rmutex.unlock();
}
void task2() {
rmutex.lock();
cout << "task2" << endl;
rmutex.unlock();
}
void thread_rec(int i) {
for (;;) {
rmutex.lock();
task1();
task2();
this_thread::sleep_for(1s);
rmutex.unlock();
this_thread::sleep_for(10ms);
}
}
int main() {
for (int i = 0; i < 3; i++) {
thread th(thread_rec,i);
th.detach();
}
getchar();
}
值得注意的是加了几次锁就需要解几次锁。
1.2.4 共享锁 shared_mutex
- C++14共享超时互斥锁 shared_timed_mutex
- C++17 共享互斥锁 shared_mutex
假定有多个线程,有的线程要去读一个资源,有的线程需要去写资源。有的线程去读同一个资源的时候,其他线程可以同时读。但是当这个资源正在被修改,那么在读的多个线程就需要去等待。也就是说一个线程在写资源的时候,其他线程既不能读也不能写;如果一个线程在读的时候,其他所有的线程都可以读,但是线程不能写。所以这里就要涉及两个锁,一个读的锁,一个写的锁。如果这个线程只读的话,只需要一个读的锁,如果这个线程需要修改的时候,就需要先拿到读的锁,再去获得写的锁,然后再修改,修改之后再把两个锁释放掉。下面是一个泳道图:
当一个写进程没有获取到共享锁时将会阻塞等待,当一个读线程发现有写线程在修改资源时,将会阻塞等待。下面是代码演示:
#include<thread>
#include<iostream>
#include <mutex>
#include <shared_mutex>
using namespace std;
shared_timed_mutex stmux;
void threadread(int i) {
for (;;) {
stmux.lock_shared();
cout << i << "read" << endl;
this_thread::sleep_for(2000ms);
stmux.unlock_shared();
this_thread::sleep_for(10ms);
}
}
void threadwrite(int i) {
for (;;) {
stmux.lock();
cout << i << "write" << endl;
this_thread::sleep_for(1s);
stmux.unlock();
stmux.unlock_shared();
}
}
int main() {
for (int i = 0; i < 3; i++) {
thread th(threadread,i);
th.detach();
}
for (int i = 0; i < 3; i++) {
thread th(threadwrite, i);
th.detach();
}
getchar();
}
而如果共享锁和互斥锁嵌套,将会死锁
void threadwrite(int i) {
for (;;) {
stmux.lock_shared();
stmux.lock();
cout << i << "write" << endl;
this_thread::sleep_for(1s);
stmux.unlock();
stmux.unlock_shared();
this_thread::sleep_for(10ms);
}
}
当有线程读时,写线程无法获得lock(),将会进入阻塞,上述代码中会造成死锁,导致其他读线程无法访问资源。