dispenso 1.5.1
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 <cassert>
18#include <condition_variable>
19#include <mutex>
20#include <thread>
21
22#include <dispenso/detail/completion_event_impl.h>
23#include <dispenso/task_set.h>
24
25namespace dispenso {
26
34 public:
42 template <typename F>
43 DISPENSO_REQUIRES(OnceCallableFunc<F>)
44 void schedule(F&& f) const {
45 f();
46 }
47
53 template <typename F>
54 DISPENSO_REQUIRES(OnceCallableFunc<F>)
55 void schedule(F&& f, ForceQueuingTag) const {
56 f();
57 }
58};
59
60constexpr ImmediateInvoker kImmediateInvoker;
61
68 public:
76 template <typename F>
77 DISPENSO_REQUIRES(OnceCallableFunc<F>)
78 void schedule(F&& f) const {
79 schedule(std::forward<F>(f), ForceQueuingTag());
80 }
88 template <typename F>
89 DISPENSO_REQUIRES(OnceCallableFunc<F>)
90 void schedule(F&& f, ForceQueuingTag) const {
91 auto* waiter = getWaiter();
92 waiter->add();
93 std::thread thread([f = std::move(f), waiter]() {
94 // RAII so remove() runs even if f() throws. In practice an uncaught exception out of a
95 // std::thread function calls std::terminate -> abort, which kills the process before the
96 // atexit waiter could block, but the guard documents the invariant and covers exotic
97 // terminate-handler setups.
98 RemoveGuard guard{waiter};
99 f();
100 });
101 thread.detach();
102 }
103
104 private:
105 // DO NOT REMOVE WITHOUT READING THE FOLLOWING:
106 //
107 // schedule() above spawns a detached std::thread per call. On Windows shared-lib builds, if the
108 // process exits while a detached thread is mid-execution inside dispenso's code, the OS unmaps
109 // dispenso.dll's pages during DLL_PROCESS_DETACH while the thread is still executing them →
110 // EXCEPTION_ACCESS_VIOLATION (0xC0000005). On any platform, the detached thread also has a
111 // non-trivial window between Future::status notify(kReady) and the final
112 // decRefCountMaybeDestroy + thread-local-storage teardown, which extends past the user-visible
113 // future.get() return.
114 //
115 // ThreadWaiter blocks the atexit handler until every detached thread has called remove(), so
116 // no thread is mid-execution when _cexit / DLL unload runs. The SmallBufferAllocator
117 // controlled-leak fix (small_buffer_allocator.cpp) addresses a different hazard (returning
118 // small buffers to a destroyed central store) and is NOT a substitute for this. Both
119 // mitigations are needed.
120 //
121 // The relevant tests are future_test_sans_exceptions and future_shared_test, the
122 // Future.AsyncNotAsyncSpecifyNewThread / NewThreadInvoker / AsyncSpecifyNewThread cases.
123 //
124 // The ThreadWaiter object itself is intentionally leaked (controlled-leak singleton in
125 // schedulable.cpp). Deleting it on shutdown would create a UAF window for any post-atexit
126 // schedule() call (external thread, static destructor) that hits a freed waiter.
127 struct ThreadWaiter {
128 int count_ = 0;
129 std::mutex mtx_;
130 std::condition_variable cond_;
131
132 void add() {
133 std::lock_guard<std::mutex> lk(mtx_);
134 ++count_;
135 }
136
137 void remove() {
138 std::lock_guard<std::mutex> lk(mtx_);
139 assert(count_ > 0 && "remove() called without matching add()");
140 if (--count_ == 0) {
141 cond_.notify_one();
142 }
143 }
144
145 void wait() {
146 std::unique_lock<std::mutex> lk(mtx_);
147 cond_.wait(lk, [this]() { return count_ == 0; });
148 }
149 };
150
151 struct RemoveGuard {
152 ThreadWaiter* w;
153 ~RemoveGuard() {
154 w->remove();
155 }
156 };
157
158 DISPENSO_DLL_ACCESS static ThreadWaiter* getWaiter();
159};
160
161constexpr NewThreadInvoker kNewThreadInvoker;
162
163} // namespace dispenso
void schedule(F &&f) const
Definition schedulable.h:44
void schedule(F &&f) const
Definition schedulable.h:78