原文地址:https://xie-peiquan.gitee.io/tags/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/
何为线程同步?有时候我们需要规范线程的执行顺序,令独立线程上的行为同步。以下介绍C++标准库提供的线程同步工具。
1 信号量
#include <semaphore.h>
sem_t semaphore;
sem_init(&semaphore, 0, 0);
void data_preparation_thread()
{
while(more_data_to_prepare()){
......
sem_post(&semaphore); //信号量发送(增加)
}
}
void data_processing_thread()
{
while(true){
sem_wait(&semaphore); //等待信号量(减小)
......
}
}
2 条件变量
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_cond.wait() 建议配合 data_cond.notify_one() 一起用。notify_one() 通知条件变量,使wait() 从阻塞中被唤醒,重新尝试获取锁,再次查验条件。也就是 data_cond.wait() 并非一直检查条件,而是需要 notify_one() 触发。 当有多个线程阻塞等待时,用notify_all() .
另外,为什么这里要使用 std::unique_lock 呢?因为当条件不成立时,wait()要释放锁。而std::lock_guard 无法提供这种灵活性。
利用条件变量构建线程安全队列
虽然std::queue 的操作是原子性的,但是并不代表线程安全,其接口还是存在固有的条件竞争。我们需要把front()和pop()合并成一个函数,这与栈容器的top()和pop()合并相似。另外,为了增加灵活性,这里提供pop的两个变体,try_pop()和wait_and_pop(),一个非阻塞一个阻塞。以下是类定义:
#include <memory> ⇽--- ①为使用std::shared_ptr而包含此头文件
template<typename T>
class threadsafe_queue
{
public:
threadsafe_queue();
threadsafe_queue(const threadsafe_queue&);
threadsafe_queue& operator=(
const threadsafe_queue&) = delete; ⇽--- ②为简化设计而禁止赋值操作
void push(T new_value);
bool try_pop(T& value); ⇽--- ③
std::shared_ptr<T> try_pop(); ⇽--- ④
void wait_and_pop(T& value);
std::shared_ptr<T> wait_and_pop();
bool empty() const;
};
完整接口实现如下:
#include <queue>
#include <memory>
#include <mutex>
#include <condition_variable>
template<typename T>
class threadsafe_queue
{
private:
mutable std::mutex mut; ⇽--- ①互斥必须用mutable修饰(针对const对象,准许其数据成员发生变动)
std::queue<T> data_queue;
std::condition_variable data_cond;
public:
threadsafe_queue()
{}
threadsafe_queue(threadsafe_queue const& other)
{
std::lock_guard<std::mutex> lk(other.mut);
data_queue=other.data_queue;
}
void push(T new_value)
{
std::lock_guard<std::mutex> lk(mut);
data_queue.push(new_value);
data_cond.notify_one();
}
void wait_and_pop(T& value)
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk,[this]{return !data_queue.empty();});
value=data_queue.front();
data_queue.pop();
}
std::shared_ptr<T> wait_and_pop()
{
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk,[this]{return !data_queue.empty();});
std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
data_queue.pop();
return res;
}
bool try_pop(T& value)
{
std::lock_guard<std::mutex> lk(mut);
if(data_queue.empty())
return false;
value=data_queue.front();
data_queue.pop();
return true;
}
std::shared_ptr<T> try_pop()
{
std::lock_guard<std::mutex> lk(mut);
if(data_queue.empty())
return std::shared_ptr<T>();
std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
data_queue.pop();
return res;
}
bool empty() const
{
std::lock_guard<std::mutex> lk(mut);
return data_queue.empty();
}
};
3 future机制
如果某个线程按计划只需等待一次,那么利用 future 等待更合适。标准库中有std::future 和 std::shared_future,其参照了std::unique_ptr 和 std::shared_ptr. 注意,虽然future对象能用于线程间通信,但是future对象不提供同步访问。若有多个线程访问同一个future对象,必须用互斥访问。
3.1 async 异步运行
使用 std::async 启动一个异步任务。与 std::thread 对象等待运行方式的不同, std::async 会返回一个 std::future 对象,这个对象持有最终计算出来的结果。当你需要这个值时,你只需要调用 future 对象的get()方法阻塞获取。
std::async是C++中一种快速建立并行的方法,类似的,python中ProcessPoolExecutor也可以快速并行。
#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的传参一致,若传入的是普通函数,则第一个参数是函数对象,其余为函数的入参;若传入的是类成员函数,则第一个参数为类成员函数地址,第二个参数是对象地址,其余为函数入参。注意如果要传引用,用std::ref()包装。
std::async 有两种运行方式,一种是起新线程的异步运行方法,一种是在当前线程的同步运行方法。通过设定 std::async 的第一个参数,可以指定运行方式。该参数值可以是 std::launch::deferred 或 std::launch::async,前者指定在本线程上延迟执行函数,后者则新起一个线程执行。注:若不设定,则由系统自行选择。
3.2 packaged_task 打包任务
std::packaged_task<>可打包函数对象和future对象,他可以作为线程池的构件单元。std::packaged_task<>是一个类模板,其模板参数是函数签名,如:void()表示一个函数,无入参,无返回值。std::packaged_task<>对象具有 get_futrue()方法,能够返回future对象。std::packaged_task 是个可调用对象,调用时函数开始运行。
std::packaged_task 这种特性使得可以在线程间传递任务,即当前线程将任务包装在 std::packaged_task 中,获得对应 future 对象后,传递给另外的线程,并由其触发任务运行。等需要用到结果时,再用future 对象获取。以下代码是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; ⇽--- ⑩
}
3.3 promise 等待设定值
std::promise 配合 std::futrue 可实现以下机制:等待数据的线程在 futrue 上阻塞,提供数据的线程则利用 promise 设定关联值,使futrue就绪。
#include <iostream>
#include <thread>
#include <future>
void initiazer(std::promise<int> * promObj)
{
std::cout<<"新线程内部"<<std::endl;
promObj->set_value(35);
}
int main()
{
std::promise<int> promiseObj;
std::future<int> futureObj = promiseObj.get_future();
std::thread th(initiazer, &promiseObj);
std::cout<<futureObj.get()<<std::endl;
th.join();
return 0;
}
3.4 shared_future 共享期待
std::future只有一个实例可以获得特定的同步结果,而 std::shared_future 实例是可拷贝的,所以多个对象可以引用同一关联“期望”的结果。
std::promise<std::string> p;
std::shared_future<std::string> sf(p.get_future()); ⇽--- ①隐式转移归属权
3.5 等待多个futrue
当线程需要汇总各个std::async任务的处理结果时,需要逐个等待future对象,可能写出如下代码:
std::future<FinalResult> process_data(std::vector<MyData>& vec)
{
size_t const chunk_size=whatever;
std::vector<std::future<ChunkResult>> results;
for(auto begin=vec.begin(),end=vec.end();beg!=end;){
size_t const remaining_size=end-begin;
size_t const this_chunk_size=std::min(remaining_size,chunk_size);
results.push_back(
std::async(process_chunk,begin,begin+this_chunk_size));
begin+=this_chunk_size;
}
return std::async([all_results=std::move(results)](){
std::vector<ChunkResult> v;
v.reserve(all_results.size());
for(auto& f: all_results)
{
v.push_back(f.get()); ⇽--- ①
}
return gather_results(v);
});
}
当有任务完成时,代码①被唤醒,随之又进入休眠,等待下一个任务唤醒。我们有更简洁的写法,可以减少这种切换开销。采用 std::experimental::when_all
等待所有任务结束再唤醒。
std::experimental::future<FinalResult> process_data(
std::vector<MyData>& vec)
{
size_t const chunk_size=whatever;
std::vector<std::experimental::future<ChunkResult>> results;
for(auto begin=vec.begin(),end=vec.end();beg!=end;){
size_t const remaining_size=end-begin;
size_t const this_chunk_size=std::min(remaining_size,chunk_size);
results.push_back(
spawn_async(
process_chunk,begin,begin+this_chunk_size));
begin+=this_chunk_size;
}
return std::experimental::when_all(
results.begin(),results.end()).then( ⇽--- ①
[](std::future<std::vector<
std::experimental::future<ChunkResult>>> ready_results)
{
std::vector<std::experimental::future<ChunkResult>>
all_results=ready_results .get();
std::vector<ChunkResult> v;
v.reserve(all_results.size());
for(auto& f: all_results)
{
v.push_back(f.get()); ⇽--- ②
}
return gather_results(v);
});
}