4.1.1等待条件达成
C++标准对条件变量有两套实现:std::condition_variable和std::condition_variable_any。包含在<condition_variable>头文件声明中。两者都需要与一个互斥量一起才能工作;前者仅限于std::mutex一起工作,而后者可以和任何满足最低标准的互斥量一起工作,从而加上了_any后缀。因为std::condition_variable_any更加通用,这样体积、性能,以及系统资源的使用产生额外的开销。所以一般std::condition_variable作为首选,当对灵活性要求时候才考虑std::condition_variable_any。
如下使用std::condition_variable处理等待的数据:
std::mutex mut;
std::queue<data_chunk> data_queue; //1
std::conditon_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); //2
data_cond.notify_one(); //3
}
}
void data_processing_thread()
{
while(true)
{
std::unique_lock<std::mutex> lk(mut); //4
data_cond.wait(lk,[]{return !data_queue.empty()}); //5
data_chunk data = data_queue.front();
data_queue.pop();
lk.unlock(); //6
process(data);
if(is_last_chunk(data))
break;
}
}
首先,你拥有一个在两个线程之间传递数据的的队列1,当数据准备好,使用lock_guard对队列上锁,将数据压入队列中2,然后调用condition_variable的notify_one成员函数对等待的线程进行通知。
在另一侧,你有一个正在处理的数据的线程,这个线程首先对互斥量上锁,之后调用condition_variable的成员函数wait,传递一个锁和一个lambba表达式。当data_queue不为空——意味着数据准备好了。wait会去检查这个条件:当条件满足——即lambda返回true,如果条件不满足lamba返回false,将解锁互斥量,并且将这个线程置于阻塞或等待状态。使用unique_lock的原因:等待中的线程必须在等待期间解锁互斥量,并在这之后对互斥量继续上锁。
4.1.2 使用条件变量构建线程安全队列
设计一个通用队列的时候,可以看下标准库的的队列实现,如下是std::queue接口清单:
template<class T,class Container = std::deque<T> >
class queue
{
public:
explicit queue(const Container&);
explicit queue(Container&& = Container());
template<class Alloc> explicit queue(const Alloc&);
template<class Alloc> queue(const Container&,const Alloc&);
template<class Alloc> queue(const Container&&,const Alloc&);
template<class Alloc> queue(queue&&,const Alloc&);
void swap(queue& q);
bool empty() const;
size_type size() const;
T& front();
const T& front() const;
T& back();
const T& back() const;
void push(const T& x);
void push(T&& x);
void pop();
template<class ... Args> void emplace(Args&& ... args);
};
忽略构造、赋值以及交换操作时候,你就剩三组操作:
- 对整个队列的状态进行查询(empty(),size());
- 查询队列的各个元素(front(),back());
- 修改队列的操作(push(),pop()和empace())。
这和构建线程安全栈一样,合并top和pop一样,你需要将front和pop合并成一个函数调用。与上述代码不同的是,当使用队列在多个线程中传递数据的时,接受线程通常需要等待数据的压入。这里提供了pop两个变种:try_pop和wait_and_pop。try_pop,尝试从队列弹出数据,总是直接返回(即使失败的时候);wait_and_pop将等待有值可检索的时候才返回。
如下是线程安全栈的接口:
#include <memory>
template<typename T>
class threadsafe_queue
{
public:
threadsafe_queue();
threadsafe_queue(const threadsafe_queue&);
threadsafe_queue operator=(const threadsafe_queue&) = delete;//不允许简单的赋值
void push();
bool try_pop(T& value); //1
std::shared_ptr<T> try_pop();//2
void wait_and_pop(T& value);
std::shared_ptr<T> wait_and_pop();
bool empty() const;
};
就像对栈做的一样,在这里你将很多构造函数剪掉,禁止了对队列的简单赋值。
和之前一样,你也需要提供两个版本的try_pop和wait_and_pop。第一个重载的try_pop1在引用了变量存储检索值,所以它可以用来返回队列中的值状态;第二个重载2不能这样做,因为它直接返回检索值的,当没有值可检索了,返回NULL。
将代码和之前的条件变量的代码合在一起,最开始的代码中提取push和wait_and_pop:
#include <queue>
#include <mutex>
#include <condition_variable>
template<typename T>
class threadsafe_queue
{
private:
std::mutex mut;
std::queue<data_chunk> data_queue;
std::conditon_variable data_cond;
public:
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();
}
};
threadsafe_queue<data_chunk> data_queue; //1
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); //2
}
}
void data_processing_thread()
{
while(true)
{
data_chunk data;
data_queue.wait_and_pop(data); //3
process(data);
if(is_last_chunk(data))
break;
}
}
线程队列的实例包含了互斥量和条件变量,所以独立的变量就不需要 了1,并且调用push也不需要外部同步2,wait_and_pop还要兼顾条件量的等待。
如下是完整的使用条件变量的线程安全队列:
#include <queue>
#include <memory>
#include <mutex>
#include <condition_variable>
template<typename T>
class threadsafe_queue
{
private:
mutable std::mutex mut; //1互斥量必须是可变的
std::queue<T> data_queue;
std::conditon_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::unique_lock<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::unique_lock<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();
}
};
当等待线程只等待一次,当条件为true,它就不等待条件变量了,所以一个条件变量可能并非同步机制最好的选择。尤其是,条件在等待一组可用的数据块时,这种情况下就要选择期望(future)。