Есть ли безопасный способ вызвать wait() на std:: future?

В стандарте С++ 11 говорится:

30.6.6 Следующий шаблонный шаблон

(3) "Эффект вызова любой функции-члена, отличной от деструктора, оператор присваивания перемещения или действителен для будущего объекта, для которого valid() == false - undefined."

Итак, означает ли это, что следующий код может встретить поведение undefined?

void wait_for_future(std::future<void> & f)
{
    if (f.valid()) {
        // what if another thread meanwhile calls get() on f (which invalidates f)?
        f.wait();
    }
    else {
        return;
    }
}

Q1: Это действительно возможное поведение undefined?

Q2: Есть ли стандартный способ, позволяющий избежать возможного поведения undefined?

Обратите внимание, что стандарт имеет интересную заметку [также в 30.6.6 (3)]:

"[Примечание. Реализации поощряются чтобы обнаружить этот случай и бросить объект типа future_error с помощью условие ошибки future_errc::no_state. -endnote]"

Q3: Это нормально, если я просто полагаюсь на стандартную ноту и просто использую f.wait() без проверки f действительности?

void wait_for_future(std::future<void> & f)
{
    try {
        f.wait();
    }
    catch (std::future_error const & err) {
        return;
    }
}

EDIT: Резюме после получения ответов и дальнейших исследований по теме

Как оказалось, реальная проблема с моим примером была не напрямую из-за параллельных модификаций (из одного потока вызывается один модифицирующий get, другой поток, называемый valid и wait, который должен быть безопасным).

Реальная проблема заключалась в том, что функция std::future object get была доступна из другого потока, который не предназначен для использования! Объект std::future должен использоваться только из одного потока!

Единственный другой поток, который задействован, - это поток, который устанавливает общее состояние: через возврат из функции, переданной в std::async, или вызов set_value для связанного объекта std::promise и т.д.

Больше: даже wait Включение объекта std::future из другого потока не является предполагаемым поведением (из-за того же самого UB, что и в моем примере # 1). Мы будем использовать std::shared_future для этого случая использования, каждый поток которого имеет собственную копию объекта std::shared_future. Обратите внимание, что все это не через один и тот же общий объект std::future, но через отдельные (связанные) объекты!

Итог: Эти объекты не должны делиться между потоками. Используйте отдельный (связанный) объект в каждом потоке.

Ответы

Ответ 1

Обычный std::future сам по себе не является потокобезопасным. Так что да, это UB, если вы вызываете модификационные функции из нескольких потоков на одном std::future, поскольку у вас есть потенциальное состояние гонки. Хотя вызов wait из нескольких потоков в порядке, поскольку он const/non-modify.

Однако, если вам действительно нужно получить доступ к возвращаемому значению std::future из нескольких потоков, вы можете сначала вызвать std::future::share в будущем, чтобы получить std::shared_future, который вы можете скопировать в каждый поток, а затем каждый поток может вызвать get. Обратите внимание, что важно, чтобы каждый поток имел свой собственный объект std::shared_future.

Вам нужно только проверить правильность, если возможно, что ваше будущее может быть недействительным, что не относится к нормальным нормам usecases (std::async и т.д.) и правильному использованию (например: не callig get дважды).

Ответ 2

Фьючерсы позволяют сохранять состояние из одного потока и извлекать его из другого. Они не обеспечивают дополнительную безопасность потока.

Это действительно возможное поведение undefined?

Если у вас есть два потока, пытающихся получить состояние будущего без синхронизации, да. Я понятия не имею, почему вы можете это сделать.

Есть ли стандартный способ, позволяющий избежать возможного поведения undefined?

Попробуйте только получить состояние из одного потока; или, если вам действительно нужно поделиться им между потоками, используйте мьютекс или другую синхронизацию.

Это нормально, если я просто полагаюсь на стандартную заметку

Если вы знаете, что единственные реализации, которые вам нужны для поддержки, следуют этой рекомендации, да. Но не должно быть необходимости.

и просто используйте f.wait() без проверки f достоверности?

Если вы не делаете каких-либо странных махинаций с несколькими потоками, обращающимися к будущему, вы можете просто предположить, что они действительны до тех пор, пока вы не восстановите состояние (или не переместили его в другое будущее).