1、线程的创建和使用
引入线程库<thread>,其提供了thread类,创建一个线程即实例化一个该类的对象,以此进行线程的创建和使用。
#include<thread>
using namespce std;
1.1 创建线程
一个线程可以用一个 thread 类的对象来表示,thread类中重载了构造函数和移动拷贝构造
#include<iostream>
#include<thread>
using namespace std;
void funa()
{
cout << "thread_funa()" << endl;
}
int main()
{
//创建线程——构造函数
thread a(funa);
cout << a.get_id() << endl;
//移动拷贝构造
thread b(std::move(a));
cout << b.get_id() << endl;
b.join();
//移动拷贝构造进行资源的交换,所有要注释掉
//a.join();
cout << "main end" << endl;
}
注意: thread类无拷贝构造函数、无赋值运算符,即不能直接用一个事先定义好的thread对象拷贝构造另一个thread对象,也不能不能进行赋值操作。但可以将临时的thread对象赋值给另一个thread即移动拷贝构造,也可以移动赋值。
1.2 线程的使用
thread 提供的函数: 线程支持库
thread类常用的成员函数:
成员函数 | 含义 |
---|---|
get_id() | 获取当前thread对象的线程id |
joinable() | 判断当前线程是否活跃(是:true;否:false)即是否支持调用 join() |
join() | 阻塞当前thread 对象所在的线程,直至thread 对象表示的线程执行完毕 |
detach() | 将当前线程从调用该函数的线程中分离出去,他们彼此独立运行 |
swap() | 交换两个线程的状态 |
注意:每个thread 对象在调用析构销毁前,要么调用 join() 函数令主线程等待子线程执行完毕,要么调用detach() 函数将子线程和主线程分离,二者必选一,否则存在以下问题:
- 线程占用的资源无法全部释放,造成内存泄露
- 当主线程执行完毕而子线程未完时,程序执行引发异常。
在this_thread 命名空间的部分函数:
函数 | 含义 |
---|---|
get_id() | 获得当前线程的 ID |
yield() | 阻塞当前线程让出cpu,直至条件成熟 |
sleep_for() | 阻塞当前线程指定的时间 |
sleep_until() | 阻塞当前线程,直至某个时间点为止 |
例:
//休眠1000毫秒,1s
this_thread::sleep_for(chrono::milliseconds(1000));
2.实现线程同步
2.1互斥锁
互斥锁用 mutex 类(位于std命名空间中)的对象表示,定义在头文件<mutex>头文件中。
成员函数:lock()——加锁; unlock()——解锁
例:对临界资源变量n的访问
#include<mutex>
using namespcase std;
mutex m; //实例化mutex对象m
void Show_n()
{
m.lock();
cout << n << endl;
m.unlock();
}
实现严格基于作用域的互斥体所有权包装器:lock_fuard
原理:创建 lock_guard 对象时,它试图接收给定互斥的所有权。控制离开创建 lock_guard 对象的作用域时,销毁 lock_guard 并释放互斥。即,创建即加锁,作用域结束自动解锁。
#include<mutex>
using namespcase std;
mutex m; //实例化mutex对象m
void Show_n()
{
lock_guard<mutex> g1(m);
cout << n << endl;
}
//作用域结束,析构g1,m解锁
lock_gurad传入两个参数,第二个为adopt_lock标识时,表示该互斥量之前必须已经lock,才能使用此参数,故需要提前手动锁定。
#include<mutex>
using namespcase std;
mutex m; //实例化mutex对象m
void Show_n()
{
m.lock(); //手动锁定
lock_guard<mutex> g1(m, adopt_lock);
cout << n << endl;
}
//作用域结束,析构g1,m解锁
实现可移动的互斥体所有权包装器:unique_lock
与lock_guard类似,用法更丰富。
成员函数 | 含义 |
---|---|
lock() | 锁定 |
try_lock() | 尝试锁定,若不能,先去执行其他代码并返回false;可以,进行加锁并返回true |
unlock() | 解锁 |
unique_lock与lock_guard
unique_lock | lock_guard | |
---|---|---|
手动lock、unlock | 支持 | 不支持 |
参数 | 支持 adopt_lock/try_to_lock/defer_lock | 支持 adopt_lock |
try_to_lock: 尝试用mutx的lock()去锁定这个mutex,但如果没有锁定成功,会立即返回,不会阻塞在那里
defer_lock: 这个参数表示暂时先不lock,之后手动去lock,但是使用之前也是不允许去lock。
2.2 条件变量
C11提供的两种表示条件变量的类:
都定义在<condition_variable>头文件中,常与互斥锁搭配使用,避免线程间抢夺资源。
- condition_variable
该类表示的条件变量只能和unique_lock类表示的互斥锁搭配使用; - condition_variable_any
该类表示的条件变量可以与任意类型的互斥锁搭配使用(如:递归互斥锁、定时互斥锁等)
条件变量常用的成员函数
成员函数 | 功能 |
---|---|
wait() | 阻塞当前线程,等待条件成立 |
wait_for() | 阻塞当前线程的过程中,该函数会自动调用unlock()解锁,令其他线程使用公共资源。当条件成立或超过指定的等待时间,该函数自动调用lock()加锁,同时令线程继续执行 |
wait_until() | 与wait_for() 类似,区别是其可以设定一个具体的时间点,当条件成立或等待时间超过了指定的时间点,函数自动加锁,线程执行 |
notify_one() | 唤醒一个等待的线程 |
notify_all() | 唤醒所有等待线程 |
关于wait() :
调用wait()1先获得mutex,2线程被阻塞,当wait陷入休眠是会自动释放mutex。直到另外某个线程调用 notify_one或notify_all唤醒了当前线程。当线程被唤醒时,此时线程是已经自动占有了mutex。
3.例
1:两线程交替打印奇偶数
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
std::mutex data_mutex;
std::condition_variable data_var;
bool tag = true;
// odd : 1
// even: 2
// odd : 3
// even : 4
void printodd() // 打印奇数
{
std::unique_lock<std::mutex> ulock(data_mutex);
for (int odd = 1; odd <= 100; odd += 2)
{
while (!tag)
{
data_var.wait(ulock);
}
cout << "odd: " << odd << endl;
tag = false;
data_var.notify_all();
}
}
void printeven() //打印偶数
{
std::unique_lock<std::mutex> ulock(data_mutex);
for (int even = 2; even <= 100; even += 2)
{
while (tag)
{
data_var.wait(ulock);// 1 2
}
cout << "even: " << even << endl;
tag = true;
data_var.notify_all();
}
}
int main()
{
thread thodd(printodd);
thread theven(printeven);
thodd.join();
theven.join();
return 0;
}
参考文献:
C++多线程unique_lock详解