Hacktoberfest 2026 : les issues que les mainteneurs ont marquées pour octobre, ouvertes et accessibles aux débutants. Parcourir les issues Hacktoberfest

`timed_thread_context`: an operation stopped just before the context is destroyed never completes

Fermée
#2,334 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Les mainteneurs répondent en général sous 1 jour

Une pull request liée a déjà été fusionnée.

  • #2335 par @workingBanana — fusionnée

Évaluation

Difficulté
4/5
Temps estimé
3-5 jours
Accessibilité débutants
20/100
Type d'issue
Bug
Clarté
Plutôt claire
Activité
À l'abandon
Stack technique
cpp
Domaine
backend

Piste de recherche

The bug is in timed_thread_context::run() in the timed thread scheduler header that the reproducer includes (exec/timed_thread_scheduler.hpp). Read the loop that drains command_queue_ and the stop branch that closes submissions and calls set_stopped_() on the heap. Build the reproducer, which should report 1000 of 1000 lost completions. Done means every operation completes exactly once after a stop request races with destruction. Linked PR #2335 already proposes a fix, so check it before starting.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Description

Summary

When timed_thread_context::run() sees stop_requested_, it closes submissions, stops the operations in the heap and exits, without draining command_queue_ again:

while (true) {
  while (command_type* op = command_queue_.pop_front()) { ... }  // the only drain
  ...
  cv_.wait_until(lock, deadline, [this] { return ready_ || stop_requested_; });
  if (stop_requested) {
    // close submissions (CAS n_submissions_in_flight_ to context_closed)
    // set_stopped_() on every operation in heap_
    break;  // commands pushed since the drain above are never processed
  }
}

A stop request from another thread raises the operation's ref_count_ from 1 to 2 and queues a stop command. If the context is destroyed before its thread drains that command, the heap pass only drops the count to 1. The stop command, which would drop it to 0 and call set_stopped, is never processed. So the receiver gets neither set_value nor set_stopped, and anything waiting on it (sync_wait, async_scope::on_empty(), ...) hangs.

A schedule command queued after the last drain is lost the same way: it is never inserted into the heap, so the heap pass doesn't stop it.

This breaks the context's own shutdown guarantee: the destructor stops the operations in the heap, and submissions made after close complete inline with set_stopped.

It seems that the current behavior violates https://eel.is/c++draft/exec.async.ops#8: “After its start operation has begun executing, exactly one completion operation shall execute.”

Reproducer

#include <exec/timed_thread_scheduler.hpp>
#include <stdexec/execution.hpp>

#include <atomic>
#include <chrono>
#include <cstdio>
#include <optional>
#include <thread>

namespace ex = STDEXEC;

struct env {
  ex::inplace_stop_token token;
  auto query(ex::get_stop_token_t) const noexcept { return token; }
};

struct receiver {
  using receiver_concept = ex::receiver_tag;
  std::atomic<int>* completed;
  ex::inplace_stop_token token;
  void set_value() noexcept { completed->store(1); }
  void set_stopped() noexcept { completed->store(2); }
  env get_env() const noexcept { return {token}; }
};

int main() {
  int lost = 0;
  const int runs = 1000;
  for (int i = 0; i < runs; ++i) {
    std::atomic<int> completed{0};
    ex::inplace_stop_source source;
    std::optional<exec::timed_thread_context> ctx{std::in_place};
    auto op = ex::connect(exec::schedule_after(ctx->get_scheduler(), std::chrono::hours(1)),
                          receiver{&completed, source.get_token()});
    ex::start(op);
    std::this_thread::sleep_for(std::chrono::microseconds(200)); // the operation reaches the heap
    source.request_stop(); // queues a stop command
    ctx.reset();           // request_stop() + join, usually before the thread drains the command
    if (completed.load() == 0) { // the thread has exited, so nothing will ever complete it
      ++lost;
    }
  }
  std::printf("receiver never completed in %d of %d runs\n", lost, runs);
  return lost == 0 ? 0 : 1;
}

The output will be: receiver never completed in 1000 of 1000 runs. It should be 0 of 1000.

Suggested fix: https://github.com/NVIDIA/stdexec/pull/2335

Langage dominant
C++
Étoiles
2.5k
Forks
277
Merge moyen
1 j 16 h
PR mergées (30 j)
65

Préparer son environnement

Ce projet ne fournit ni conteneur de développement, ni Dockerfile, ni guide de contribution : l'installation est à votre charge. Commencez par son README, et consultez notre guide de la première contribution pour les étapes générales.

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de NVIDIA/stdexec

Toutes les issues de NVIDIA/stdexec

Issues similaires

Plus d'issues C++

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.