您现在的位置是:首页 >技术教程 >【C++】线程库网站首页技术教程
【C++】线程库
文章目录
一、thread类
线程库是C++11标准提出来的,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的线程,必须包含< thread >头文件。
常见接口:
函数名 | 功能 |
---|---|
thread() | 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程 |
get_id() | 获取线程id |
jion() | 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行 |
detach() | 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关 |
注意:
1️⃣ 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的
状态。
2️⃣ 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。
3️⃣ get_id()的返回值类型为id类型,id类型实际为std::thread命名空间下封装的一个类。
4️⃣ 当创建一个线程对象后,并且给线程关联线程函数,该线程就被启动,与主线程一起运行。线程函数一般情况下可按照以下三种方式提供:
函数指针
lambda表达式
函数对象
使用:
#include <iostream>
#include <thread>
#include <vector>
#include <windows.h>
using namespace std;
void fun()
{
Sleep(1100);
cout << this_thread::get_id() << endl;
}
int main()
{
thread t1([]() {
while (true)
{
Sleep(1000);
cout << this_thread::get_id() << endl;
}
});
thread t2(fun);
t1.join();
t2.join();
return 0;
}
二、线程安全问题
static int val = 0;
void fun1(int n)
{
for (int i = 0; i < n; i++)
{
val++;
}
}
void fun2(int n)
{
for (int i = 0; i < n; i++)
{
val++;
}
}
int main()
{
thread t1(fun1, 100000);
thread t2(fun2, 200000);
t1.join();
t2.join();
cout << val << endl;
return 0;
}
可以看到本来应该加到300000,现在却没有加到。
因为val++操作并不是原子的。
2.1 加锁
为了保证线程安全我们需要加锁:
static int val = 0;
mutex mtx;
void fun1(int n)
{
mtx.lock();
for (int i = 0; i < n; i++)
{
val++;
}
mtx.unlock();
}
void fun2(int n)
{
mtx.lock();
for (int i = 0; i < n; i++)
{
val++;
}
mtx.unlock();
}
int main()
{
thread t1(fun1, 100000);
thread t2(fun2, 200000);
t1.join();
t2.join();
cout << val << endl;
return 0;
}
这里注意加锁和解锁放在for循环外边比较好,因为加锁和解锁的过程也是要消耗资源的。
这里如果两个线程同时调用一个函数也可以。
这里的原因是每个线程都会有独立的栈结构来保存私有数据。
2.2 CAS操作
CAS全称compare and swap,JDK提供的非阻塞原子性操作,它通过硬件保证了更新操作的原子性。它允许多线程非阻塞地对共享资源进行修改,但是同一时刻只有一个线程可以修改,其他线程并不会阻塞而是重新尝试。
2.3 原子性操作库(atomic)
原子性操作库就提供了CAS的相关接口。
如果要使用先得引入头文件:
#include <atomic>
atomic<int> val = 0;
void fun1(int n)
{
for (int i = 0; i < n; i++)
{
val++;
}
}
void fun2(int n)
{
for (int i = 0; i < n; i++)
{
val++;
}
}
int main()
{
thread t1(fun1, 100000);
thread t2(fun1, 200000);
t1.join();
t2.join();
cout << val << endl;
return 0;
}
这里有val++会被放弃,以此来保证线程安全
而在实际中要尽量避免使用全局变量。
int main()
{
atomic<int> val = 0;
auto func = [&](int n) {
for (int i = 0; i < n; i++)
{
val++;
}
};
thread t1(func, 10000);
thread t2(func, 20000);
t1.join();
t2.join();
cout << val << endl;
return 0;
}
三、锁
3.1 lock与try_lock的区别
lock的加锁过程:如果没有锁就申请锁,如果其他线程持有锁就会阻塞等待,知道其他线程unlock。
而try_lock就可以不让线程阻塞,如果申请不了就可以去干其他的事情。成功返回true,失败返回false。
if (try_lock())
{
// ...
}
else
{
// 干其他的事情
}
3.2 recursive_mutex递归锁
如果在递归函数中我们想要正常用lock加锁,很可能能会导致死锁。因为上锁后递归到下一层,锁并没有被解开,相当于自己上了锁以后又申请锁。
void fun()
{
lock();
fun();// 递归
unlock();
}
而使用recursive_mutex就可以避免这种情况。
原理:
递归到下一层后遇到加锁,就先判断线程的id值,如果一样就不用加锁,直接走接下来的流程。
3.3 lock_guard RAII锁
什么是RAII?
是C++语言的一种管理资源、避免泄漏的惯用法。利用的就是C++构造的对象最终会被销毁的原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。
我们知道有可能加锁后会抛出异常,那么就可能会导致锁没有被释放。为了避免这种情况,我们可以把锁封装一下,在析构函数中就可以加上解锁,这样出了作用域就可以自动销毁。
具体的实现在【linux】线程的互斥与同步 2.5 锁的封装。
而线程库给我们提供了这样一把锁。
int main()
{
int val = 0;
mutex mtx;
auto func = [&](int n) {
lock_guard<mutex> lock(mtx);
for (int i = 0; i < n; i++)
{
val++;
}
};
thread t1(func, 10000);
thread t2(func, 20000);
t1.join();
t2.join();
cout << val << endl;
return 0;
}
3.4 unique_lock主动解锁
它和lock_guard的区别就是lock_guard只能实现RAII,而unique_lock能主动把自己的锁解开,不用等到析构。
四、两个线程交替打印1~100
现在我们想让两个线程交替打印从1到100,一个线程打印奇数,一个线程打印偶数。
int main()
{
int val = 1;
thread t1([&]() {
while (val < 100)
{
if (val % 2 != 0)
{
cout << "thread 1" << "->" << val << endl;
val++;
}
}
});
thread t2([&]() {
while (val <= 100)
{
if (val % 2 == 0)
{
cout << "thread 2" << "->" << val << endl;
val++;
}
}
});
t1.join();
t2.join();
return 0;
}
虽然这可以做到要求,但是可能会造成资源浪费。
有这样一种场景,t2满足条件正在运行,但是时间片到了,切换到t1,此时t1不满足条件,一直在while处死循环,知道时间片到了才切换出去。
这样就会导致浪费CPU资源。
所以我们希望两个线程能够相互通知,这就需要条件变量控制。
4.1 条件变量
关于条件变量的概念在【linux】线程的互斥与同步 3.1 条件变量里面有详细介绍。
而C++11也对条件变量进行了封装。
头文件:#include <condition_variable>
相关接口:
而我们知道条件变量不是线程安全的,所以要先加一把锁。
这里注意使用wait的时候必须把锁传递进去,而且必须是unique_lock
。
wait把锁传进去是为了解锁,返回时才会重新上锁。
int main()
{
int val = 1;
mutex mtx;
condition_variable cv;
thread t1([&]() {
while (val < 100)
{
unique_lock<mutex> lock(mtx);
while (val % 2 == 0)
{
cv.wait(lock);// 阻塞
}
cout << "thread 1" << "->" << val << endl;
val++;
cv.notify_one();
}
});
thread t2([&]() {
while (val <= 100)
{
unique_lock<mutex> lock(mtx);
while (val % 2 != 0)
{
cv.wait(lock);
}
cout << "thread 2" << "->" << val << endl;
val++;
cv.notify_one();
}
});
t1.join();
t2.join();
return 0;
}
分析:
刚开始t1申请到锁,那么t2就会在申请锁的地方阻塞等待。但是t1却不满足条件,所以进行wait等待,进入wait函数会自动解锁,那么t2就可以运行了。这里注意notify_one()可能要唤醒的是一个正在运行的线程,但是没问题,此时notify_one()默认什么都不会做。