dispenso 1.6.2
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
65namespace detail {
66// Drains every outstanding NewThreadInvoker thread. Idempotent; defined in
67// schedulable.cpp.
68DISPENSO_DLL_ACCESS void drainNewThreadInvokerThreads();
69
70// Runs the drain from the destructor of a static living in *the caller's*
71// module, which is the whole point of it being in a header.
72//
73// dispenso also registers the drain with atexit(), but in a shared build that
74// registration belongs to the dispenso DLL and therefore runs at
75// DLL_PROCESS_DETACH. ExitProcess reaches DLL_PROCESS_DETACH only after it has
76// already terminated every other thread in the process, so a drain registered
77// there can never see the threads it is meant to join -- they have been killed
78// mid-execution, which is exactly the access violation this avoids. Static
79// destructors in the executable run earlier, during ordinary exit processing
80// and before ExitProcess, while the threads are still alive and joinable.
81struct NewThreadDrainRegistrar {
82 ~NewThreadDrainRegistrar() {
83 drainNewThreadInvokerThreads();
84 }
85};
86
87inline void ensureNewThreadDrainRegistered() {
88 static NewThreadDrainRegistrar registrar;
89 (void)registrar;
90}
91} // namespace detail
92
99 public:
114 template <typename F>
115 DISPENSO_REQUIRES(OnceCallableFunc<F>)
116 void schedule(F&& f) const {
117 schedule(std::forward<F>(f), ForceQueuingTag());
118 }
126 template <typename F>
127 DISPENSO_REQUIRES(OnceCallableFunc<F>)
128 void schedule(F&& f, ForceQueuingTag) const {
129 // The thread is retained (not detached) and joined at process exit; see
130 // ThreadTracker for why detaching is unsafe on Windows. `done` is set by the
131 // thread as its very last act so schedule() can reap already-finished threads
132 // and keep retention bounded across a long-running process.
133 // Must precede thread creation: the registrar's destructor is what drains
134 // this thread at exit in a shared build.
135 detail::ensureNewThreadDrainRegistered();
136 auto done = std::make_shared<std::atomic<bool>>(false);
137 std::thread thread([f = std::move(f), done]() {
138 f();
139 done->store(true, std::memory_order_release);
140 });
141 getTracker()->add(std::move(thread), std::move(done));
142 }
143
144 private:
146 // NewThreadInvoker spawns one std::thread per schedule(). On Windows shared-lib
147 // builds a *detached* thread that is still executing during process exit faults
148 // (EXCEPTION_ACCESS_VIOLATION): it runs a synchronization primitive
149 // (e.g. WakeByAddressAll, from CompletionEvent::notify) after ntdll has begun
150 // tearing down its wait machinery at shutdown. See T282829604.
151 //
152 // The fix is to wait for each thread to FULLY terminate before shutdown proceeds
153 // -- not merely for its functor to return. Only the OS thread handle signals true
154 // termination (after the thread's last sync call and OS thread-exit), so we retain
155 // the threads joinable and join them from an atexit handler, which runs before
156 // module teardown. On Windows the wait is BOUNDED (see joinAll): a thread that
157 // cannot terminate -- e.g. one parked on the loader lock during a static-CRT
158 // DLL_PROCESS_DETACH -- must not wedge shutdown, so it is detached and left for
159 // process termination to reclaim (pinModuleForNewThread keeps our code mapped so
160 // that stays benign).
161 //
162 // The tracker is a controlled-leak singleton (schedulable.cpp); it is never
163 // destroyed, so a schedule() from a late static destructor still finds it valid.
164 struct ThreadTracker {
165 struct Entry {
166 std::thread thread;
167 // Set true by the thread as its last act. Lets add() reap finished threads
168 // without blocking; shared so the store outlives an entries_ reallocation.
169 std::shared_ptr<std::atomic<bool>> done;
170 };
171
172 std::mutex mtx_;
173 std::vector<Entry> entries_;
174
175 void add(std::thread&& t, std::shared_ptr<std::atomic<bool>> done)
176 DISPENSO_NO_THREAD_SAFETY_ANALYSIS {
177 // Opportunistically reap already-finished threads so entries_ does not grow
178 // unbounded over the lifetime of a long-running process. The joins happen
179 // after mtx_ is released: `done` only means the functor returned, and the
180 // OS-level teardown that follows is precisely the window this class does not
181 // trust, so it must not block every other schedule() behind the lock.
182 std::vector<std::thread> finished;
183 {
184 std::lock_guard<std::mutex> lk(mtx_);
185 for (size_t i = 0; i < entries_.size();) {
186 if (entries_[i].done->load(std::memory_order_acquire)) {
187 finished.push_back(std::move(entries_[i].thread));
188 entries_[i] = std::move(entries_.back());
189 entries_.pop_back();
190 } else {
191 ++i;
192 }
193 }
194 entries_.push_back(Entry{std::move(t), std::move(done)});
195 }
196 for (std::thread& thread : finished) {
197 thread.join(); // already finished -> returns promptly
198 }
199 }
200
201 // Defined in schedulable.cpp; joins each thread, bounded on Windows.
202 // Returns how many were still mid-functor, which is the count that says
203 // nothing had synchronized on them. Threads that had finished but were not
204 // yet reaped do not count.
205 size_t joinAll() DISPENSO_NO_THREAD_SAFETY_ANALYSIS;
206 };
207
208 DISPENSO_DLL_ACCESS static ThreadTracker* getTracker();
209
210 friend void detail::drainNewThreadInvokerThreads();
212};
213
214constexpr NewThreadInvoker kNewThreadInvoker;
215
216} // namespace dispenso
void schedule(F &&f) const
Definition schedulable.h:47
void schedule(F &&f) const