等待事件或其他条件
(1)条件变量
当一个线程正等待另一个线程完成某一项任务,可以使用条件变量。当某个线程已经确定条件得到满足,他就会唤醒正在条件变量上等待的线程继续执行。
标准C++库提供了二个条件变量的实现:
std::condition_variable
和
std::condition_variable_any
。这二个实现都在
<condition_variable>头文件
中说明。两者都需要与一个互斥量一起才能工作(互斥量是为了同步);前者仅限于与std::mutex一起工作,而后者可以和任何满足最低标准的互斥量一起工作,从而加上了*_any*的后缀。通常使用condition_variable。
std::mutex mut;
std::queue<data_chunk> 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<std::mutex> lk(mut);//注意加上了锁
data_queue.push(data);
data_cond.notify_one(); //唤醒等待的线程
}
}
void data_processing_thread()
{
while (true)
{
std::unique_lock<std::mutex> lk(mut); //使用的是unique_lock,因为中间要加锁解锁
data_cond.wait(
lk, []{return !data_queue.empty(); }); //等待条件变量就绪
data_chunk data = data_queue.front();
data_queue.pop();
lk.unlock(); //解锁
process(data);
if (is_last_chunk(data))
break;
}
}
在一个线程中,std::condition_variable的实例上调用
notify_one()成员函数
来
通知一个等待中的线程
(如果有的话)。
另一个线程中,在std::condition_variable的实例上调用
wait()函数
,
阻塞等待条件变量的就绪。
wait()会去检查这些条件(通过调用所提供的lambda函数),当条件满足(lambda函数返回true)时返回。如果条件不满足(lambda函数返回false),wait()函数将解锁互斥量,并且将这个线程置于阻塞或等待状态。当准备数据的线程调用notify_one()通知条件变量时,处理数据的线程从睡眠状态中苏醒,重新获取互斥锁,并且对条件再次检查,在条件满足的情况下,从wait()返回并继续持有锁。当条件不满足时,线程将对互斥量解锁,并且重新开始等待。这就是为什么用std::unique_lock而不使用std::lock_guard——等待中的线程必须在等待期间解锁互斥量,并在这之后对互斥量再次上锁,而std::lock_guard没有这么灵活。如果互斥量在线程休眠期间保持锁住状态,准备数据的线程将无法锁住互斥量,也无法添加数据到队列中;同样的,等待线程也永远不会知道条件何时满足。
使用future等待一次性时间
如果线程等待其他线程的任务完成只等待一次,那么条件变量就不是最好的同步机制,这种场景下,使用future更加合适。
C++标准库将这种一次性事件成为
future
。C++中,有二种future:
唯一funture
(unique funtures,
std::future<>
)和
共享future
(share future,
std::shared_future<>
)。二个future都声明在
头文件<future>
中。在与数据无关的地方,可以使用std::future<void>与std::shared_future<void>的特化模板。虽然,我希望用于线程间的通讯,但是“future”对象本身并不提供同步访问。当多个线程需要访问一个独立“future”对象时,必须使用互斥量或类似同步机制对访问进行保护。
(1)带返回值的后台任务
std::thread并不提供直接接收返回值的机制。这里就需要
std::async函数模板
(也是在头文<future>中声明的)了。
当你不急需得到任务的结果时,你可以
使用std::async启动一个异步任务
。与std::thread对象等待的方式不同,std::async会返回一个std::future对象,这个对象持有最终计算出来的结果。当你需要这个值时,你只需要调用这个对象的
get()成员函数
;
并且会阻塞线程直到“future”状态为就绪为止;之后,返回计算结果。
int find_the_answer_to_ltuae();
void do_other_stuff();
int main()
{
//使用async开启了一个异步任务,返回的future赋值给了the_answer
std::future<int> the_answer = std::async(find_the_answer_to_ltuae);
do_other_stuff();
std::cout << "The answer is " << the_answer.get() << std::endl;//通过get()成员函数得到返回值。调用get会阻塞
}
与std::thread 做的方式一样,std::async允许你通过添加额外的调用参数,向函数传递额外的参数。
当第一个参数是一个指向成员函数的指针,第二个参数提供有这个函数成员类的具体对象
(不是直接的,就是通过指针,还可以包装在std::ref中),
剩余的参数可作为成员函数的参数传入
。否则,第二个和随后的参数将作为函数的参数,或作为指定可调用对象的第一个参数。就如std::thread,当参数为右值(rvalues)时,拷贝操作将使用移动的方式转移原始数据。
默认情况下,std::async是否立刻启动线程执行,还是等到get()调用后才执行 取决于具体的实现方式。当时可以在函数调用之前,向std::async传递额外的参数来人为改变std::async的执行方式。
额外的参数可以是
std::launch::defered
,
用来表明函数调用被延迟到wait()或get()函数调用时才执行。
或者参数可以是
std::launch::async
表明函数必须在其所在的独立线程上执行
。
std::launch::deferred | std::launch::async
是默认的执行方式,表明实现可以选择这两种方式的一种。
auto f6=std::async(std::launch::async,Y(),1.2); // 在新线程上执行
auto f7=std::async(std::launch::deferred,baz,std::ref(x)); // 在wait()或get()调用时执行
auto f8=std::async(
std::launch::deferred | std::launch::async,
baz,std::ref(x)); // 实现选择执行方式
auto f9=std::async(baz,std::ref(x));
f7.wait(); // 调用延迟函数
(2)任务与future
std::packaged_task<>
将一个future绑定到一个函数或者可调用对象上。
当std::packaged_task<>对象被调用时,它就调用相关的函数或可调用对象让future就绪,将返回值作为关联数据存储。
std::packaged_task<>类模板的模板参数为
函数签名
,例如
int(std::string&, double*),void()这样。 当你构造一个
std::packaged_task实例的时候,你必须传入一个函数或者可调用对象,它可以接受指定的参数并且返回指定的返回类型。类型不需严格匹配,可以有隐式转换。
指定的函数签名的返回类型指定了从
get_future()
成员函数返回的std::funture<>类型
,而函数签名的参数列表指定了传入任务的函数调用运算符。
std::mutex m;
std::deque<std::packaged_task<void()> > tasks;//packaged_task队列
bool gui_shutdown_message_received();
void get_and_process_gui_message();
//任务执行线程
void gui_thread()
{
while (!gui_shutdown_message_received()) // 2
{
get_and_process_gui_message(); //
std::packaged_task<void()> task;
{
std::lock_guard<std::mutex> lk(m);
if (tasks.empty()) // 无任务,继续等待
continue;
task = std::move(tasks.front()); // 取出packaged_task任务
tasks.pop_front();
}
task(); // 执行packaged_task的任务
}
}
std::thread gui_bg_thread(gui_thread);
//发布任务,传出等待任务就绪的future,任务函数为f
template<typename Func>
std::future<void> post_task_for_gui_thread(Func f)
{
std::packaged_task<void()> task(f); //创建packaged_task实例
std::future<void> res = task.get_future(); //将packaged_task实例的等待条件返回
std::lock_guard<std::mutex> lk(m); // 9
tasks.push_back(std::move(task)); // 传入packaged_task队列
return res;
}
这个例子中使用了std::packaged_task<void()>,它封装了一个接受零参数且返回void的函数或可调用对象(如果它返回了别的东西,则返回值会被丢弃),这是最简单的任务模板参数。
(3)使用std::promise
std::promise<T>
提供一种设置值(类型T)的方式,它可以在这之后通过相关联的std::future<T>对象进行读取。
一对std::promise/std::future使得等待中的线程可以阻塞future,同时提供数据的线程可以使用配对中的promise项,来设置相关的值使得future就绪。
可以通过
get_future()成员函数
来获取与一个给定的std::promise相关的std::future对象,就像是与std::packaged_task相关。
当“promise”的值已经设置完毕(使用
set_value()
成员函数),对应“future”的状态变为“就绪”,并且可用于检索已存储的值。当你在设置值之前销毁std::promise,将会存储一个异常。
//在这个例子中,你可以使用一对std::promise<bool>/std::future<bool>找出一块传出成功的数据块
void process_connections(connection_set& connections)
{
while (!done(connections)) // 1
{
for (connection_iterator //遍历每一个连接
connection = connections.begin(), end = connections.end();
connection != end;
++connection)
{
if (connection->has_incoming_data()) // 有数据传来
{
data_packet data = connection->incoming();
std::promise<payload_type>& p =
connection->get_promise(data.id); //赋值给std::promise
p.set_value(data.payload); //设置std::promise的值,使得对应的future就绪
}
if (connection->has_outgoing_data()) // 有数据发送
{
outgoing_packet data =
connection->top_of_outgoing_queue();
connection->send(data.payload);
data.promise.set_value(true); //表示成功传输
}
}
}
}
(4)为future保存异常
考虑下面的代码。square_root()函数可能产生一个异常。
double square_root(double x)
{
if(x<0)
{
throw std::out_of_range(“x<0”);
}
return sqrt(x);
}
通过异步调用的方式运行:
std::future<double> f=std::async(square_root,-1);
double y=f.get();
在这样的情况下,即
如果作为std::async一部分的函数调用引发了异常,该异常会存储在future中,代替所存储的值,并且对get()的调用会重新引发所存储的异常。
同样对std::packaged_task中的异常也会发生在调用get()后。
当然,通过函数的显式调用,std::promise也能提供同样的功能。当你希望存入的是一个异常而非一个数值时,你就需要调用
set_exception()成员函数
,而非set_value()。这通常是用在一个catch块中,并作为算法的一部分,为了捕获异常,使用异常填充promise:
extern std::promise<double> some_promise;
try
{
some_promise.set_value(calculate_value());
}
catch(...)
{
some_promise.set_exception(std::current_exception());
}
这里使用了
std::current_exception()
来检索抛出的异常;可用
std::copy_exception()
作为一个替换方案,std::copy_exception()会直接存储一个新的异常而不抛出:
some_promise.set_exception(std::copy_exception(std::logic_error("foo ")));
(5)多个线程的等待
如果需要多个线程等待同一个事件,则需要使用
std::shared_future
。
std::future模型独享同步结果的所有权,因此future只是可移动的(move),不可复制,在一个线程get()后,就没有值可以获取了。
而std::shared_future实例是可拷贝的,所以多个对象可以引用同一关联“期望”的结果。
因为std::shared_future是可以拷贝的,所以在多个线程等待同一个事件的情境下,可以让每一个线程拥有自己的std::shared_future实例,等待同一个事件。
std::shared_future实例可以通过std::future实例来构造,但是std::future是无法共享的,所以要通过move()转移future的所有权。
std::promise<int> p;
std::future<int> f(p.get_future());
assert(f.valid()); // “future” f 是合法的
std::shared_future<int> sf(std::move(f));
assert(!f.valid()); // “future”f 现在是不合法的,f为空future
assert(sf.valid()); // sf 现在是合法的
因为右值转换是隐式的,所以可以直接传入get_future()。
std::promise<std::string> p;
std::shared_future<std::string> sf(p.get_future()); // 1 隐式转移所有权
std::future还有一个share()函数,可以直接构造一个新的std::shared_future,并且将所有权转移。
std::promise< std::map< SomeIndexType, SomeDataType, SomeComparator,
SomeAllocator>::iterator> p;
auto sf=p.get_future().share();
限制等待时间
(1)时钟
对于C++标准库来说,时钟就是时间信息源。特别是,时钟是一个类,提供了四种不同的信息:
1.现在时间 2.时间类型 3.时钟节拍 4.通过时钟节拍的分布,判断时钟是否稳定
时钟的当前时间可以通过调用
静态成员函数now()
从时钟类中获取;例如,
std::chrono::system_clock::now()
是将返回系统时钟的当前时间。特定的时间点类型可以通过time_point的数据typedef成员来指定,所以some_clock::now()的类型就是some_clock::time_point。
时钟节拍被指定为1/x(x在不同硬件上有不同的值)秒,这是由时间周期所决定——一个时钟一秒有25个节拍,因此一个周期为
std::ratio<1, 25>
,当一个时钟的时钟节拍每2.5秒一次,周期就可以表示为std::ratio<5, 2>。
如果一个时钟以均匀速率计时且不能被调整,则该时钟被称为匀速时钟。如果时钟是匀速的,则
时钟类的is_steady静态数据成员为true,反之为false
。 通常情况下,td::chrono::system_clock是不匀速的。 但匀速时钟对于计算超时是非常重要的,因此C++标准库提供形式为
std::chrono::steady_clock
。
C++标准库提供的其他时钟可表示为std::chrono::system_clock(在上面已经提到过),它代表了系统时钟的“实际时间”,并且提供了函数可将时间点转化为time_t类型的值;
std::chrono::high_resolution_clock
可能是标准库中提供的具有最小节拍周期(因此具有最高的精度[分辨率])的时钟。它实际上是typedef的另一种时钟,这些时钟和其他与时间相关的工具,都被定义在
<chrono>库头文件
中。
(2)时间段
std::chrono::duration<>函数模板
能够对时间段进行处理(线程库使用到的所有C++时间处理工具,都在std::chrono命名空间内)。
第一个模板参数是一个类型表示(比如,int,long或double),第二个模板参数是制定部分,表示每一个单元所用秒数
。例如,当几分钟的时间要存在short类型中时,可以写成std::chrono::duration<short, std::ratio<60, 1>>
标准库在std::chrono命名空间内,为时间段变量提供一系列预定义类型:nanoseconds[纳秒] , microseconds[微秒] , milliseconds[毫秒] , seconds[秒] , minutes[分]和hours[时]。
当不要求截断值的情况下(时转换成秒是没问题,但是秒转换成时就不行)时间段的转换是隐式的。显示转换可以由
std::chrono::duration_cast<>
来完成。
时间段支持计算,所以你能够对两个时延变量进行加减,或者是对一个时延变量乘除一个常数(模板的第一个参数)来获得一个新延迟变量。
基于时间段的等待可由std::chrono::duration<>来完成。例如,你等待一个“future”状态变为就绪已经35毫秒:
std::future<int> f=std::async(some_task);
if(f.wait_for(std::chrono::milliseconds(35))==std::future_status::ready)
do_something_with(f.get());
(3)时间点
时钟的时间点是通过
std::chrono::time_point<>类模板
的实例来表示的,
它以第一个模板参数表示其参看的时钟,并且以第二个模板参数表示时间的计量单位
(std::chrono::duration<>的特化)。时间点是基于unix时间戳的一个时间的长度,通常unix时间戳表示1970年1月1日。
你可以通过std::chrono::time_point<>实例来加/减时延,来获得一个新的时间点,所以std::chrono::hight_resolution_clock::now() + std::chrono::nanoseconds(500)将得到500纳秒后的时间。
你也可以减去一个时间点(二者需要共享同一个时钟)。结果是两个时间点的时间差。
这
对于代码块的计时是很有用的,例如:
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<double,std::chrono::seconds>(stop-start).count()
<<” seconds”<<std::endl;
如你期望的那样,后缀为_unitl的(等待函数的)变量会使用时间点。通常是使用某些时钟的::now()(程序中一个固定的时间点)作为偏移,虽然时间点与系统时钟有关,可以使用std::chrono::system_clock::to_time_point() 静态成员函数,在用户可视时间点上进行调度操作。例如,当你有一个对多等待500毫秒的,且与条件变量相关的事件,你可以参考如下代码:
std::condition_variable cv;
bool done;
std::mutex m;
bool wait_loop()
{
auto const timeout= std::chrono::steady_clock::now()+
std::chrono::milliseconds(500); //实例化一个时间点,通过now()的当前时间
std::unique_lock<std::mutex> lk(m);
while(!done)
{
if(cv.wait_until(lk,timeout)==std::cv_status::timeout) //wait_until中有了等待时间点
break;
}
return done;
}
(4)超时函数
使用超时的最简单方式就是,对一个特定线程添加一个延迟处理;当这个线程无所事事时,就不会占用可供其他线程处理的时间。你在4.1节中看过一个例子,你循环检查“done”标志。两个处理函数分别是
std::this_thread::sleep_for()和std::this_thread::sleep_until()
。他们的工作就像一个简单的闹钟:当线程因为指定时延而进入睡眠时,可使用sleep_for()唤醒;或因指定时间点睡眠的,可使用sleep_until唤醒。
超时甚至可以在尝试获取一个互斥锁时(当互斥量支持超时时)使用。std::mutex和std::recursive_mutex都不支持超时锁,但是std::timed_mutex和std::recursive_timed_mutex支持。这两种类型也有try_lock_for()和try_lock_until()成员函数,可以在一段时期内尝试,或在指定时间点前获取互斥锁。表4.1展示了C++标准库中支持超时的函数。
参数列表为“延时”(duration)必须是std::duration<>的实例,并且列出为时间点(time_point)必须是std::time_point<>的实例。