线程的状态
线程拥有的状态:
- 初始化:准备创建线程。
- 就绪:等待线程执行。
- 运行:调用该线程,执行逻辑。
- 阻塞:该线程被挂起,等待重新唤醒,阻塞包括锁、事件、信号量阻塞,延迟等。
- 退出:结束线程并释放资源。
线程的竞争状态:
- 竞争状态:包括读写状态的竞争
- 临界区:读写数据的代码片段
为了保护临界区的状态,一般使用锁来控制。
使用锁
一般锁
一般可以使用锁来保证对临界区的控制:
1 |
|
这是锁最普通的用法,但上述逻辑存在bug:当锁使用完成后释放锁,在for中跳转到下一个逻辑是相当快的,就可能导致unlock后还未来得及释放就再次上锁,导致下一个循环逻辑阻塞。
因此可以延后来确保锁被释放:
1 | //... |
超时锁
超时锁可以记录锁的获取情况,如果获取到锁则返回true,大致使用如下:
1 |
|
递归锁
对于一些逻辑的处理,我们希望可以处理多次,因而需要多次加锁,对于一般的锁这是难以做到的,所以可以使用递归锁来实现该功能。
递归锁:通过增加计数来加锁,增加几次锁就到释放几次锁。
1 |
|
共享锁
一块数据可能不止一个线程在访问,可以多个线程同时读取,这是没有问题的;但若有线程此时对数据进行写操作自然是希望其它线程先暂停访问。
为了实现上述功能,可以使用共享锁来实现,在c++14中共享锁为shared_timed_mutex,而在C++17中为shared_mutex,这里使用C++14实现:
1 |
|
管理锁
在一段逻辑中可能会由于抛出异常或return语句等提前结束逻辑导致锁没有被unlock,所以为了防止死锁就需要对锁进行一些手动或自动管理。基于RAII这一特性。
手动管理
我们可以使用局部对象离开作用域时自动调用析构函数的特性来实现手动管理。
所以选择对锁进行封装:
1 | class Mutex |
这是我们自己实现的手动管理锁,在C++中也为我们提供了现成的方法。
使用C++提供的管理方式
C++为我们提供了几种锁来管理:
lock_guard
lock_guard提供了两种构造函数,只要传入对象包含lock和unlock函数
该锁功能包括:
- 自动加锁,只要包装对象拥有lock和unlock函数
- 不支持对象所有权转移,即没有拷贝构造和赋值运算
- 可以使用adopt_lock不上锁
1 | static mutex mtx; |
unique_lock
unique_lock包括:
- 支持临时释放锁
- 使用adopt_lock,即用有锁则不加锁
- 支持defer_lock,延后拥有锁
- 支持tryto_lock,尝试获得锁所有权而不阻塞,获取失败不会释放,通过owns_lock判断是否获取到锁
- 可以转换所有权
1 | staic mutex mtx; |
shared_lock
从C++14开始支持。
shared_lock支持移动,即可以通过赋值一脚锁所有权。
1 | static shared_time_mutex smtx; |
scoped_lock
这是C++17中新增的lock,用于两个线程之间的锁
1 | static mutex mtx1; |
如果两个线程同时运行可能会由于时间差导致死锁,一般可以使用lock同时锁:
1 | lock(mtx1,mtx2); |
使用C++17提供的解决方案:
1 | scope_lock Lock(mtx1,mtx2); |
这是一个多参数构造函数,会为提供的锁同时加锁,离开作用域时自动释放
- 本文作者: KongXinQing
- 本文链接: https://13114987559.github.io/2024/03/27/note/C++多线程入门:多线程通信与同步/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!