https://gcc.gnu.org/bugzilla/show_bug.cgi?id=92895

            Bug ID: 92895
           Summary: [libstdc++] stop_token conformance issues
           Product: gcc
           Version: unknown
            Status: UNCONFIRMED
          Severity: normal
          Priority: P3
         Component: libstdc++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: lewissbaker.opensource at gmail dot com
  Target Milestone: ---

The current implementation of std::stop_token in trunk has a few issues that
make it non-conformant with regards to the latest draft specification N4842.

The version I reviewed was:
https://github.com/gcc-mirror/gcc/blob/87f8610d5d61ceda80ca91be963bdf3e989af4f6/libstdc%2B%2B-v3/include/std/stop_token

Note that there is a reference implementation available here:
https://github.com/josuttis/jthread/blob/master/source/stop_token.hpp


stop_token::stop_possible()

  This implementation is just checking whether the stop_token has
  ownership of a shared-state.
  This is necessary as per [stoptoken.mem]/2.1, but insufficient.

  It also needs to take into account [stoptoken.mem]/2.2 which requires that
this
  method also return false when it does have ownership of a shared state but
where
  there is no associated stop_source.
  This generally requires keeping a separate ref-count for the number of
stop_source
  instances.

stop_token::_Stop_cb::_M_callback

  The function pointer type could be declared noexcept here.
  This might avoid some extra exception-handling codegen being generated in
  _M_request_stop() when this function pointer is invoked.

stop_token::_Stop_cb::_S_execute()

  It is valid for the invocation of a callback registered with the
stop_callback to
  end up calling the destructor of the stop_callback object from within the
callback.

  See [stopcallback.cons]/6 which says:
    "... If callback is executing on the current thread, then the destructor
does not block (3.6)
    waiting for the return from the invocation of callback. ..."

  The intent of this wording was to indicate that it is potentially valid for
the destructor of
  a stop_callback object to execute on the thread that is currently executing
that instance's
  registered callback. i.e. that the destructor might run inside the
user-provided callback.

  Thus the current logic that tries to update '__cb->_M_prev = __cb->_M_next =
nullptr;' after
  invoking the registered callback is thus potentially writing to a dangling
pointer. You would
  either need to update these pointers before invoking the callback or
otherwise somehow detect
  whether the destructor has been run during the invocation of the callback.

stop_token::_Stop_state_t::_M_request_stop()

  This method holds the _Stop_state_t::_M_mtx lock while invoking user-provided
callbacks.
  This prevents stop_callbacks from being deregistered concurrently while the
user-provided
  callback is executing.

  See [stopcallback.cons]/6 which says:
    "The destructor does not block waiting for the execution of another
callback
    registered by an associated stop_callback."

  This is necessary to prevent certain deadlock situations that would otherwise
arise were
  a callback to try to acquire a lock on some mutex that might currently be
held by another thread
  that is concurrently trying to deregister a stop_callback.

stop_source::operator=(stop_source&& rhs)

  This implementation just does a swap() which leaves rhs with the previous
state
  of *this rather than leaving rhs as an empty state.

  See [stopcallback.cons]/10 which says that this should instead be equivalent
to:
    stop_source(std::move(rhs)).swap(*this).

  ie. you first need to construct a temporary stop_source and call swap() with
that.

stop_callback::stop_callback(const stop_token&, Callback&&)
stop_callback::stop_callback(stop_token&&, Callback&&)

  These constructors are only supposed to throw exceptions that are thrown by
  the constructor of the callback object.

  See [stopcallback.cons]/5 which states:
    "Throws: Any exception thrown by the initialization of callback."

  This implementation calls the _Stop_state_t::_M_register_callback()
  method which is not declared noexcept and which calls std::mutex::lock()
which might
  throw a std::system_error if it failed to acquire the mutex lock.

  It might be easier to provide a noexcept implementation here if the
implementation was built
  on top of std::atomic and used std::atomic::wait()-based spin-locks instead
of std::mutex.


  These constructors also have a race-condition that fails to call the callback
being registered
  inline if request_stop() is called concurrently by another thread in-between
the call to
    if (__state->_M_stop_requested())
  and
    else if (__state->_M_register_callback(this))


stop_callback::~stop_callback()

  This needs to ensure that it blocks until the callback finishes executing if
the
  the callback is executing concurrently on another thread and that it does not
block
  if the callback is currently executing on the current thread.

  It also needs to ensure that it does not block waiting for the execution of
some
  other associated callback to complete.

  See [stopcallback.cons]/6 for more info.

Reply via email to