mutex库
不加锁会出现的问题
我们写出下面的代码 : 创建两个线程 让这两个线程执行同一个任务 打印0~99的数字
void func(int n) { for (int i = 0; i < n; i++) { cout << this_thread::get_id() << " : " << i << endl; } } int main() { thread t1 = thread(func, 100); thread t2 = thread(func, 100); t1.join(); t2.join(); return 0; }
我们会发现打印的时候出现这种情况
这是因为我们没有给线程加锁 在一个线程运行到一半的时候时间到了 切换到另一个线程的输出
解决的方式就是通过加锁
我们首先在定义一个锁 mtx
mutex mtx;
并且在任务开始之前加锁 任务开始之后解锁
mtx.lock(); for (int i = 0; i < n; i++) { cout << this_thread::get_id() << " : " << i << endl; } mtx.unlock();
线程运行就不会出现上面的问题了
C++线程库的引用问题
在C++的线程库当中 我们不能直接使用引用传递参数 否则一些编译器会报错 (博主使用的vs2022) 一些编译器虽然编译通过了 但是没有引用的效果 (我们老师使用vs2019出现上述效果)
如果说我们要往线程调用函数中传引用 则我们需要使用到这样的一个函数
ref(value)
代码和表示结果如下
void func(int n , int& x) { for (int i = 0; i < n; i++) { mtx.lock(); cout << this_thread::get_id() << " : " << i << endl; x++; mtx.unlock(); } } int main() { int x = 0; thread t1 = thread(func, 10, ref(x)); thread t2 = thread(func, 10, ref(x)); t1.join(); t2.join(); cout << x << endl; return 0; }
当然 我们使用全局锁是很不安全的 并且会污染命名空间 所以说最好我们把锁也定义在main函数当中 并且以参数的形式 引用传递给函数
注意!我们不能使用传值的形式传递锁 因为它的拷贝构造函数被禁用了
演示结果如下
函数指针替换
我们在线程传参的时候不光可以传函数指针 还可以传递仿函数和lamabda表达式(底层就是仿函数)等等
有关于lamadba表达式的内容大家可以参考我的这篇博客
原子操作相关
关于原子性的相关问题 我在Linux线程互斥中详细介绍了 大家可以参考我的这篇博客
如果大家阅读完了上面一篇博客之后就会知道 x++;
这并不是一个原子操作
要保证我们一个操作是原子的 除了加锁之外我们还可以使用C++提供的一个原子类
它的类名叫做 atomic
我们可以这么定义一个变量
atomic<int> x
此时我们使用 x++
就是一个原子操作了 也就不需要互斥锁了
为什么我们使用atomic之后x++就变成原子操作了呢?
这里其实和我们mysql当中的事务很类似
具体可以参考我的这篇博客
它将x++底层的三条汇编语言封装成了一个整体 要么成功 要么失败 (失败之后就回滚)
靠着事务保证了原子性
注意:
- 由于我们要保证原子性操作的资源一般是临界资源 所以说是不允许拷贝的 所以说atomic类中禁用了拷贝构造 移动构造等函数
条件变量库
学习到这个阶段我们应该有了一定自主学习的能力了
我们这里只介绍几个常用的函数 如果大家想深入学习以后可以直接在cspp网站上搜索函数学习
现在要求我们实现两个线程交替打印1-100
尝试用两个线程交替打印1-100的数字,要求一个线程打印奇数,另一个线程打印偶数,并且打印数字从小到大依次递增。
我们尝试使用上面学过的知识去做这道题
代码和表示结果如下
void func(mutex& mtx) { for (int i = 0; i < 100; i+=2) { mtx.lock(); cout << this_thread::get_id() << " : " << i << endl; mtx.unlock(); } } void func2(mutex& mtx) { for (int i = 1; i < 100; i += 2) { mtx.lock(); cout << this_thread::get_id() << " : " << i << endl; mtx.unlock(); } } int main() { mutex mtx; thread t1 = thread(func ,ref(mtx)); thread t2 = thread(func2, ref(mtx)); t1.join(); t2.join(); return 0; }
我们发现 虽然我们能够让两个线程分别打印奇数和偶数 但是却不能让它们依次执行
在解决了原子性之后就该我们的条件变量库上场了
我们一般会这样子定义一个条件变量
condition_variable cv;
然后我们用两个函数来使用它
template <class Predicate> void wait (unique_lock<mutex>& lck, Predicate pred);
参数说明:
- 第一个参数是一个互斥锁 我们使用之前定义的锁构造一个就可以
- 第二个参数是一个函数指针 它返回一个bool类型的参数 可以被反复调用 直到返回true为止
在线程进入等待状态之后会主动释放锁
void notify_one() noexcept;
我们可以使用该函数来唤醒处于等待状态的一个线程 唤醒处于等待状态的线程之后会自动获取锁
那么使用上面学的条件变量我们就可以修改我们的代码 使它完成功能
condition_variable cv; bool flag = true; bool Flag() { return flag; } bool UFlag() { return !flag; } void func(mutex& mtx) { unique_lock<mutex> lck(mtx); for (int i = 0; i < 100; i+=2) { cv.wait(lck, Flag); cout << this_thread::get_id() << " : " << i << endl; flag = false; cv.notify_one(); } } void func2(mutex& mtx) { unique_lock<mutex> lck(mtx); for (int i = 1; i < 100; i += 2) { cv.wait(lck, UFlag); cout << this_thread::get_id() << " : " << i << endl; flag = true; cv.notify_one(); } } int main() { mutex mtx; thread t1 = thread(func ,ref(mtx)); thread t2 = thread(func2, ref(mtx)); t1.join(); t2.join(); return 0; }
解释下上面的代码
- 首先我们创造一个
unique_lock
对象后它会自动调用lock()函数加锁 - 当我们调用
wait()
函数的时候会自动解锁 - 当我们调用
notify_one()
函数的时候会自动加锁
此外我们可以通过控制flag的初始值来控制哪一个线程先行动
运行结果如下