C++线程库
文章目录
- pthread_create
- C++中的thread类
- C++中的锁的使用
- recursive_mutex
- lock_guard的使用
- unique_lock
- C++中的原子操作
- atomic类
- CAS操作
pthread_create
bd对pthread_create的概念是
他们是基于POSIX的, 那什么是POSIX呢?
而windows下只是接口不一样,大同小异, 用到再学
C++中的thread类
c++11提供了一套线程相关的库, win和Linux中可以公共一套代码,(在早期是需要各写一份的)
通过对条件编译的处理, 分别支持win和Linux
thread的构造函数
创建线程, 等待线程
同时 打印线程ID
c++线程提供一个this_thread命名空间, 里面包含API可以实现当前线程信息的输出
在无锁的线程中, 放弃这个线程的占用也不会争夺, 主动先让给其他线程处理
当然, 多个参数也支持
线程是不支持拷贝构造的
但支持移动构造
//想创建几个创建几个
还可以这样:
当然, move 换成swap也行, swap底层也是move
总结:
1.带参的构造, 创建可执行线程
2.先创建空线程对象,移动构造或者是移动赋值, 把右值线程对象转移过去
lambda演示:(针对这个例子对用一个变量++, 或者–, 即类似抢票的实现)
C++中的锁的使用
线程与互斥锁:
mutex
lambda不加锁演示:
加锁代码:
结果演示:
使用函数指针代码需要传入方式不同:
传锁参数必须用ref(),这样传过去才是引用
否则会先传给构造函数, 然后在形参处的&是&的那个构造函数的拷贝
如果是不想改变, 传过去后接收是const type&为形参, 那么在实参处就不用使用ref()
也可以使用指针来进行传参也能解决
recursive_mutex
递归互斥锁:
在递归中直接使用mutex会死锁
所以引入了递归互斥锁recursive_mutex
timed_mutex:
解锁方式:unlock,
或到达某个时间点,
或到达某个时间段
recursive_timed_mutex:用法差不多
同样, 在c++同样会有死锁的情况发生(
有关死锁和锁悬置的不同
当一个线程持有锁并在临界区执行时,如果该线程因异常终止而未能释放锁,这会导致其他试图获取同一锁的线程永久阻塞,等待一个永远不会释放的锁。这种情况被称为锁悬置(或称为悬挂锁)而非严格意义上的死锁。
死锁是指两个或更多线程各自持有一个锁并等待另一个线程持有的锁,形成一个循环依赖,导致所有涉及的线程都无法继续执行的情况。而锁悬置则是单个线程由于异常终止而未释放锁,导致其他线程无法获取该锁,从而阻塞。
然而,锁悬置可能导致的后果类似于死锁,即程序中的某些线程将永远处于等待状态,无法继续执行.
)
所以使用RAII进行解决(将这个锁交给类的对象来管理, 释放是在对象析构时进行处理)
template <class Lock> //设计成模板可以适应多种锁机制:如时间锁 递归锁等
class LockGuard
{
public:LockGuard(Lock& mtx) //需要接收来自外部的锁, 所以这边是引用:_mtx(mtx){_mtx.lock();}~LockGuard() {_mtx.unlock();}
private:Lock& _mtx;//需要接收来自外部的锁, 所以这边是引用
};
void Print(size_t j, size_t n, mutex &mtx)
{for (size_t i = j; i < n; i++){LockGuard<mutex> a(mtx);cout << this_thread::get_id()<< ":" << i << endl;}
}
int main()
{mutex mtx;thread t1(Print, 10, 200, ref(mtx));thread t2(Print, 10, 200, ref(mtx));t1.join();t2.join();return 0;
}
这边需要注意:
有三类成员必须在初始化列表进行初始化:
const
引用
没有默认构造的自定义类型的成员变量
lock_guard的使用
当然, 库内有现成的lock_guard(), 不需要手搓,底层与我们写的一样
LockGuard a(mtx);
lock_guard a(mtx);
只能析构解锁
unique_lock
支持手动加锁,手动解锁
与timed_mutex配合
关于支持手动加锁,手动解锁的unique_lock在条件变量互斥方面的使用:
在如下场景:
在t1的1没结束之前wait(),这边是阻塞等待
或者是t2先到达, 要对他进行解锁(wait的时候会解锁), 让其他线程访问这个临界资源
然后就是与pthread一样的
经典题:
两个线程, 交替打印奇偶数:
如何借助条件变量来控制?怎么保证t1线程先打印?
如果使用两个条件变量, 那这个起始的1 2谁打印就无法控制
分析场景:
1.
t1先运行:最正常的情况, 因为flag是false, 所以不进入wait, 打印1, 然后修改flag为true, 首次进行条件唤醒, 但是没有进行wait的线程, 所以, 首次notify_one什么都不做, 然后t1进入第二次循环, 申请锁, 因为flag为true, 所以, 会在此时进行等待. 从t1第一次进入循环, 成功申请了锁到当前wait之前, 就算t2启动了, 他也会进行阻塞式等待, 因为锁只有一把,且已经被t1线程带走了, 所以t2就算被启动, 也在等待. 此时的t1进行wait, 那么会首先进行解锁, t2得到了锁, flag为true, ~flag就不进行wait, 打印值, flag被再次修改为false, 然后唤醒条件变量, t1被唤醒, 得到了锁, 同时t2到达第二次循环, 被锁再次阻塞, t1打印值, 然后又修改为true, 再次进行这个过程, 到他的下次循环, 进行wait, 然后两个线程就这样进行交替
总结:
C++中的原子操作
int i = 0;
++i;
这个++操作不是原子的, 内部实现不是一次性的
C++使用:
好处, 对于比较简单的这样的操作, 这种加锁解锁对比实际处理的数据是效率不高的, 这样适合用自旋锁(申请不到锁的时候会进行不同的策略, 但是c++没有自旋锁, 所以要自己造轮子), 而不是互斥锁,也可以使用try_lock的方式模拟自旋锁, 对数据量小的操作, 进行不断的try_lock(好了没, 好了没, 一直在询问), 但是当数据大时try_lock反而不如普通的lock, 如果非要使用try_lock就可以使用yield, 申请失败进行一个时间片的等待
atomic类
用来使操作原子化
原代码
相比加锁, 这样的方式更加的高效率
对该类型的变量进行相关的操作, 需要实现对应运算符的重载, 对内置类型是已经实现了++重载
这个内部就是类似自旋的操作
对他指定类型的打印要使用load接口
改进后:
CAS操作
相关文章
利用CAS的无锁编程:
两个线程对这个函数进行重入时, 这样的原子代码操作能保证他的安全性
目前不进行深入, 等经验丰富在手搓
atomic底层实现原子操作的实例:
#include <iostream>#include <thread>#include <atomic>using namespace std;int main(){size_t n1 = 100000;size_t n2 = 100000;atomic<size_t> x = 0;thread t1([&]() {for (size_t i = 0; i < n1; i++){size_t old, newval;do {old = x;newval = old + 1;} while (!atomic_compare_exchange_weak(&x, &old, newval));}});thread t2([&]() {for (size_t i = 0; i < n2; i++){size_t old, newval;do {old = x;newval = old + 1;} while (!atomic_compare_exchange_weak(&x, &old, newval));}});t1.join();t2.join();cout << x << endl;return 0;}