条件变量std::condition_variable
- 在前面使用互斥量实现从消息队列中读写数据的代码中,从消息队列读取元素(消费者)的时候,会先加锁,然后判断消息队列是否为空,当写数据(生产者)速度较慢时,读数据会不断的给共享数据加锁,导致做了很多无用功,CPU占用率很高。
- 常用解决办法:双重锁定,消费者延时
bool outMsgLULproc(int& command)
{
if(!msgRecvQueue.empty())
{
std::lock_guard<mutex> guard(my_mutex);
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front();
msgRecvQueue.pop_front();
return true;
}
return false;
}
}
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; i++)
{
bool result = outMsgLULproc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
}
else
{
cout << "outMsgRecvQueue()执行,目前消息队列为空" << endl;
this_thread::sleep_for(chrono::milliseconds(500));
}
}
}
- 更好的解决办法:条件变量std::condition_variable。条件变量需要和互斥量一起使用,其中有两个重要的接口,
notify_one()
和wait()
。wait()
可以让线程陷入休眠状态,在消费者生产者模型中,如果生产者发现队列中没有东西,就可以让自己休眠,但是不能一直不干活啊,notify_one()
就是唤醒处于wait
中的条件变量。 - 当
wait()
被唤醒之后,会不断尝试重新获取互斥量锁,如果获取不到,那么就会阻塞着等着获取,如果获取到了锁,那么就判断第二个参数是否为true
,如果为true
则流程继续执行,否则释放该锁,继续休眠。如果wait()
没有第二个参数,则跟true
是一样的,流程继续执行。
class A
{
public:
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; i++)
{
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
unique_lock<mutex> guard(my_mutex);
msgRecvQueue.push_back(i);
my_cond.notify_one();
}
}
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; i++)
{
unique_lock<mutex> guard(my_mutex);
my_cond.wait(guard, [this] {
if (!msgRecvQueue.empty())
return true;
return false;
});
int command = msgRecvQueue.front();
msgRecvQueue.pop_front();
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
}
}
private:
list<int> msgRecvQueue;
mutex my_mutex;
condition_variable my_cond;
};
int main()
{
A a;
thread inThread(&A::inMsgRecvQueue, std::ref(a));
thread outThread(&A::outMsgRecvQueue, std::ref(a));
inThread.join();
outThread.join();
return 0;
}
注意
- 使用的是
std::unique_lock
而不是std::lock_guard
,wait()
函数会先调用互斥锁的unlock()
函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面的队列操作。而lock_guard
没有lock
和unlock
接口,而unique_lock
提供了。 - 减小锁的粒度,在使用
notify_one()
唤醒条件变量之后尽快将锁unlock()
notify_one()
不一定绝对会把另一个线程的wait()
唤醒,当另一个线程在处理其他事物时,没有堵塞在wait()
等待被唤醒时,此时的notify_one()
就无效。wait()
也有可能被虚假唤醒,比如多次调用notify_one()
或者notify_all()
多个wait()
时使得其中一个从消息队列中取出了元素导致其他wait()
再去取元素时消息队列已经为空。解决虚假唤醒的重要方法是给wait()
带上第二个判断的参数,如上面代码段所示。
std::async, std::future
int my_func()
{
cout << "function start threadid = " << std::this_thread::get_id() << endl;
std::this_thread::sleep_for(chrono::seconds(5));
cout << "function end threadid = " << std::this_thread::get_id() << endl;
return 5;
}
int main()
{
std::future<int> result = std::async(my_func);
std::future_status status = result.wait_for(std::chrono::seconds(6));
if(status==std::future_status::timeout)
{
cout << "超时,线程还没执行完毕" << endl;
cout << result.get() << endl;
}
else if (status == std::future_status::ready)
{
cout << "线程执行完毕,返回" << endl;
cout << result.get() << endl;
}
else if (status == std::future_status::deferred)
{
cout << "线程被延迟执行" << endl;
cout << result.get() << endl;
}
return 0;
}
-
std::async()
创建一个异步任务并开始执行,实现与线程函数的绑定,返回一个std::future
对象。
-
std::future
对象的get()
成员函数等待线程执行结束并返回结果。
-
std::future
对象的wait()
成员函数只是等待线程返回,本身并不返回结果。
-
std::future
对象只能调用一次get()
函数,这是因为get()
是一个移动语义,在get()
之后future
的值就为空了。要想能够在多个线程中多次get()
,可以使用std::shared_future
对象。
std::launch::async和std::launch::deferred参数
std::async()
函数可以传入两个参数,std::launch::async
和std::launch::deferred
,前者是创建一个新线程并立即执行线程入口函数,后者表示线程入口函数等到std::future
的get()
或wait()
函数调用时才执行,但是线程根本不会被创建!- 如果传入的参数是
std::launch::async | std::launch::deferred
,则系统会自动选择其中一种来运行。该参数也是std::async()
函数的默认参数。
与std::thread的区别
std::thread
创建线程,如果系统资源紧张,就会导致创建线程失败,程序会报异常崩溃std::async
创建线程,系统会自动选择创建运行方式,如果资源紧张,则会以同步的方式执行线程入口函数,即以std::launch::deferred
为参数执行。
std::packaged_task, std::promise
std::packaged_task
int my_func1(int var)
{
cout << "my_func1 start threadid = " << std::this_thread::get_id() << endl;
cout << var << endl;
std::this_thread::sleep_for(chrono::milliseconds(5000));
cout << "my_func1 end threadid = " << std::this_thread::get_id() << endl;
return 5;
}
void my_func2(std::future<int>& tmpf)
{
cout << "my_func2 start threadid = " << std::this_thread::get_id() << endl;
auto result = tmpf.get();
cout << "my_func2 result = " << result << endl;
return;
}
int main()
{
std::packaged_task<int(int)> mypt(my_func1);
std::future<int> result = mypt.get_future();
std::thread t1(std::ref(mypt), 1);
std::thread t2(my_func2, std::ref(result));
t1.join();
t2.join();
return 0;
}
std::package_task
是个类模板,模板参数是各种可调用对象,他将可调用对象包装起来,方便将来作为线程入口函数来调用。std::packaged_task
包装起来的可调用对象也可以直接调用,所以packaged_task
对象也是一个可调用对象。
int main()
{
std::packaged_task<int(int)> mypt1(my_func);
mypt1(10);
std::future<int> result = mypt1.get_future();
cout << result.get() << endl;
vector<std::packaged_task<int(int)>> my_task;
std::packaged_task<int(int)> mypt2(my_func);
my_task.push_back(std::move(mypt2));
auto iter = my_task.begin();
std::packaged_task<int(int)> mypt3;
mypt3 = std::move(*iter);
my_task.erase(iter);
mypt3(15);
std::future<int> result2 = mypt3.get_future();
cout << result2.get() << endl;
return 0;
}
std::promise
- 能够在某个线程中给他赋值,然后在其他线程中取出来,实现线程之间的参数传递
- 通过std::promise保存一个值,通过
std::future
绑定到这个std::promise
上来得到这个绑定的值
void Thread_Fun1(std::promise<int>& p)
{
std::this_thread::sleep_for(std::chrono::seconds(5));
int iVal = 233;
std::cout << "传入数据(int):" << iVal << std::endl;
p.set_value(iVal);
}
void Thread_Fun2(std::future<int>& f)
{
auto iVal = f.get();
std::cout << "收到数据(int):" << iVal << std::endl;
}
int main()
{
std::promise<int> myprom;
std::future<int> fu1 = myprom.get_future();
std::thread t1(Thread_Fun1, std::ref(myprom));
std::thread t2(Thread_Fun2, std::ref(fu1));
t1.join();
t2.join();
return 0;
}
原子操作std::atomic
- 原子操作是一种不需要用到互斥量加锁(加锁)技术的多线程编程方式。
- 原子操作是指”不可分割的操作“。
- 原子操作比互斥量效率要高。
- 互斥量加锁一般针对一个代码段,而原子操作一般针对的是一个变量。
int icount = 0;
void my_func()
{
for (int i = 0; i < 100000; i++)
icount++;
return;
}
int main()
{
std::thread t1(my_func);
std::thread t2(my_func);
t1.join();
t2.join();
cout << icount << endl;
return 0;
}
- 按理说,上述两个线程运行完之后icount的值应为200000,但程序输出并不是,说明一个简单的自增运算在多线程中也需要加锁。
- 如果使用std::mutex在数据自增前加锁,自增后解锁,可以保证自增运算的完整性,使程序输出结果符合预期,但是这样效率太低。
- 使用原子操作可以较好地解决上述问题:
std::atomic<int> icount = 0;
- 注意:原子操作支持的操作符:++,–,+=,&=,|=,^=等复合运算符,像下面这样是不对的!
void my_func()
{
for (int i = 0; i < 100000; i++)
icount = icount + 1;
return;
}