Std:: обещание set_value и безопасность потоков
Я немного запутался в требованиях с точки зрения безопасности потоков, размещенных в std::promise::set_value()
.
Стандарт гласит:
Эффекты: атомарно сохраняет значение r в общем состоянии и делает это состояние готовым
Тем не менее, он также говорит, что promise::set_value()
может использоваться только для установки значения один раз. Если он вызывается несколько раз, std::future_error
. Таким образом, вы можете установить значение обещания только один раз.
И действительно, почти каждый учебник, пример кода в сети или фактический вариант использования для std::promise
включает канал связи между двумя потоками, где один поток вызывает std::future::get()
, а другой поток вызывает std::promise::set_value()
.
Я никогда не видел случая использования, когда несколько потоков могли бы вызывать std::promise::set_value()
, и даже если бы они это делали, все, кроме одного, вызывали бы исключение std::future_error
.
Так почему же стандартный мандат, который вызывает std::promise::set_value()
является атомарным? Каков вариант использования для одновременного вызова std::promise::set_value()
из нескольких потоков?
РЕДАКТИРОВАТЬ:
Так как ответ с наибольшим количеством голосов здесь на самом деле не отвечает на мой вопрос, я предполагаю, что то, что я спрашиваю, неясно. Итак, чтобы уточнить: я в курсе, что такое будущее и обещания и как они работают. У меня вопрос, почему, в частности, стандарт настаивает на том, что std::promise::set_value()
должен быть атомарным? Это более тонкий вопрос, чем "почему не должно быть гонки между вызовами promise::set_value()
и вызовами future::get()
"?
На самом деле, многие ответы здесь (неправильно) отвечают, что причина в том, что если std::promise::set_value()
не является атомарным, то std::future::get()
потенциально может вызвать состояние гонки. Но это не так.
Единственное требование избегать условия гонки - это то, что std::promise::set_value()
должна иметь отношение " происходит до" с std::future::get()
- другими словами, должно быть гарантировано, что когда std::future::wait()
возвращает, std::promise::set_value()
завершено.
Это полностью ортогонально тому, что std::promise::set_value()
сам по себе является атомарным или нет. В типичной реализации, использующей условные переменные, std::future::get()/wait()
будет ожидать условную переменную. Затем std::promise::set_value()
может std::promise::set_value()
выполнять любые произвольно сложные вычисления, чтобы установить фактическое значение. Затем он уведомил бы переменную общего условия (подразумевающую ограничение памяти с семантикой выпуска), и std::future::get()
проснулся бы и безопасно прочитал результат.
Таким образом, std::promise::set_value()
само по себе не обязательно должно быть атомарным, чтобы избежать здесь условия гонки - оно просто должно удовлетворять отношению "происходит до" с std::future::get()
.
Итак, еще раз, мой вопрос: почему стандарт C++ настаивает на том, что std::promise::set_value()
должна быть атомарной операцией, как если бы вызов std::promise::set_value()
был выполнен полностью в блокировка мьютекса? Я не вижу причин, по которым это требование должно существовать, если только не существует какой-либо причины или std::promise::set_value()
использования для нескольких потоков, вызывающих std::promise::set_value()
одновременно. И я не могу придумать такой вариант использования, отсюда и этот вопрос.
Ответы
Ответ 1
Если это не было хранилище атомов, то два потока могли одновременно вызывать promise::set_value
, что делает следующее:
- убедитесь, что будущее не готово (т.е. имеет сохраненное значение или исключение)
- сохранить значение
-
- отметить состояние готовности
- освободить что-либо, блокирующее доступное общее состояние.
Выполняя эту последовательность атома, первый поток, выполняемый (1), доходит до (3), и любой другой поток, вызывающий promise::set_value
в то же время, терпит неудачу (1) и поднимет a future_error
с promise_already_satisfied
.
Без атомарности два потока могли бы потенциально сохранить свое значение, а затем можно было бы успешно пометить состояние готово, а другое - создать исключение, то есть тот же результат кроме, который может быть значение из потока, который видел исключение, которое прошло.
Во многих случаях, которые могут не иметь значения, какой поток "выигрывает", но когда это имеет значение, без гарантии атомарности вам нужно будет обернуть еще один мьютекс вокруг вызова promise::set_value
. Другие подходы, такие как сравнение и обмен, не будут работать, потому что вы не можете проверить будущее (если это не a shared_future
), чтобы узнать, выиграло ли ваше значение или нет.
Когда не имеет значения, какой поток "выигрывает", вы можете дать каждой теме свое собственное будущее и использовать std::experimental::when_any
для соберите первый результат, который стал доступным.
Редактировать после некоторых исторических исследований:
Хотя вышеупомянутое (два потока, использующие один и тот же объект обещания), не похоже на хороший прецедент, он, безусловно, предусматривался одной из современных статей введения future
в С++: N2744. В этом документе было предложено несколько вариантов использования, в которых были конфликтующие потоки, вызывающие set_value
, и я приведу их здесь:
Во-вторых, рассмотрите случаи, когда две или более асинхронные операции выполняются параллельно и "конкурируют", чтобы удовлетворить обещание. Вот некоторые примеры:
- Последовательность сетевых операций (например, запрос веб-страницы) выполняется в сочетании с ожиданием таймера.
- Значение может быть получено с нескольких серверов. Для избыточности все серверы проверяются, но требуется только первое полученное значение.
В обоих примерах первая асинхронная операция для завершения - это та, которая удовлетворяет обещанию. Поскольку любая операция может завершиться второй, код для обоих должен быть написан, чтобы ожидать, что вызовы на set_value()
могут выйти из строя.
Ответ 2
Я никогда не видел прецедента, который может вызвать несколько потоков std:: promise:: set_value(), и даже если бы они это сделали, все, кроме одного, вызывают исключение std:: future_error.
Вы пропустили всю идею promises и фьючерсов.
Обычно у нас есть пара обещаний и будущее. обещание - это объект, который вы нажимаете асинхронный результат или исключение, а будущее - это объект, который вы тянете асинхронный результат или исключение.
В большинстве случаев будущее и пара обещаний не находятся в одном потоке (в противном случае мы бы использовали простой указатель). поэтому вы можете передать обещание какой-либо потоковой, потоковой или какой-либо третьей асинхронной функции библиотеки и установить результат оттуда и вывести результат в поток вызывающего.
установка результата с помощью std::promise::set_value
должна быть атомарной, а не потому, что многие promises устанавливают результат, а потому, что объект (будущее), который находится в другом потоке, должен прочитать результат, а его неатомно - undefined, поэтому установка значения и вытягивание его (либо путем вызова std::future::get
, либо std::future::then
) должно произойти атомарно
Помните, что каждое будущее и обещание имеет разделяемое состояние, устанавливая результат из одного потока, обновляет общее состояние и получает результат чтения из общего состояния. как каждое разделяемое состояние/память на С++, когда это делается из нескольких потоков, обновление/чтение должно происходить под блокировкой. в противном случае это поведение undefined.
Ответ 3
Все это хорошие ответы, но есть еще один важный момент. Без атомарности установки значения, чтение значения может быть предметом наблюдаемых побочных эффектов.
Например, в наивной реализации:
void thread1()
{
// do something. Maybe read from disk, or perform computation to populate value
v = value;
flag = true;
}
void thread2()
{
if(flag)
{
v2 = v;//Here we have a read problem.
}
}
Атомарность в std::promise<>
позволяет вам избежать очень простого состояния гонки между записью значения в одном потоке и чтением в другом. Конечно, если flag был std::atomic<>
и используются правильные флаги забора, у вас больше нет побочных эффектов, и std::promise
это.