《C++ concurrency in action》 读书笔记 -- Part 3 第四章 线程的同步
唐风
《C++ concurreny in action》 第四章 Synchronizing concurrent operations
这一章主要讲C++11中的进行线程同步的方法
4.1 Waiting for an event or other condition
经常遇到的一种场景是一个线程A需要等待另一个线程B完成一些“事”(操作)才能往下运行继续处理。
有以下几种“办法”
一,设置一个多个线程都能访问到的变量,B线程一旦完成自己的操作,就把这个全局的变量设为某个值,而线程A则 不断地去检查变量的值是不是已经设置为这个值,一直到满足条件才往下执行。否则就一直循环地Check这个变量。(当然,A和B都要通过互斥锁来访问变量)。这个方法一般来说都不行,A不断地执行检查变量值只是个纯粹浪费CPU处理时间的操作。而且由于A进行锁定的时候,B还不能去操作那个被锁定全局变量,也无形中增加了最终条件满足所需要的时间
二,方法一的改进,就是A线程Sleep一段时间再检查一次
bool flag; std::mutex m; void wait_for_flag() { std::unique_lock<std::mutex> lk(m); while(!flag) { lk.unlock(); std::this_thread::sleep_for(std::chrono::milliseconds(100)); lk.lock(); } }
这个方法性能更好,但是A的sleep时间取多少并不很好判断,而且这个方法对于实时的或是要求快速反应的程序并不合并
三,用事件来进行同步
这是最“正常的”解法。C++11中提供了相应的库和函数来完成这种任务。
其中最为常见的就是:condition variable(条件变量)。条件变量(condition variable)会把信号量(event)与某些条件在一起,如果一个线程把这个条件变量设置某满足某一条件时,condition variable就会自动地通知其它等待这个条件的线程。
4.1.1 Waiting for a condition with condition variable
C++11中有两种condition variable的实现:
std::condition_variable 和 std::condition_variable_any (都定义在<condition_variable>头文件中)
- std::condition_variable只能与mutex一起使用
- std::condition_variable_any可以与mutext-like(只需要满足一些最低要求锁操作的“锁”,不仅限于mutex)一起使用,但是,可能会“更大更慢更占操作系统资源一些”
std::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); // (1) 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; } }
注意(1)必须使用 unique_lock,而不能用guard_lock,因为在condition_variable内部实现中,有可能要多次进行lock和unlock操作。
4.1.2
因为线程间的共享数据的queue非常之有用,而上面的例子并不是一个很好的用法,本节构造了一个好的“线程间共享queue”。
4.2 Waiting for one-off events with futures
one-off event就是“只需要等一次的事件”。C++11对这种场景提供了 future 模型。如果一个线程需要等待一个“只会发生一次的”事件,它就可以获取一个包含这个 event 的 future。别的线程可以把 future 设置为 ready 状态,这样等待 future的线程就可以知晓并接着进行处理。future一旦ready就不能再重新设置了(不会再变成 unready状态了)
C++标准库中有两种future,
- unique future -> std::future<>
- shared_future -> std::shared_future<>
是模仿智能指针 std::unique_ptr 和 std::share_ptr 的思路来分类的。
最常见的一种one-off event的场景就是开启一个线程进行后台的计算,而主线程去做些别的事,之后再等这个计算的结果出来。当从第二章我们知道,std::thread 没有提供什么方便的方法可以让你取得线程调用函数的“返回值”。这种场景的一个方案就是:std::async
4.2.1 从后台线程中返回值
std::async和“线程”几乎有同样的作用,但是它会返回一个future,这个future就带有线程函数的返回值。你可以用future.get()来获取这个值,get()函数会在线程结果之前一直阻塞。
#include <future> #include <iostream> int find_the_answer_to_ltuae(); void do_other_stuff(); int main() { 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; }
std::async像std::thread一样,可以在构造的时候给线程主函数进行传参。本节比较详细地说明了传参的方法(主要是:如何传引用和如何move)
struct X { void foo(int,std::string const&); std::string bar(std::string const&); }; X x; auto f1=std::async(&X::foo,&x,42,"hello"); auto f2=std::async(&X::bar,x,"goodbye"); struct Y { double operator()(double); }; Y y; auto f3=std::async(Y(),3.141); auto f4=std::async(std::ref(y),2.718); X baz(X&); std::async(baz,std::ref(x)); class move_only { public: move_only(); move_only(move_only&&) move_only(move_only const&) = delete; move_only& operator=(move_only&&); move_only& operator=(move_only const&) = delete; void operator()(); }; auto f5=std::async(move_only());
std::async一构造完就会启动线程函数,还是要等future.get()被调用才启动线程函数,这是取决是编译器提供商的实现的(标准并没有进行规定)。但如果你想确认它,你可以指定参数:std::launch(立即启动新线程) 或是 std::deferred(等到wait或是get函数被调用时才启动新线程)。
auto f6=std::async(std::launch::async,Y(),1.2); auto f7=std::async(std::launch::deferred,baz,std::ref(x)); auto f8=std::async( std::launch::deferred | std::launch::async, baz,std::ref(x)); auto f9=std::async(baz,std::ref(x)); f7.wait();
将future与并行任务关联的方法除了使用 std::async之外,还可以使用 std::packaged_task<>,或是std::promise<>,其中std::packaged_task是std::promise的高层一些的抽象。下节就讲这个。
4.2.2 Associating a task with a future
std::packaged_task<>可以把任务(Task,函数或是函数对象之类可调用的东西)与future关联起来,当std::packaged_task被调用的时候,它就会调用自己所包装的任务,并在可调用对象返回时把值设置到future中,然后再把future设定为ture。这个可以用在构造线程池时使用(第九章)会讲这个,也可以用在其它的任务管理调度。
std::packaged_task<>的模板参数与std::function的类似,是可调用对象的类型签名(像void(),int(std::string&, double*)之类的),但参数部分是用来进行并行任务的参数列表,而返回值部分则是可从future中get到的值的类型
template<> class packaged_task<std::string(std::vector<char>*,int)> { public: template<typename Callable> explicit packaged_task(Callable&& f); std::future<std::string> get_future(); void operator()(std::vector<char>*,int); };
std::packaged_task是个可调用的对象,因此它也可以被封装在std::function中,或是使用在std::thread中表示一个线程的执行函数,或是在别的函数中调用,调用完之后它会把返回值作为异步的结果放在所关联的std::future中。所以使用时,我们可以把一个需要执行的任务包装在std::packaged_task中,然后获取它的std::future后,把这个包装好的任务传递给其它的线程。等我们需要这个任务的返回值的时候,我们就可以等待future变成ready。
下面是一个GUI的例子:
#include <deque> #include <mutex> #include <future> #include <thread> #include <utility> std::mutex m; std::deque<std::packaged_task<void()> > 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<void()> task; { std::lock_guard<std::mutex> lk(m); if(tasks.empty()) continue; task=std::move(tasks.front()); tasks.pop_front(); } task(); } } std::thread gui_bg_thread(gui_thread); template<typename Func> std::future<void> post_task_for_gui_thread(Func f) { std::packaged_task<void()> task(f); std::future<void> res=task.get_future(); std::lock_guard<std::mutex> lk(m); tasks.push_back(std::move(task)); return res; }
《C++ concurreny in action》 第四章 Synchronizing concurrent operations
4.2.3 Makeing (std::)promise
但是有时候我们在异步地获取值的时候,并不能总是“获取一个可调用对象的返回值”这么简单,在这些场景下,我们不一定能包装成一个可调用对象,或是,我们需要在一个函数获取多种不同类型的返回,这时std::packaged_task就不好用了。这种情况下我们可以使用std::promise。总的来说,std::promise/std::future对与std::packaged_task/std::future对的关系是类似的。我们也可以从一个promise关联获取一个std::future,当调用std::promise的set_value成员函数时,它所关联的std::future的值就会被设定,而且成为ready状态。
下面是一个例子:
#include <future> void process_connections(connection_set& connections) { while(!done(connections)) { 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); p.set_value(data.payload); } if(connection->has_outgoing_data()) { outgoing_packet data= connection->top_of_outgoing_queue(); connection->send(data.payload); data.promise.set_value(true); } } } }
4.2.4 Saving an exception for the future
在上面的例子中,我们都没有提到出现异常的情况。虽然 std::future与std::async/std::packaged_task/std::promise的场景大部分是多线中,但标准仍然提供了一个像“单线程环境”中的异常那样比较“符合”我们想像的处理方法:当std::async/std::packaged_task/std::promise发生异常时,可以把异常保存在std::future当中,等另一个线程调用 std::future和get/wait函数时,再把这个异常重新抛出(这时抛出的是原来异常的引用还是拷贝,则取决于编译器的实现)。std::async/std::packaged_task的情况下,我们不需要做额外的处理,这一切库函数已经做好了,std::promise的情况下,为了抛出异常,我们要则要调用 set_exception而不是set_value
extern std::promise<double> some_promise; try { some_promise.set_value(calculate_value()); } catch(...) { some_promise.set_exception(std::current_exception()); }
另外,如果std::promise的set_value函数没有被调用,或是std::packaged_task没有被调用就被析构了的话,他们也会把异常(std::future_error,值为std::future_errc::broken_promise)存在std::future中,并把future设置为ready。
4.2.5 Waiting from multiple threads
std::future的资源占有方式是unique的,只能在一个线程中获取(moveable)。如果有多个线程需要获取同一个future的话,必须使用std::shared_future(copyable)。
std::promise<std::string> p;
std::shared_future<std::string> sf(p.get_future());
4.3 Waiting for a time limit
前面介绍的所以有阻塞的调用“永远等待”的,不会超时。如果要设置超时,需要另外指定。
大体上分,C++11中有两种设置超时的方法,一种是设置超时的期间(duration-based),也就是从调用起多长时间超时(一般都是_for后缀),另一种是设置超时的绝对时间点(absolute timeout,一般是_util后缀)。
4.3.1 Clocks
C++中处理时间一般可以使用clock,clock可以提供下面4项内容
The time now
■ The type of the value used to represent the times obtained from the clock
■ The tick period of the clock
■ Whether or not the clock ticks at a uniform rate and is thus considered to be a steadyclock
静态函数now()可以获得现在的时间。比如std::chrono::system_clock::now()可以获取现在的系统时钟的时间。
tick period是以秒的分数形式指定的,std::ratio<1,25>就是1秒跳25次,std::ratio<5,2>就是2.5秒跳一次
4.3.2 Duration
时间段用std::chrono::duration来表示,这个类有两个模板参数,第一个表示时间的类型(int, long, double),第二个参数是一个“分数”,表示了一个单位的duration对象是多少秒,比如
std::chrono::duration<short, std::ratio<60, 1>> 就是60秒
std::chrono::duration<double,std::ratio<1,1000>> 是1/1000秒
标准库定义了一些duration的typedef:
nanoseconds, microseconds, milliseconds, seconds, minutes, hours
duration之间是可以隐式转换和进行算术运算的(此处不详记)
4.3.3 Time point
std::chrono::time_point<>表示一个时间点。
接受两个模板参数,第一个模板参数表示使用的“时钟”,第二个模板参数表示“计量的单位”(用 std::chrono::duration<>来指定)
比如:
std::chrono::time_point<std::chrono::system_clock, std::chrono::minutes> 就是指使用系统时钟进行,并以分为单位测定的时间点。std::chrono::time_point是可以与std::chrono::duration进行运算而得到新的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<double,std::chrono::seconds>(stop-start).count() <<” seconds”<<std::endl;
4.3.4 Functions that accept timeouts
线程库中可以指定Timeout的函数如下表
4.4 主要是讲如何使用前章和本章中介绍的技术来简化C++的代码(模拟一些Function Programming风格)。此处略去,