Cla*_*tus 6 c++ multithreading mutex condition-variable c++11
请考虑以下示例.
std::mutex mtx;
std::condition_variable cv;
void f()
{
{
std::unique_lock<std::mutex> lock( mtx );
cv.wait( lock ); // 1
}
std::cout << "f()\n";
}
void g()
{
std::this_thread::sleep_for( 1s );
cv.notify_one();
}
int main()
{
std::thread t1{ f };
std::thread t2{ g };
t2.join();
t1.join();
}
Run Code Online (Sandbox Code Playgroud)
g()"知道" f()在我想讨论的场景中等待.根据cppreference.com,g()在调用之前无需锁定互斥锁notify_one.现在在标记为"1"的行cv中将释放互斥锁并在发送通知后重新锁定它.析构函数在lock此之后立即再次释放它.这似乎是多余的,特别是因为锁定很昂贵.(我知道在某些情况下需要锁定互斥锁.但这不是这种情况.)
为什么condition_variable没有函数" wait_nolock",一旦通知到达,它就不会重新锁定互斥锁.如果答案是pthreads没有提供这样的功能:为什么不能延长提供它的pthreads?是否有实现理想行为的替代方案?
Yak*_*ont 13
你误解了你的代码所做的事情.
您的在线代码// 1可以免费阻止. condition_variables可以(而且会!)有虚假的唤醒 - 他们可以毫无理由地醒来.
您有责任检查唤醒是否是虚假的.
condition_variable正确使用需要3件事:
condition_variablemutexmutex由互斥锁保护的数据被修改(在mutex)下.然后(mutex可能脱离),condition_variable通知.
另一方面,你锁定mutex,然后等待条件变量.当你醒来时,你会mutex被重新锁定,并且通过查看被守卫的数据来测试唤醒是否是虚假的mutex.如果它是有效的唤醒,则处理并继续.
如果它不是一个有效的唤醒,你会回去等待.
在您的情况下,您没有任何数据保护,您无法区分虚假唤醒和真实唤醒,并且您的设计不完整.
毫不奇怪,对于不完整的设计,您没有看到mutex重新锁定的原因:它被重新锁定,因此您可以安全地检查数据以查看唤醒是否是虚假的.
如果你想知道为什么条件变量是以这种方式设计的,可能是因为这种设计比"可靠"设计(无论出于何种原因)更有效,而不是暴露更高级别的原语,C++暴露了更低级别更高效的原语.
在此基础上构建更高级别的抽象并不难,但有设计决策.这是一个建立在std::experimental::optional:
template<class T>
struct data_passer {
std::experimental::optional<T> data;
bool abort_flag = false;
std::mutex guard;
std::condition_variable signal;
void send( T t ) {
{
std::unique_lock<std::mutex> _(guard);
data = std::move(t);
}
signal.notify_one();
}
void abort() {
{
std::unique_lock<std::mutex> _(guard);
abort_flag = true;
}
signal.notify_all();
}
std::experimental::optional<T> get() {
std::unique_lock<std::mutex> _(guard);
signal.wait( _, [this]()->bool{
return data || abort_flag;
});
if (abort_flag) return {};
T retval = std::move(*data);
data = {};
return retval;
}
};
Run Code Online (Sandbox Code Playgroud)
现在,每一个都send可以使a get在另一端取得成功.如果出现多个send,则a只消耗最新的一个get.如果abort_flag设置了if和when ,则get()立即返回{};
以上支持多个消费者和生产者.
可以使用上述内容的示例是预览状态源(例如,UI线程),以及一个或多个预览渲染器(其速度不足以在UI线程中运行).
预览状态将预览状态转储到data_passer<preview_state>willy-nilly中.渲染器竞争,其中一个抓住它.然后他们渲染它,并将其传回(通过任何机制).
如果预览状态比渲染器消耗它们更快,那么只有最新的状态才有意义,所以先前的那些被丢弃.但现有的预览并没有因为一个新的州出现而中止.
下面提到有关种族条件的问题.
如果传递的数据是atomic,我们不能在没有"发送"端的互斥量的情况下做到吗?
所以像这样:
template<class T>
struct data_passer {
std::atomic<std::experimental::optional<T>> data;
std::atomic<bool> abort_flag = false;
std::mutex guard;
std::condition_variable signal;
void send( T t ) {
data = std::move(t); // 1a
signal.notify_one(); // 1b
}
void abort() {
abort_flag = true; // 1a
signal.notify_all(); // 1b
}
std::experimental::optional<T> get() {
std::unique_lock<std::mutex> _(guard); // 2a
signal.wait( _, [this]()->bool{ // 2b
return data.load() || abort_flag.load(); // 2c
});
if (abort_flag.load()) return {};
T retval = std::move(*data.load());
// data = std::experimental::nullopt; // doesn't make sense
return retval;
}
};
Run Code Online (Sandbox Code Playgroud)
以上都无法奏效.
我们从监听线程开始.它执行步骤2a,然后等待(2b).它在步骤2c评估条件,但还没有从lambda返回.
然后广播线程执行步骤1a(设置数据),然后发出条件变量的信号.此时,没有人在等待条件变量(lambda中的代码不计数!).
然后监听线程完成lambda,并返回"虚假唤醒".然后它会阻塞条件变量,并且从不会注意到数据已发送.
在std::mutex使用,而等待条件变量必须警惕在写数据"通过"的条件变量(你做任何测试,以确定是否唤醒是虚假的),读(在lambda),或可能性"丢失的信号"存在.(至少在一个简单的实现中:更复杂的实现可以为"常见情况"创建无锁路径,并且仅mutex在双重检查中使用.这超出了这个问题的范围.)
使用atomic变量并不能解决这个问题,因为"确定消息是否是虚假的"和"在条件变量中重新等待"这两个操作必须是关于消息"虚假"的原子.