Состояние гонки в pthread_once()?

У меня есть std::future в одном потоке, который ждет, когда std::promise устанавливается в другом потоке.

EDIT: Обновлен вопрос с помощью примерного приложения, которое будет блокироваться навсегда:

UPDATE: Если вместо этого я использую pthread_barrier, нижеприведенный код делает не.

Я создал тестовое приложение, которое иллюстрирует это:

В основном класс foo создает thread, который устанавливает promise в своей функции запуска и ждет в конструкторе для этого promise, который должен быть установлен. После установки он увеличивает значение atomic count

Затем создаю кучу этих объектов foo, оторвав их, а затем проверьте мои count.

#include <iostream>
#include <thread>
#include <atomic>
#include <future>
#include <list>
#include <unistd.h>

struct foo
{
    foo(std::atomic<int>& count)
        : _stop(false)
    {
        std::promise<void> p;
        std::future <void> f = p.get_future();

        _thread = std::move(std::thread(std::bind(&foo::run, this, std::ref(p))));

        // block caller until my thread has started 
        f.wait();

        ++count; // my thread has started, increment the count
    }
    void run(std::promise<void>& p)
    {
        p.set_value(); // thread has started, wake up the future

        while (!_stop)
            sleep(1);
    }
    std::thread _thread;
    bool _stop;
};

int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        std::cerr << "usage: " << argv[0] << " num_threads" << std::endl;
        return 1;
    }
    int num_threads = atoi(argv[1]);
    std::list<foo*> threads;
    std::atomic<int> count(0); // count will be inc'd once per thread

    std::cout << "creating threads" << std::endl;
    for (int i = 0; i < num_threads; ++i)
        threads.push_back(new foo(count));

    std::cout << "stopping threads" << std::endl;
    for (auto f : threads)
        f->_stop = true;

    std::cout << "joining threads" << std::endl;
    for (auto f : threads)
    {
        if (f->_thread.joinable())
            f->_thread.join();
    }

    std::cout << "count=" << count << (num_threads == count ? " pass" : " fail!") << std::endl;
    return (num_threads == count);
}

Если я запустил это в цикле с 1000 потоками, ему нужно выполнить его несколько раз, пока не произойдет гонка, и один из futures никогда не проснулся, и поэтому приложение застряло навсегда.

# this loop never completes
$ for i in {1..1000}; do ./a.out 1000; done

Если я теперь SIGABRT приложение, результирующая трассировка стека показывает, что он застрял на future::wait Трассировка стека ниже:

// main thread
    [email protected]@GLIBC_2.3.2 () from /lib64/libpthread.so.0
    __gthread_cond_wait (__mutex=<optimized out>, __cond=<optimized out>) at libstdc++-v3/include/x86_64-unknown-linux-gnu/bits/gthr-default.h:846
    std::condition_variable::wait (this=<optimized out>, __lock=...) at ../../../../libstdc++-v3/src/condition_variable.cc:56
    std::condition_variable::wait<std::__future_base::_State_base::wait()::{lambda()#1}>(std::unique_lock<std::mutex>&, std::__future_base::_State_base::wait()::{lambda()#1}) (this=0x93a050, __lock=..., __p=...) at include/c++/4.7.0/condition_variable:93
    std::__future_base::_State_base::wait (this=0x93a018) at include/c++/4.7.0/future:331
    std::__basic_future<void>::wait (this=0x7fff32587870) at include/c++/4.7.0/future:576
    foo::foo (this=0x938320, count=...) at main.cpp:18
    main (argc=2, argv=0x7fff32587aa8) at main.cpp:52


// foo thread
    pthread_once () from /lib64/libpthread.so.0
    __gthread_once (__once=0x93a084, __func=0x4378a0 <[email protected]>) at gthr-default.h:718
    std::call_once<void (std::__future_base::_State_base::*)(std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()>&, bool&), std::__future_base::_State_base* const, std::reference_wrapper<std::function<std::unique_ptr<std::__future_base::_Result_base, std::__future_base::_Result_base::_Deleter> ()> >, std::reference_wrapper<bool> >(std::once_flag&, void (std::__future_base::_State_base::*&&)(std::function<std::unique_ptr<std::__future_base::_Result_base, ...) at include/c++/4.7.0/mutex:819
    std::promise<void>::set_value (this=0x7fff32587880) at include/c++/4.7.0/future:1206
    foo::run (this=0x938320, p=...) at main.cpp:26

Я уверен, что я не делаю ничего плохого в своем коде, верно?

Это проблема с реализацией pthread или реализация std:: future/std:: prom?

Мои версии библиотеки:

libstdc++.so.6
libc.so.6 (GNU C Library stable release version 2.11.1 (20100118))
libpthread.so.0 (Native POSIX Threads Library by Ulrich Drepper et al Copyright (C) 2006)

Ответы

Ответ 1

Действительно, между деструктором локального объекта promise существует условие расы (в конце конструктора и вызов set_value() из потока. То есть set_value() просыпает основной протектор, который сразу же уничтожает объект обещания, но функция set_value() еще не закончена, а мертвые блокировки.

Чтение стандарта С++ 11, я не уверен, разрешено ли ваше использование:

void promise<void>::set_value();

Эффекты: атомарно сохраняет значение r в общем состоянии и делает это состояние готовым.

Но где-то еще:

Элементы set_value, set_exception, set_value_at_thread_exit и set_exception_at_thread_exit ведут себя так, как будто они приобретают один мьютекс, связанный с объектом обещания, при обновлении объекта обещания.

Являются ли вызовы set_value() атомами относительно других функций, таких как деструктор?

ИМХО, я бы сказал, нет. Эффекты будут сопоставимы с уничтожением мьютекса, в то время как другой поток все еще блокирует его. Результатом является undefined.

Решение заключалось бы в том, чтобы p пережить поток. Два решения, о которых я могу думать:

  • Сделайте p членом класса, как предложил Майкл Берр в другом ответе.

  • Переместите обещание в поток.

В конструкторе:

std::promise<void> p;
std::future <void> f = p.get_future();
_thread = std::thread(&foo::run, this, std::move(p));

Кстати, вам не нужен вызов bind, (конструктор потока уже перегружен) или вызовите std::move для перемещения потока (правое значение уже является r-значением). Однако вызов std::move в обещание является обязательным.

И функция потока не получает ссылку, но перенесенное обещание:

void run(std::promise<void> p)
{
    p.set_value();
}

Я думаю, что именно поэтому С++ 11 определяет два разных класса: promise и future: вы переносите обещание в поток, но вы сохраняете будущее для восстановления результата.

Ответ 2

Попробуйте переместить std::promise<void> p;, чтобы вместо того, чтобы быть локальным для конструктора, он будет членом struct foo:

struct foo
{
    foo(std::atomic<int>& count)
        : _stop(false)
    {
        // std::promise<void> p;    // <-- moved to be a member
        std::future <void> f = p.get_future();

        // ...same as before...
    }
    void run(std::promise<void>& p)
    {
        // ... same ...
    }

    std::promise<void> p;   // <---
    std::thread _thread;
    bool _stop;
};

Я полагаю, что может произойти то, что вы попадаете в расу, где p уничтожается в конструкторе, а p.set_value() действует на ссылку на promise. Что-то происходит внутри set_value(), пока оно заканчивается/очищается; действующий на ссылку на уже уничтоженный std::promise, искажает некоторое состояние в библиотеке pthread.

Это просто предположение - у меня нет готового доступа к системе, которая воспроизводит проблему на данный момент. Но создание p члена гарантирует, что его время жизни продлится задолго до завершения вызова set_value().