有时候线程之间是需要同步的,为了使线程同步,我们的方法有
1、设置一个共享标志,一个线程持续检查共享标志,一个线程更新标志,使用一个锁来保护这个标志
2、在方法一的基础上使用std::this_thread::sleep_for()让出CPU时间片,以降低不必要的时间损耗
#include //std::this_thread::sleep_for
#include //std::chrono::milliseconds
bool flag;
std::mutex m;
void wait_for_flag()
{
std::unique_lock lk(m); //加锁保护共享标志
while(!flag) //循环检测标志
{
lk.unlock(); // 解锁互斥量
std::this_thread::sleep_for(std::chrono::milliseconds(100)); //主动让出时间片
lk.lock(); // 锁互斥量以便检测标志/标志为真后带锁执行任务
}
do_sometihng();
}
3、使用条件变量
std::mutex mut;
std::queue data_queue; //线程间传递数据的队列
std::condition_variable data_cond; //需要同步的线程都需要使用同一个条件变量
void data_preparation_thread()
{
while(more_data_to_prepare())
{
data_chunk const data=prepare_data();
std::lock_guard lk(mut); //条件变量必须和锁一起使用
data_queue.push(data);
data_cond.notify_one(); //仅唤醒一个阻塞额线程
}
}
void data_processing_thread()
{
while(true)
{
std::unique_lock lk(mut); //必须使用std::unique_lock,以使条件变量在wait()时可以解锁,在wait()返回时可以上锁,而std::lock_guard只能在析构时解锁
data_cond.wait
(lk,[]{return !data_queue.empty();}); //第二个参数可以是任意可调用对象或函数,此处是lambda表达式
data_chunk data=data_queue.front(); //返回队头元素的引用
data_queue.pop();
lk.unlock(); //如果有多个处理数据的线程,这里的unlock()操作可以提高并发性
process(data);
if(is_last_chunk(data))
break;
}
}
关于为何使用条件变量需要使用一个锁的解释请看这里,关于为什么使用的锁是std::unique_lock而不是std::lock_guard我们需要看下对std::condition_variable::wait()的概述
unconditional (1) |
void wait (unique_lock |
predicate (2) |
template void wait (unique_lock |
The execution of the current thread (which shall have locked lck's mutex) is blocked until notified.
At the moment of blocking the thread, the function automatically calls lck.unlock(), allowing other locked threads to continue.
Once notified (explicitly, by some other thread), the function unblocks and calls lck.lock(), leaving lck in the same state as when the function was called. Then the function returns (notice that this last mutex locking may block again the thread before returning).
Generally, the function is notified to wake up by a call in another thread either to member notify_one or to member notify_all. But certain implementations may produce spurious wake-up calls without any of these functions being called. Therefore, users of this function shall ensure their condition for resumption is met.
If pred is specified (2), the function only blocks if pred returns false, and notifications can only unblock the thread when it becomes true (which is specially useful to check against spurious wake-up calls). This version (2) behaves as if implemented as:
while (!pred()) wait(lck);
当线程在条件变量wait()时,会调用锁的unlock()函数解锁以使其他线程得以安全完成任务并有机会通知当前线程,当条件变量收到通知wait()返回时又会上锁互斥量以便当前线程能安全完成任务,而利用RAII机制管理锁的两种机制中,只有std::unique_lock含有unlock()与lock()成员函数,std::lock_guard仅有构造函数与析构函数
wait()的第二个参数可以是任意函数或可调用对象(返回值需转化为bool类型),当第二个参数返回false时,会继续wait()阻塞当前线程,当接收到其他线程的通知且第二个参数为true后才会解除阻塞状态
由上图可知,第二个参数返回false时会再次wait,只有返回为true与收到通知同时满足才能真正解除阻塞状态
std::condition_variable::notify_one 解除一个因等待条件变量而阻塞的线程,如果没有线程正在等待,什么也不做,如果正在等待的线程超过一个,则哪个线程被解除阻塞是不确定的
再来一个例子
// condition_variable::wait (with predicate)
#include // std::cout
#include // std::thread, std::this_thread::yield
#include // std::mutex, std::unique_lock
#include // std::condition_variable
std::mutex mtx;
std::condition_variable cv;
int cargo = 0;
bool shipment_available() { return cargo!=0; }
void consume(int n) {
for (int i=0; i lck(mtx);
cv.wait(lck, shipment_available);
// consume:
std::cout << cargo << '\n';
cargo=0;
}
}
int main()
{
std::thread consumer_thread(consume, 10);
// produce 10 items when needed:
for (int i=0; i<10; ++i) {
while (shipment_available()) std::this_thread::yield();
std::unique_lock lck(mtx);
cargo = i+1;
cv.notify_one();
}
consumer_thread.join();
return 0;
}
例子中的std::this_thread::yield()请看这里
#include
#include
#include
#include
template
class threadsafe_queue
{
private:
mutable std::mutex mut; //必须声明为mutable
std::queue data_queue;
std::condition_variable data_cond;
public:
threadsafe_queue()
{}
threadsafe_queue(threadsafe_queue const& other)
{
//锁住other对象中的锁,防止其他线程在当前对象拷贝构造时对other中的数据进行修改
std::lock_guard lk(other.mut); //实际上mutable作用于此,由于other是const引用而lock_guard的拷贝构造函数需求非const引用,mutable实际上是作用于other对象中的互斥量成员,以使const引用得以被非const引用所接受,此处还有一个不太理想的做法:使用const_cas将other的const属性去掉
data_queue=other.data_queue;
}
void push(T new_value)
{
std::lock_guard lk(mut);
data_queue.push(new_value);
data_cond.notify_one();
}
void wait_and_pop(T& value)
{
std::unique_lock lk(mut);
data_cond.wait(lk,[this]{return !data_queue.empty();}); //注意,此处的empty()必须是队列的成员函数,而不是当前类的公有成员函数empty(),否则会出现两次对同一普通互斥量上锁,造成未定义错误
value=data_queue.front();
data_queue.pop();
}
std::shared_ptr wait_and_pop()
{
std::unique_lock lk(mut);
data_cond.wait(lk,[this]{return !data_queue.empty();});
std::shared_ptr res(std::make_shared(data_queue.front()));
data_queue.pop();
return res;
}
bool try_pop(T& value)
{
std::lock_guard lk(mut);
if(data_queue.empty)
return false;
value=data_queue.front();
data_queue.pop();
}
std::shared_ptr try_pop()
{
std::lock_guard lk(mut);
if(data_queue.empty())
return std::shared_ptr();
std::shared_ptr res(std::make_shared(data_queue.front()));
data_queue.pop();
return res;
}
bool empty() const
{
std::lock_guard lk(mut);
return data_queue.empty();
}
};
由上图可知,lock_guard的构造函数需求的是非const _Mutex引用
期望std::future<>可以用来获取异步任务的结果,也就是说可以将它作为一种线程间同步的手段
有效的期望是与一种共享状态(或者说期望状态)相关联的,通过函数std::async<>()、std::promise<>类成员函数get_future()、std::packaged_task<>类成员函数get_future()都可以获取到一个有效的期望,使用std::future<>类的默认构造函数构造的期望对象是无效的,除非通过移动操作移动了一个有效期望对象
通过在一个有效的std::future<>上调用成员函数get()可以获取异步任务(即一个线程)的执行结果:获取异步任务的返回值或是期望对象存储的异常(如果异步任务产生了异常),但如果期望对象的共享状态不为就绪(ready)函数get()的调用将会导致调用线程发生阻塞,直到对象的共享状态变为ready
需要注意的是1、对同一std::future<>对象使用两次get()是不合理的,这将会引发一个异常
2、当期望状态不为ready时,就调用std::promise<>或std::packaged_task<>的析构函数,期望对象将会存储一个std::future_error异常(此异常继承自标准异常的logic_error)
联合使用期望与函数模板std::async<>()可以使我们以一种更为简单的方式获取异步任务的执行结果,这是相对于std::thread来说的,因为std::thread并不直接提供返回异步任务结果的机制,虽然我们可以通过引用或指针的方式返回,但使用这两种方式更改麻烦不说,还需要注意被指向或被引用对象的生命周期
unspecified policy (1) |
template future async (Fn&& fn, Args&&... args); |
specific policy (2) |
template future async (launch policy, Fn&& fn, Args&&... args); |
std::async<>()通过Fn开启一次异步任务,它返回一个std::future<>对象,通过返回的期望对象的成员函数get()就可以获取异步任务的执行结果了。当异步任务执行完毕后,期望对象的共享状态变为ready
Fn 可以是函数指针、或者任何拥可移动构造(即可由右值参数构造)的函数对象、可调用对象。Fn被调用后的返回值通过std::async<>()返回的期望通过get()获取,当Fn执行时抛出了异常,异常会被存储在期望当中,并在get()时再次抛出
值得注意的是,即使Fn的返回值类型为void,std::async<>()的返回值也不能忽略,即应使用std::future
args 传递给Fn的参数,需要可移动构造,当Fn是成员函数的指针时,第二个参数提供类的对象(可以是直接传递,或是传递指向对象的指针,或是通过std::ref()传递引用)
policy 可以是std::launch::async表示启动一个新线程来调用Fn,可以是std::launch::deferred表示期望对象get()时才创建线程调用Fn,或者是std::launch::deferred | std::launch::async 表示自动选择启动策略
栗子
#include // std::cout
#include // std::async, std::future
// a non-optimized way of checking for prime numbers:
bool is_prime (int x) {
std::cout <<"Calculating. Please, wait...\n";
for (int i=2; i fut = std::async (is_prime,313222313);
std::cout <<"Checking whether 313222313 is prime.\n";
// ...
bool ret = fut.get(); // waits for is_prime to return
if (ret) std::cout <<"It is prime!\n";
else std::cout <<"It is not prime.\n";
return 0;
}
经测试,不管何种启动方式,只要没有对期望对象get(),td::async就不会启动线程去创建异步任务(线程函数断点未命中),这个现象只与td::async与std::future<>对象有关
std::packaged_task<>可对一个函数、可调用对象进行封装,并可关联一个期望对象(使用成员函数get_future()可获得关联的期望对象),当std::packaged_task<>对象被调用时,它会调用封装的函数/可调用对象来开启一个异步任务,当异步任务完成时,其相关联的期望状态变为ready,调用期望的成员函数get()获取异步任务的执行结果或异步任务执行中发生的异常。
在构造std::packaged_task<>对象时,需要指定一个函数签名,函数签名的返回值类型用于标识期望对象的类型
#include
#include
#include
#include
#include //std::move
class Test
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
int main(){
Test t;
std::packaged_task task(t);
auto f = task.get_future(); //获取期望必须在创建线程之前,否则运行出错
std::thread th(std::move(task), 5, 5); //必须使用std::move()或std::ref() 因为std::packaged_task<>的拷贝构造函数是删除函数
std::cout << f.get() << std::endl;
if( th.joinable() )
th.join();
return 0;
}
再来一个例子
#include
#include
#include
#include
#include
std::mutex m;
std::deque> tasks;
bool gui_shutdown_message_received();
void get_and_process_gui_message();
void gui_thread()
{
while(!gui_shutdown_message_received())
{
get_and_process_gui_message();
std::packaged_task task;
std::lock_guard lk(m);
if(tasks.empty())
continue;
task=std::move(tasks.front());
tasks.pop_front();
task(); //调用std::packaged_task对象
}
}
std::thread gui_bg_thread(gui_thread); //启动gui线程
//post任务的线程如果需要知道std::packaged_task<>的执行情况可以通期望去get,不需要则直接丢弃
template
std::future post_task_for_gui_thread(Func f)
{
std::packaged_task task(f);
std::future res=task.get_future();
std::lock_guard lk(m);
tasks.push_back(std::move(task));
return res;
}
一个std::promise<>对象存储了供关联的期望对象(可能在另一线程)读取的某一T类型的值,从而提供同步点,通过std::promise<>的成员函数get_future可以获得与之相关联的对象,当std::promise<>对象使用set_value或set_exception设置一个值或异常时,关联的期望对象变为ready状态并可通过get()获取设置的值或异常。
栗子
// promise example
#include // std::cout
#include // std::ref
#include // std::thread
#include // std::promise, std::future
void print_int (std::future& fut) {
int x = fut.get();
std::cout <<"value: "<< x << '\n';
}
int main ()
{
std::promise prom; // create promise
std::future fut = prom.get_future(); // engagement with future
std::thread th1 (print_int, std::ref(fut)); // send future to new thread
prom.set_value (10); // fulfill promise
// (synchronizes with getting the future)
th1.join();
return 0;
}
再来一个例子
#include
#include
#include
int main()
{
std::promise p;
std::future f = p.get_future();
std::thread t([&p]{
try {
// code that may throw
throw std::runtime_error("Example");
}
catch (...) {
try {
// store anything thrown in the promise
p.set_exception(std::current_exception());
}
catch (...) {} // set_exception() may throw too
}
});
try {
std::cout << f.get();
}
catch (const std::exception& e) {
std::cout <<"Exception from the thread: "<< e.what() << '\n';
}
t.join();
}
std::shared_future<>与std::future<>不同,后者是只移动的,虽然期望的共享状态可以在不同的std::future<>中转移,但它总是值属于一个对象,前者是可拷贝的,也就是说多个对象(在不同线程中)可以共享一个期望状态
当需要同步多个线程的时候,应该使用std::shared_future<>,并让每一个线程都有自己的那一份期望的拷贝,而不是传递引用
可以使用std::move(),将共享状态从std::future<>转移到std::shared_future<>,也可以通过std::shared_future<>的移动构造函数转移
例子
std::promise p;
std::future f(p.get_future()); //移动构造后p无效
assert(f.valid()); // f有效
std::shared_future sf(std::move(f)); //显示移动
//或std::shared_future sf(f); //移动构造后f无效
assert(!f.valid()); // f现在无效
assert(sf.valid()); // sf有效
通过std::future<>的成员函数shar()可以创建std::shared_future<>对象同时转移所有权
std::promise p;
auto sf=p.get_future().share();
C++库提供了两种时钟类,稳定时钟std::chrono::steady_clock类与不稳定时钟类std::chrono::system_clock类
从字面意思上看,前者为稳定时钟,后者是系统时钟,一个时钟是否是稳定的要看时钟节拍是否均匀分布,且是否可调整,检测方法是根据类的静态成员变量is_steady,如果该变量为true,说明是稳定时钟
线程库使用到的所有C++时间工具据来自std::chrono命名空间内
std::chrono::duration<>表示时延(时间段),
template > class duration;
其第一个参数表示表示一种数值类型,第二个参数为std::ratio类型,表示秒表示的时间单位
ratio<3600, 1> hours
ratio<60, 1> minutes
ratio<1, 1> seconds
ratio<1, 1000> microseconds
ratio<1, 1000000> microseconds
ratio<1, 1000000000> nanosecons
将几分钟存储在short中,写成std::chrono::duration
std::ratio<60, 1>>将毫秒级存在double中,写成std::chrono::duration
标准库的std::chrono命名空间内,为延时变量提供了一系列预定义类型:
nanoseconds[纳秒] , microseconds[微秒] , milliseconds[毫秒] , seconds[秒] , minutes[分]和hours[时]
类型之间可以通过隐式/显示转换来完成单位的转换,从大单位转换到小单位是隐式发生,小单位转化为大单位需要显示转换,且转化的过程中出现了结果的截断
std::chrono::milliseconds ms(54802);
std::chrono::seconds s=std::chrono::duration_cast
std::chrono::duration<>对象是支持运算的,对象之间可以加减,且可以乘除一个整数以得到一个新的时间段
在时延中通过count()成员函数可以获得单位时间的数量,如std::chrono::milliseconds(1234).count()得到1234
支持时延处理的某些类的成员函数,他们以_for结尾。
如std::future<>的成员函数wait_for(),调用此函数的线程可能在一段指定的时间内等待(阻塞)直到期望对象的状态变为ready(以指定时间与变为ready中先者为准),函数返回值:
如果状态在指定的时间结束之前没有变为ready(任务完成或异常),返回std::future_status::timeout
如果状态在指定时间内变为ready,返回std::future_status::ready
如果期望任务被延迟,返回std::future_status::deferred
例子
std::future f = std::async(some_task);
if( f.wait_for(std::chrono::milliseconds(35)) == std::future_status::ready )
do_something_with(f.get())
时间点可由std::chrono::time_point<>对象来表示
template class time_point;
第一个参数需要指定一个时钟类型(std::chrono::steady_clock或std::chrono::system_clock),第二个参数表示时间的计量单位(特化的std::chrono::duration<>)
std::chrono::time_point<>对象支持运算,可以通过加/减时延对象来得到一个新的时间点
std::chrono::hight_resolution_clock::now() + std::chrono::nanoseconds(500)会得到500纳秒后的时间
std::chrono::time_point<>对象之间也可以相减(运算的对象间需要共享同一时钟类型)以得到两个时间点的时间差,也就是得到一个时间段
如:
auto start = std::chrono::high_resolution_clock::now();
do_something();
auto stop = std::chrono::high_resolution_clock::now();
std::cout << ”do_something() took “
<< std::chrono::duration(stop-start).count()
<<” seconds” << std::endl;
支持时间点处理的某些类的成员函数,他们以_until结尾。
例子
#include
#include
#include
std::condition_variable cv;
bool done;
std::mutex m;
bool wait_loop()
{
auto const timeout= std::chrono::steady_clock::now()+
std::chrono::milliseconds(500);
std::unique_lock lk(m);
while(!done)
{
if(cv.wait_until(lk,timeout)==std::cv_status::timeout)
break;
}
return done;
}
参数为duration必须为std::duration<> 对象,参数为time_point必须是std::time_point<> 对象
Class/Namespace |
Functions |
Return values |
std::this_thread |
sleep_for(duration) |
N/A |
std::condition_ |
wait_for(lock, |
std::cv_status:: |
wait_for(lock, |
bool—the return value of |
|
std::timed_mutex or |
try_lock_for |
bool—true if the lock was |
std::unique_ |
unique_lock(lockable, |
N/A—owns_lock() on the |
try_lock_for(duration) |
bool—true if the lock was |
|
std::future<ValueType> or |
wait_for(duration) |
std::future_status:: |
参考:
C++并发编程实战
http://www.cplusplus.com/
http://en.cppreference.com/w/