《c++ concurrency in action》读书笔记4--线程间同步
线程间同步也是十分常见的场景,可以选择的方法:
1. 一直检查变量的值,看任务是否完成。但这个方法有很多弊端(线程需要占用cpu的处理时间,而且检查变量需要拿互斥锁)
2. 周期检查变量的值(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时间的设置,
3. c++标准提供了条件变量和futures来处理这种情况。
一、条件变量
1. c++标准库定义了std::condition_variable 和 std::condition_variable_any两个条件变量,std::condition_variable只能和std::mutex一起工作,而 std::condition_variable_any可以和满足互斥最低要求的变量一起工作,所以condition_variable_any更通用,但condition_variable的性能更好,应该优先选用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); 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; } }
data_preparation_thread中拿锁将数据放入队列中,并通知条件变量。data_processing_thread中data_cond的wait函数会同过拿锁检查第二个参数得结果,如果为true,则继续执行。如果为false放锁,并阻塞,直到其他线程调用notify.
二、future
c++标准库利用future来处理一次性事件(比如登机), c++中定义了两种类型的future, unique futures(std::future<>) 和 shared futures (std::shared_future<>)。 一个std::futures的实例是引用关联事件的唯一实例,而多个std::shared_future的实例可以对应到同一个事件。
需要注意,如果多个线程访问同一个future对象,需要加互斥锁(或类似的机制),而多线程利用自己std::shared_funture<>的拷贝访问数据则不需要同步。
1. std::async和future
std::async可以直接返回一个future对象。
#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启动一个异步的task,并返回一个future对象,通过调用future对象的get()函数可以得到异步task的运行结果。如果调用get()函数时future不是ready状态,则当前调用线程会被block住,直到future的状态变成ready为止。
std::async可以通过第一个参数决定其运行方式:std::launch::async 在一个新线程中运行, std::launch::deferred推迟到调用wait()或get()时执行。
// Run in new thread auto f6=std::async(std::launch::async,Y(),1.2); // Run in wait() or get() auto f7= std::async(std::launch::deferred,baz,std::ref(x)); // Implementation chooses auto f8=std::async( std::launch::deferred | std::launch::async, baz,std::ref(x)); auto f9=std::async(baz,std::ref(x)); f7.wait(); // Invoke deferred function
2.std::packaged_task 和 future
packaged_task提供了一种方法将future和函数或callback对象关联起来,当std::packaged_task<>的对象被唤醒,就会调用关联的函数或callback,并将future的状态置为ready。
#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; }
3. std::promise和future
std::promise<>提供了一种方法设置一个值,并可以通过相关连的future进行读取。当std::promise<>调用set_value()后,对应的future会变成ready状态。 如果std::promise<>没有设置值,就本析构了,则异常值会被存储在future中。
#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 future和异常
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、std::packaged_task抛出异常后,异常会被存储到future中,future的状态会变成ready, 当调用get()时,异常会被重新抛出来。
当使用std::promise,想要存储exception时,则调用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());
// 或者
// some_promise.set_exception(std::copy_exception(std::logic_error("foo ")));
}
如果promise没有调set函数或者packaged_task没有被唤醒,对象就被销毁的话,future会被设置为ready状态,exception被存在future中。
5. std::shared_future
对于std::future来说,这有一个线程能获取数据,调用一次get()后,则没有数据留下。如果想在多线程中等待同一个事件的结果,应该使用std::shard_future.
需要注意的是,如果想在多线程中利用同一个std::shard_futured对象获取数据,也会有数据竞争问题,需要用锁保护。更优的方法是,每个线程拷贝一个std::shard_futured对象的备份,然后利用自身拷贝获取数据。
利用std::future构造std::shared_future
std::promise<int> p; std::future<int> f(p.get_future()); assert(f.valid()); std::shared_future<int> sf(std::move(f)); assert(!f.valid()); assert(sf.valid());
std::future直接转为std::shared_future
std::promise< std::map< SomeIndexType, SomeDataType, SomeComparator, SomeAllocator>::iterator> p; auto sf=p.get_future().share();
三、 有时间限制的等待
有两种timeout的时间,持续时间(比如等待30ms) 和绝对时间(比如等待到17:30:15 utc), 处理持续时间的变体带有_for后缀, 处理绝对时间的变体带有_until后缀。
所以对于std::condition_variable来说,会有对应的wait_for, wait_until()函数。
支持时间限制的函数: