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(),将会进入阻塞,上述代码中会造成死锁,导致其他读线程无法访问资源。

posted @ 2024-01-18 16:43  fangwanglong  阅读(9)  评论(0编辑  收藏  举报