为什么对condition_variable没有等待功能,该功能不会重新锁定互斥锁 [英] Why is there no wait function for condition_variable which does not relock the mutex

查看:125
本文介绍了为什么对condition_variable没有等待功能,该功能不会重新锁定互斥锁的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

请考虑以下示例.

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();
}

g()知道"在我想讨论的场景中f()正在等待. 根据 cppreference.com ,无需g()进行锁定调用notify_one之前的互斥体.现在,在标记为"1"的行中,cv将释放互斥锁,并在发送通知后将其重新锁定. lock的析构函数在此之后立即再次释放它.这似乎是多余的,尤其是因为锁定很昂贵. (我知道在某些情况下需要锁定互斥锁.但这不是这种情况.)

g() "knows" that f() is waiting in the scenario I would like to discuss. According to cppreference.com there is no need for g() to lock the mutex before calling notify_one. Now in the line marked "1" cv will release the mutex and relock it once the notification is sent. The destructor of lock releases it again immediately after that. This seems to be superfluous especially since locking is expensive. (I know in certain scenarios the mutex needs to be locked. But this is not the case here.)

为什么condition_variable没有功能"wait_nolock",一旦通知到达,该功能就不会重新锁定互斥锁.如果答案是pthread不提供这样的功能:为什么不能扩展pthread以提供它?有没有其他方法可以实现所需的行为?

