0

Following the question from here, which explains to some degree why getting a std::stop_token internally instead of getting it as parameter to the thread wrapper function leads to a race-condition. I want to understand more exactly what is the problem. Looking at the implementation of the jthread ctor in the msvc, it seems that the member _Ssource is actually constructed before conditionally invoking the correct _Start, depending if a std::stop_token a parameter is the first parameter.

jthread() noexcept : _Impl{}, _Ssource{nostopstate} {}

template <class _Fn, class... _Args, enable_if_t<!is_same_v<remove_cvref_t<_Fn>, jthread>, int> = 0>
_NODISCARD_CTOR explicit jthread(_Fn&& _Fx, _Args&&... _Ax) {
    if constexpr (is_invocable_v<decay_t<_Fn>, stop_token, decay_t<_Args>...>) {
        _Impl._Start(_STD forward<_Fn>(_Fx), _Ssource.get_token(), _STD forward<_Args>(_Ax)...);
    } else {
        _Impl._Start(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
    }
}

Before the _Start we are still on the caller thread and all the memory should be visible to the new thread, which has not yet started. Then on the new thread you would call get_stop_token

auto t = std::jthread{ [&]() {

    auto token = t.get_stop_token();

which is implemented like this and should return a copy of the token from the _Ssource:

_NODISCARD stop_token get_stop_token() const noexcept {
    return _Ssource.get_token();
}

Where is the race condition? Is the _Source modified after the new thread was actually started but before the _Start returns on the original thread?

  • 1
    The question you cite starts off with an incorrect assumption, namely "it has no guarantee that the `std::jthread` object is actually constructed by the time that it [the new thread] begins executing." The `jthread` object is too guaranteed to be fully constructed by the time the thread function begins executing: "**[thread.jthread.cons]/7** *Synchronization:* The completion of the invocation of the constructor synchronizes with the beginning of the invocation of the copy of `f`." I don't believe there's any data race in your example. – Igor Tandetnik Feb 24 '23 at 00:31
  • 1
    Actually, it depends on what happens to `t` next. Say you have `std::vector threads; auto t = std::jthread{..}; threads.push_back(std::move(t));` Then there's a race - by the time the thread gets around to calling `t.get_stop_token()`, `t` might already have been moved from. In general, it's dangerous for a thread to capture a local variable from the calling thread by reference. Just make your lambda take `std::stop_token` as its first parameter; it gets passed automatically. – Igor Tandetnik Feb 24 '23 at 00:48
  • You're totally right, on the accepted answer from the actual original question (https://stackoverflow.com/questions/65700593/stdjthread-runs-a-member-function-from-another-member-function/75397590#75397590) the thread is a class member, so the line `j1 = std::jthread(&JT::init, this);` involves a constructor and a move operator, so the race happens as you describe it. – Liviu Stancu Feb 24 '23 at 14:57

1 Answers1

0

I found the answer thanks to Igor Tandetnik

std::jthread t; (0)
t = std::jthread{ [&]() { // (1) and (2)

    auto token = t.get_stop_token(); // (3)
    }
};

The line 2 involves two operations: construction of temporary object(1) and move assignment operator(2), which will move this temporary object into previously default constructed t object.

After (1) has happened, the thread can already start concurrently executing instructions from (3). This could result in the stop token being copied from the default constructed object (or other partially object, since the instructions for the move operator are interlacing with the instructions from line 3.