在多个线程访问同一数据或资源时,会造成数据竞争,此时需要加锁来保证各个线程独立有序运行,互不造成干扰。
基本语法(不建议使用,因为无法处理受保护代码发生异常时的情况):
std::mutex mu; //定义互斥锁
mu.lock(); //加锁
受保护的代码
mu.unlock(); //解锁
示例:使用mutex互斥锁防止主线程main函数和新建线程t1竞争同一资源cout
1)未加锁:多线程竞争同一资源
#include
#include
using namespace std;
void function_1()
{
for (int i = 0; i > -100; --i)
cout << "From t1: " << i << endl;;
}
int main()
{
thread t1(function_1);
for (int i = 0; i < 100; ++i)
cout << "From main: " << i << endl;
t1.join();
return 0;
}
2) 使用互斥锁mutex:资源在同一时间只被一个线程使用
#include
#include
#include
using namespace std;
mutex mu;
void shared_print(string msg, int id)
{
mu.lock();
cout << msg << id << endl;
mu.unlock();
}
void function_1()
{
for (int i = 0; i > -100; --i)
shared_print(string("From t1: "), i);
}
int main()
{
thread t1(function_1);
for (int i = 0; i < 100; ++i)
shared_print(string("From main: "), i);
t1.join();
return 0;
}
1) lock_guard:管理互斥锁对象,使其总保持上锁状态。
( cpp reference: A lock guard is an object that manages a mutex object by keeping it always locked.)
在构造时,互斥对象被调用线程锁定,并且在销毁时,互斥锁被解锁。它是最简单的锁,特别适用于具有自动持续时间的对象,该持续时间一直持续到其上下文结束。通过这种方式,它可以保证在抛出异常时正确解锁互斥对象。
使用lock_guard
std::mutex mu; //定义互斥锁
mu.lock(); //加锁
受保护的代码
mu.unlock(); //解锁
上述方式中, 如果保护代码抛出异常,那么互斥锁mu将永远不会解锁,所以不建议直接使用mu.lock()和mu.unlock()。而是使用
std::mutex mu; //定义互斥锁
lock_guard<mutex> guard(mu); //加锁
受保护的代码
但是此时cout并不在mu的保护下,其他的系统资源仍然可以使用,所以我们做如下改进:将输出置于互斥锁的保护下,此时不通过互斥量m_mutex将无法访问输出of。
(但此时不能将of 的访问接口暴露到类LogFile外,否则将不受互斥锁保护)
#include
#include
#include
#include
#include
using namespace std;
class LogFile {
mutex m_mutex;
ofstream of;
public:
LogFile(){
of.open("log.txt");
}
void shared_print(string id, int value){
lock_guard<mutex> locker(m_mutex);
of << "From" << id << " : " << value << endl;
}
//never return of outside the world , ex:
//ofstream& getStream() {return of;}
};
void function_1(LogFile& log){
for (int i = 0; i > -100; --i)
log.shared_print(string("From t1: "), i);
}
int main(){
LogFile log;
thread t1(function_1,ref(log));
for (int i = 0; i < 100; ++i)
log.shared_print(string("From main: "), i);
t1.join();
return 0;
}
死锁示例:
#include
#include
#include
#include
#include
using namespace std;
class LogFile {
mutex mutex1;
mutex mutex2;
ofstream of;
public:
LogFile() {
of.open("log.txt");
}
void shared_print1(string id, int value) {
lock_guard<mutex> locker1(mutex1);
lock_guard<mutex> locker2(mutex2);
cout << "From "<<id << " : " << value << endl;
}
void shared_print2(string id, int value) {
lock_guard<mutex> locker2(mutex2);
lock_guard<mutex> locker1(mutex1);
cout << "From " << id << " : " << value << endl;
}
};
void function_1(LogFile& log)
{
for (int i = 0; i > -100; --i)
log.shared_print1(string("From t1: "), i);
}
int main()
{
LogFile log;
thread t1(function_1, ref(log));
for (int i = 0; i < 100; ++i)
log.shared_print2(string("From main: "), i);
t1.join();
return 0;
}
上述例子中:线程t1调用shared_print1,给互斥锁mutex1上锁,在t1给mutex2上锁之前,若主线程main函数调用shared_print2并给mutex2上锁。此时t1等待主线程给mutex2解锁,主线程等待t1给mutex1解锁,这样就造成了死锁。
对应死锁产生的四个条件
① 互斥条件:一个资源一次只能被一个线程(进程)所使用
② 请求与保持条件:一个线程(进程)已占有一个资源,又请求别的资源,但请求的资源已被其他线程(进程)占用,此时请求被阻塞时,对已占有的资源保持不放
③ 不剥夺条件:线程(进程)已获得的资源,在未使用完之前不能被强行剥夺
④ 循环等待条件:若干线程(进程)形成一条首尾相连的循环等待资源关系。
例子: lock_guard locker1(mutex1, adopt_lock);
adopt_lock:告诉locker1互斥锁mutex1已经加锁,你所需要的做的是采用该mutex1的所有权
#include
#include
#include
#include
#include
using namespace std;
class LogFile {
mutex mutex1;
mutex mutex2;
ofstream of;
public:
LogFile() {
of.open("log.txt");
}
void shared_print(string id, int value) {
lock(mutex2, mutex1); //锁住mutex1和mutex2
lock_guard<mutex> locker1(mutex1, adopt_lock); //先锁住mutex1
lock_guard<mutex> locker2(mutex2, adopt_lock); //然后锁住mutex2
cout << "From " << id << " : " << value << endl;
}
void shared_print2(string id, int value) {
lock(mutex1, mutex2);
lock_guard<mutex> locker2(mutex2, adopt_lock); //先锁住mutex2
lock_guard<mutex> locker1(mutex1, adopt_lock); //再锁住mutex1
cout << "From " << id << " : " << value << endl;
}
};
void function_1(LogFile& log){
for (int i = 0; i > -100; --i)
log.shared_print(string("From t1: "), i);
}
int main()
{
LogFile log;
thread t1(function_1, ref(log));
for (int i = 0; i < 100; ++i)
log.shared_print2(string("From main: "), i);
t1.join();
return 0;
}
1) Unique_lock相比lock_guard更具弹性:
但是Unique_lock会消耗更多的计算机性能
基本语法:unique_locklocker(m_mutex, defer_lock);
#include
#include
#include
#include
#include
using namespace std;
class LogFile {
mutex m_mutex;
ofstream of;
public:
void shared_print(string id, int value)
{
unique_lock<mutex>locker(m_mutex, defer_lock); //defer_lock的作用是告诉locker m_mutex并未被锁住,unique_lock会消耗更多的计算机性能
//do something else
locker.lock(); //在需要的时候上锁
cout << "From " << id << " : " << value << endl;
locker.unlock();
//.......
locker.lock(); //解锁后重新上锁
unique_lock<mutex>locker2 = move(locker); //unique_lock可以在线程之间移动,而lock_guard不能
}
};
2) 懒惰初始化:只在线程第一次使用shared_print时打开log.txt
class LogFile {
mutex m_mutex;
//mutex m_mutex_open;
once_flag m_flag;
ofstream of;
public:
LogFile() {
} //Need destructor to close file
void shared_print(string id, int value)
{
/*{
unique_locklocker(m_mutex_open, defer_lock);
if (!of.is_open())
of.open("log.txt");
}*/
call_once(m_flag, [&]() { of.open("log.txt"); } ); //log.txt只会被一个线程打开而且只打开一次
unique_lock<mutex>locker(m_mutex, defer_lock); //defer_lock的作用是告诉locker m_mutex并未被锁住
of << "From " << id << " : " << value << endl;
}
};
this_thread::sleep_for(std::chrono::milliseconds(3));
chrono::steady_clock::time_point tp= chrono::steady_clock::now() +chrono::milliseconds(4);
std::this_thread::sleep_until(tp); //保持休眠直到时间点tp
mutex m;
std::unique_lock<muetx> ulocker(mu);
ulocker.try_lock(); //尝试上锁,不成功则return
ulocker.try_lock_for(chrono::nanoseconds(500)); //在500ns内尝试上锁,超出时间不成功return
ulock.try_lock_until(tp); //在时间点tp之前尝试上锁
//condition_variable future
cond.wait_for(ulocker,chrono::microseconds(2));
cond.wait_until(ulocker,tp);
fu.wait_for(chrono::milliseconds(2));
fu.wait_until(tp);
1.基本语法:
1)声明条件变量
condition_variable cond;
2)使用条件变量激活等待的线程
3)线程等待的实现
(直接休眠直到被cond.notify_one()或者cond.notify_all()唤醒,唤醒时不要额外条件)
2.为什么wait函数需要locker作为参数:因为线程在上锁后不应该休眠,休眠的进程也不需要上锁这个操作。
3.wait 在休眠前调用locker.unlock()解锁locker。在被其他线程唤醒后调用locker.lock()重新上锁 。因为需要多次实现上锁解锁操作,所以需要使用unique_lock而不能使用lock_guard.
#include
#include
#include
#include
#include
#include
using namespace std;
deque<int> q;
mutex mu;
condition_variable cond;
void function_1(){
int count = 10;
while (count > 0){
unique_lock<mutex>locker(mu);
q.push_front(count);
locker.unlock();
cond.notify_one(); //激活一个等待的线程(如果只有一个的话)
//cond.notify_all(); //激活所有等待的线程
this_thread::sleep_for(chrono::seconds(1));
count--;
}
}
void function_2(){
int data = 0;
while (data != 1){
unique_lock<mutex> locker(mu);
cond.wait(locker, []() {return !q.empty(); }); //(先将locker解锁然后)将线程2休眠,直到线程1调用notify_one才会将线程2唤醒后再重新上锁
data = q.back();
q.pop_back();
locker.unlock();
cout << "t2 got a value from t1: " << data << endl;
}
}
int main(){
thread t1(function_1);
thread t2(function_2);
t1.join();
t2.join();
return 0;
}
头文件简介:
Classes
std::future
std::future_error
std::packaged_task
std::promise
std::shared_future
Functions
std::async
std::future_category
简单的说,std::future提供了一种访问异步操作结果的机制
异步操作:在异步执行模式下,各语句执行结束的顺序与语句执行开始的顺序并不一定相同。通常一个异步操作我们是不能马上就获取操作的,只能在未来某个时候获取。
1) Future : 用来表示一个尚未有结果的对象,而产生这个结果的行为是异步操作
(Future表示“将来”你需要某些结果(一般是网络请求的结果),但是你现在就要发起这样的请求,并且这个请求会异步执行)
2) 可以通过查询future_status来获取异步操作的结果,future_status有3种状态:
① deferred:异步操作还没开始
② ready:异步操作已经完成
③ timeout:异步操作超时
3) 获取future结果有三种方式:
① get: 等待异步操作结束并返回结果
② wait: 只是等待异步操作完成,无返回值
③ wait_for: 超时等待返回结果
例子:
#include
#include
#include
using namespace std;
int factorial(int N){
int res = 1;
for (int i = N; i > 1; --i)
res = res*i;
cout << "result is: " << res << endl;
return res;
}
int main()
{
int x=1;
//future fu= async(launch::deferred,factorial,4); //async函数将不会创建子线程,而是延期启动factoria函数直到get被调用
future<int> fu = async(launch::async, factorial, 4); //async函数将创建一个新的子线程
//future fu = async(launch::async | launch::deferred, factorial, 4);
//等价于future fu = async(factorial,4),即是否创建子线程取决于实际情况
x = fu.get();
//fu.get(); //crash, cause a future can only get once
cout << "x becomes: " << x << endl;
return 0;
}
Promise : future 对象可以使用promise对象来创建(用get_future函数),创建后,promise保存的值可以被future对象读取 ,同时将两个对象的共享状态关联起来。可以认为promise为执行异步但是结果同步提供了一种手段。
1) promise成员函数
①std::promise::get_future:返回一个与promise共享状态相关联的future对象
②std::promise::set_value:设置共享状态的值,此后promise共享状态标识变为ready
③std::promise::set_exception:为promise设置异常,此后promise的共享状态标识变为ready
④std::promise::set_value_at_thread_exit:设置共享状态的值,但是不将共享状态的标志设置为 ready,当线程退出时该 promise 对象会自动设置为 ready(注意:该线程已设置promise的值,如果在线程结束之后有其他修改共享状态值的操作,会抛出future_error:(promise_already_satisfied)异常)
⑤std::promise::swap:交换 promise 的共享状态。
例子:
#include
#include
#include
#include
using namespace std;
int factoria(future<int>& fu){
int res = 1;
int N = fu.get(); //若没有给pro赋值,会抛出异常exception: std::future_errc::broken_promise
for (int i = N; i > 1; --i)
res *= i;
cout << "res is: " << res;
return res;
}
int main(){
int x;
promise<int> pro; //将来需要某些结果,先用promise对象
future<int> f = pro.get_future();
future<int> fu = async(launch::async, factoria, ref(f)); //告诉子线程我会给你传递一个value,
//但是现在我还没有这个value,你可以先执行factoria函数然后等待我的value
//do something else
pro.set_value(4); //遵守承诺给promise对象赋值
//pro.set_exception(make_exception_ptr(runtime_error("to err is human"))); //无法给pro赋值时设置异常
x = fu.get();
cout << "get from child: " << x << endl;
}
从上面的例子可以看出,通过promise对象和future对象,实现了先执行factoria()函数,再给函数传参数值的异步操作:
promise<int> pro; //将来需要某些结果,先用promise对象
future<int> f = pro.get_future();
future<int> fu = async(launch::async, factoria, ref(f));
//告诉子线程我会给你传递一个value,但是现在我还没有这个value,我会在之后给你发送,
这是我的promise,你可以先执行你要做的事情然后等待我的数据
std::packaged_task包装了一个可调用的目标(如function, lambda expression, bind expression, or another function object),以便异步调用,它和promise在某种程度上有点像,promise保存了一个共享状态的值,而packaged_task保存的是一个函数
基本用法:
int factoria(int x){}
std::packaged_task<int(int)> t(factoria);
std::future<int> fu=t.get_future();
t(6);
实例:
#include
#include
#include
#include
#include
#include
#include
using namespace std;
deque<packaged_task<int()>> task_q; //主线程和thread_1都对task_q进行访问,会造成数据竞争,所以需要加锁
mutex mu;
condition_variable cond;
int factoria(int N)
{
int res = 1;
for (int i = N; i > 1; --i)
res *= i;
cout << "result is: " <<res<< endl;
return res;
}
void thread_1()
{
packaged_task<int()> t; //t是一个将被打包成一个package的任务,这个package可以在不同函数、对象、线程之间传递
{
unique_lock<mutex>locker(mu);
cond.wait(locker, [](){return !task_q.empty(); });
t = move(task_q.front()); //将任务队列deque的第一个package移动到t中
task_q.pop_front();
locker.unlock();
}
t(); //执行t
}
int main()
{
thread t1(thread_1);
packaged_task<int()> t(bind(factoria, 6)); //t是一个打包成package的任务,任务为返回6的阶乘
future<int> fu = t.get_future();
{
unique_lock<mutex> locker(mu);
task_q.push_back(move(t)); //将t压入deque中等待被取出执行,因为t在主线程中不在执行,所以将其移动至task_q队列
}
cond.notify_one(); //在将t压入task_q后将thread_1中相应的操作唤醒
cout << fu.get() << endl;
t1.join();
}
在这个例子中:
①主线程创建一个packaged_task,任务的功能是返回6的阶乘,然后将这个packged_task压入到队列task_q中;线程thread_1将packaged_task从队列中取出并且执行它。
②由于主线程和线程thread_1都涉及到对队列task_q的访问,会造成数据竞争,所以需要对主线程和thead_1对task_q操作的相应部分进行加锁。
③ 为了保证在主线程创建packaged_task并且将其压入task_q之后线程thread_1再对其进行操作,所以需要使用条件变量保证两个操作的先后执行不会乱序。
1)std::async大概的工作过程:
先将异步操作用std::packaged_task包装起来,然后将异步操作的结果放到std::promise中,这个过程就是创造future的过程。外面再通过future.get/wait来获取这个未来的结果。
可以说,std::async帮我们将std::future、std::promise和std::packaged_task三者结合了起来。
2)std::async的原型:
async(std::launch::async | std::launch::deferred, f, args...)
第一个参数是线程的创建策略,默认的策略是立即创建线程:
第二个参数是线程函数,后面的参数是线程函数的参数。
简单的例子:
std::future<int> f1 = std::async(std::launch::async, [](){ return 8; });
cout<<f1.get()<<endl; //output: 8
std::future<int> f2 = std::async(std::launch::async, [](){ cout<<8<<endl; });
f2.wait(); //output: 8
多线程时使用shared_future
#include
#include
#include
#include
using namespace std;
int factoria(shared_future<int> fu)
{
int res = 1;
int N = fu.get();
for (int i = N; i > 1; --i)
res *= i;
cout << "res is: " << res;
return res;
}
int main()
{
int x;
promise<int> pro;
future<int> f = pro.get_future();
shared_future<int> sf = f.share();
future<int> fu = async(launch::async, factoria, sf); //shared_future可以复制(即值传递)
future<int> fu2 = async(launch::async, factoria, sf);
future<int> fu3 = async(launch::async, factoria, sf);
pro.set_value(4);
x = fu.get();
cout << "get from child: " << x << endl;
}
参考:https://blog.csdn.net/jiange_zh/article/details/51602938