Why does condition_variable have no function "wait_nolock" which does not relock the mutex once the notification arrives. If the answer is that pthreads do not provide such functionality: Why can`t pthreads be extended for providing it? Is there an alternative for realizing the desired behavior?

推荐答案

您误解了代码的作用.

您在第// 1行上的代码完全不会阻塞. condition_variables可以(并且将会!)具有虚假的唤醒功能-它们根本没有充分的理由可以唤醒.

Your code on line // 1 is free to not block at all. condition_variables can (and will!) have spurious wakeups -- they can wake up for no good reason at all.

您有责任检查唤醒是否是虚假的.

You are responsible for checking if the wakeup is spurious.

正确使用condition_variable需要3件事:

  • A condition_variable
  • A mutex
  • mutex
  • 保护的某些数据
  • A condition_variable
  • A mutex
  • Some data guarded by the mutex

互斥锁保护的数据被修改(在mutex下).然后(mutex可能已脱离),通知condition_variable.

The data guarded by the mutex is modified (under the mutex). Then (with the mutex possibly disengaged), the condition_variable is notified.

在另一端,您锁定mutex,然后等待条件变量.唤醒时,您的mutex将被重新锁定,然后通过查看mutex保护的数据来测试唤醒是否是虚假的.如果是有效的唤醒,则进行处理并继续.

On the other end, you lock the mutex, then wait on the condition variable. When you wake up, your mutex is relocked, and you test if the wakeup is spurious by looking at the data guarded by the mutex. If it is a valid wakeup, you process and proceed.

如果这不是有效的唤醒方式,请返回等待状态.

If it wasn't a valid wakeup, you go back to waiting.

在您的情况下,您没有任何数据保护,您无法将虚假唤醒与真实唤醒区分开,并且设计不完整.

In your case, you don't have any data guarded, you cannot distinguish spurious wakeups from real ones, and your design is incomplete.

对于不完整的设计,不足为奇的是您看不到mutex被重新锁定的原因:它被重新锁定,因此您可以安全地检查数据以查看唤醒是否是虚假的.

Not surprisingly with the incomplete design you don't see the reason why the mutex is relocked: it is relocked so you can safely check the data to see if the wakeup was spurious or not.

如果您想知道为什么以这种方式设计条件变量,可能是因为这种设计比可靠"的设计更有效(无论出于何种原因),并且与其公开更高级别的基元,还不如公开更高级别的基元.原语.

If you want to know why condition variables are designed that way, probably because this design is more efficient than the "reliable" one (for whatever reason), and rather than exposing higher level primitives, C++ exposed the lower level more efficient primitives.

在此之上构建更高级别的抽象并不难,但是有设计决定.这是在std::experimental::optional之上构建的:

Building a higher level abstraction on top of this isn't hard, but there are design decisions. Here is one built on top of 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;
  }
};

现在,每个send都可以使get在另一端成功.如果出现多个send,则get仅消耗最新的一个.如果设置了abort_flag,则get()会立即返回{};

Now, each send can cause a get to succeed at the other end. If more than one send occurs, only the latest one is consumed by a get. If and when abort_flag is set, instead get() immediately returns {};

以上内容支持多个消费者和生产者.

The above supports multiple consumers and producers.

如何使用上述示例的一个示例是预览状态的来源(例如,UI线程),以及一个或多个预览渲染器(不够快,无法在UI线程中运行).

An example of how the above might be used is a source of preview state (say, a UI thread), and one or more preview renderers (which are not fast enough to be run in the UI thread).

预览状态将预览状态转储到data_passer<preview_state> willy-nilly中.渲染器竞争,其中之一抓住了它.然后他们渲染它,并将其传递回(通过任何机制).

The preview state dumps a preview state into the data_passer<preview_state> willy-nilly. The renderers compete and one of them grabs it. Then they render it, and pass it back (through whatever mechanism).

如果预览状态比渲染器消耗它们更快,则仅关注最新状态,因此将丢弃较早的状态.但是现有的预览不会仅仅因为显示新状态而中止.

If the preview states come faster than the renderers consume them, only the most recent one is of interest, so the earlier ones are discarded. But existing previews aren't aborted just because a new state shows up.

以下是有关比赛条件的问题.

Questions where asked below about race conditions.

如果正在传送的数据是atomic,没有发送"侧的互斥锁,我们不能做吗?

If the data being communicated is atomic, can't we do without the mutex on the "send" side?

是这样的:

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;
  }
};

以上操作无效.

我们从侦听线程开始.它执行步骤2a,然后等待(2b).它在步骤2c中评估条件,但尚未从lambda返回.

We start with the listening thread. It does step 2a, then waits (2b). It evaluates the condition at step 2c, but doesn't return from the lambda yet.

然后,广播线程执行步骤1a(设置数据),然后向条件变量发出信号.此时,没有人在等待条件变量(lambda中的代码不计算在内!).

The broadcasting thread then does step 1a (setting the data), then signals the condition variable. At this moment, nobody is waiting on the condition variable (the code in the lambda doesn't count!).

然后,侦听线程完成lambda,并返回虚假唤醒".然后,它将阻止条件变量,并且永远不会注意到已发送数据.

The listening thread then finishes the lambda, and returns "spurious wakeup". It then blocks on the condition variable, and never notices that data was sent.

在等待条件变量时使用的std::mutex必须保护对条件变量通过"的数据的写入(无论执行何种测试以确定唤醒是否是虚假的)和读取(在lambda中) ,或者存在信号丢失"的可能性. (至少在一个简单的实现中:更复杂的实现可以为常见情况"创建无锁路径,并且仅在仔细检查中使用mutex.这不在此问题的范围之内.)

The std::mutex used while waiting on the condition variable must guard the write to the data "passed" by the condition variable (whatever test you do to determine if the wakeup was spurious), and the read (in the lambda), or the possibility of "lost signals" exists. (At least in a simple implementation: more complex implementations can create lock-free paths for "common cases" and only use the mutex in a double-check. This is beyond the scope of this question.)

使用atomic变量不能解决此问题,因为就消息的虚假性"而言,确定消息是否为伪造"和在条件变量中等待"这两个操作必须是原子的.

Using atomic variables does not get around this problem, because the two operations of "determine if the message was spurious" and "rewait in the condition variable" must be atomic with regards to the "spuriousness" of the message.

这篇关于为什么对condition_variable没有等待功能,该功能不会重新锁定互斥锁的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