dispenso 1.6.0
A library for task parallelism
Loading...
Searching...
No Matches
schedulable.h
Go to the documentation of this file.
1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
15#pragma once
16
17#include <atomic>
18#include <cassert>
19#include <chrono>
20#include <memory>
21#include <mutex>
22#include <thread>
23#include <vector>
24
25#include <dispenso/detail/completion_event_impl.h>
26#include <dispenso/task_set.h>
27
28namespace dispenso {
29
37 public:
45 template <typename F>
46 DISPENSO_REQUIRES(OnceCallableFunc<F>)
47 void schedule(F&& f) const {
48 f();
49 }
50
56 template <typename F>
57 DISPENSO_REQUIRES(OnceCallableFunc<F>)
58 void schedule(F&& f, ForceQueuingTag) const {
59 f();
60 }
61};
62
63constexpr ImmediateInvoker kImmediateInvoker;
64
71 public:
79 template <typename F>
80 DISPENSO_REQUIRES(OnceCallableFunc<F>)
81 void schedule(F&& f) const {
82 schedule(std::forward<F>(f), ForceQueuingTag());
83 }
91 template <typename F>
92 DISPENSO_REQUIRES(OnceCallableFunc<F>)
93 void schedule(F&& f, ForceQueuingTag) const {
94 // The thread is retained (not detached) and joined at process exit; see
95 // ThreadTracker for why detaching is unsafe on Windows. `done` is set by the
96 // thread as its very last act so schedule() can reap already-finished threads
97 // and keep retention bounded across a long-running process.
98 auto done = std::make_shared<std::atomic<bool>>(false);
99 std::thread thread([f = std::move(f), done]() {
100 f();
101 done->store(true, std::memory_order_release);
102 });
103 getTracker()->add(std::move(thread), std::move(done));
104 }
105
106 private:
108 // NewThreadInvoker spawns one std::thread per schedule(). On Windows shared-lib
109 // builds a *detached* thread that is still executing during process exit faults
110 // (EXCEPTION_ACCESS_VIOLATION): it runs a synchronization primitive
111 // (e.g. WakeByAddressAll, from CompletionEvent::notify) after ntdll has begun
112 // tearing down its wait machinery at shutdown. See T282829604.
113 //
114 // The fix is to wait for each thread to FULLY terminate before shutdown proceeds
115 // -- not merely for its functor to return. Only the OS thread handle signals true
116 // termination (after the thread's last sync call and OS thread-exit), so we retain
117 // the threads joinable and join them from an atexit handler, which runs before
118 // module teardown. On Windows the wait is BOUNDED (see joinAll): a thread that
119 // cannot terminate -- e.g. one parked on the loader lock during a static-CRT
120 // DLL_PROCESS_DETACH -- must not wedge shutdown, so it is detached and left for
121 // process termination to reclaim (pinModuleForNewThread keeps our code mapped so
122 // that stays benign).
123 //
124 // The tracker is a controlled-leak singleton (schedulable.cpp); it is never
125 // destroyed, so a schedule() from a late static destructor still finds it valid.
126 struct ThreadTracker {
127 struct Entry {
128 std::thread thread;
129 // Set true by the thread as its last act. Lets add() reap finished threads
130 // without blocking; shared so the store outlives an entries_ reallocation.
131 std::shared_ptr<std::atomic<bool>> done;
132 };
133
134 std::mutex mtx_;
135 std::vector<Entry> entries_;
136
137 void add(std::thread&& t, std::shared_ptr<std::atomic<bool>> done)
138 DISPENSO_NO_THREAD_SAFETY_ANALYSIS {
139 // Opportunistically reap already-finished threads so entries_ does not grow
140 // unbounded over the lifetime of a long-running process. The joins happen
141 // after mtx_ is released: `done` only means the functor returned, and the
142 // OS-level teardown that follows is precisely the window this class does not
143 // trust, so it must not block every other schedule() behind the lock.
144 std::vector<std::thread> finished;
145 {
146 std::lock_guard<std::mutex> lk(mtx_);
147 for (size_t i = 0; i < entries_.size();) {
148 if (entries_[i].done->load(std::memory_order_acquire)) {
149 finished.push_back(std::move(entries_[i].thread));
150 entries_[i] = std::move(entries_.back());
151 entries_.pop_back();
152 } else {
153 ++i;
154 }
155 }
156 entries_.push_back(Entry{std::move(t), std::move(done)});
157 }
158 for (std::thread& thread : finished) {
159 thread.join(); // already finished -> returns promptly
160 }
161 }
162
163 // Defined in schedulable.cpp; joins each thread, bounded on Windows.
164 void joinAll() DISPENSO_NO_THREAD_SAFETY_ANALYSIS;
165 };
166
167 DISPENSO_DLL_ACCESS static ThreadTracker* getTracker();
169};
170
171constexpr NewThreadInvoker kNewThreadInvoker;
172
173} // namespace dispenso
void schedule(F &&f) const
Definition schedulable.h:47
void schedule(F &&f) const
Definition schedulable.h:81