master
h 361 lines 11.2 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 WslCoreMessageQueue.h
8
9 Abstract:
10
11 This file contains a queuing implementation, guaranteeing running function objects
12 with guaranteed serialization in a threadpool thread
13
14 --*/
15
16 #pragma once
17 #include <deque>
18 #include <functional>
19 #include <memory>
20 #include <variant>
21 #include <windows.h>
22 #include <wil/resource.h>
23
24 namespace wsl::core {
25 // forward-declare classes that can instantiate a WslThreadPoolWaitableResult object
26 class WslCoreMessageQueue;
27
28 class WslBaseThreadPoolWaitableResult
29 {
30 public:
31 virtual ~WslBaseThreadPoolWaitableResult() noexcept = default;
32
33 private:
34 // limit who can run() and abort()
35 friend class WslCoreMessageQueue;
36
37 virtual void run() noexcept = 0;
38 virtual void abort() noexcept = 0;
39 };
40
41 template <typename TReturn>
42 class WslThreadPoolWaitableResult : public WslBaseThreadPoolWaitableResult
43 {
44 public:
45 // throws a wil exception on failure
46 template <typename FunctorType>
47 explicit WslThreadPoolWaitableResult(FunctorType&& functor) : m_function(std::forward<FunctorType>(functor))
48 {
49 }
50
51 ~WslThreadPoolWaitableResult() noexcept override = default;
52
53 // returns ERROR_SUCCESS if the callback ran to completion
54 // returns ERROR_TIMEOUT if this wait timed out
55 // - this can be called multiple times if needing to probe
56 // any other error code resulted from attempting to run the callback
57 // - meaning it did *not* run to completion
58 DWORD wait(DWORD timeout) const noexcept
59 {
60 if (!m_completionSignal.wait(timeout))
61 {
62 // not setting m_internalError to timeout
63 // since the caller is allowed to try to wait() again later
64 return ERROR_TIMEOUT;
65 }
66 const auto lock = m_lock.lock_shared();
67 return m_internalError;
68 }
69
70 // waitable event handle, signaled when the callback has run to completion (or failed)
71 HANDLE notification_event() const noexcept
72 {
73 return m_completionSignal.get();
74 }
75
76 const TReturn& read_result() const noexcept
77 {
78 return result;
79 }
80
81 // move the result out of the object for move-only types
82 TReturn move_result() noexcept
83 {
84 TReturn move_out(std::move(result));
85 return move_out;
86 }
87
88 // non-copyable
89 WslThreadPoolWaitableResult(const WslThreadPoolWaitableResult&) = delete;
90 WslThreadPoolWaitableResult& operator=(const WslThreadPoolWaitableResult&) = delete;
91
92 private:
93 void run() noexcept override
94 {
95 // we are now running in the TP callback
96 {
97 const auto lock = m_lock.lock_exclusive();
98 if (m_runStatus != RunStatus::NotYetRun)
99 {
100 // return early - the caller has already canceled this
101 return;
102 }
103 m_runStatus = RunStatus::Running;
104 }
105
106 DWORD error = NO_ERROR;
107 try
108 {
109 result = std::move(m_function());
110 }
111 catch (...)
112 {
113 const HRESULT hr = wil::ResultFromCaughtException();
114 // HRESULT_TO_WIN32
115 error = (HRESULT_FACILITY(hr) == FACILITY_WIN32) ? HRESULT_CODE(hr) : hr;
116 }
117
118 const auto lock = m_lock.lock_exclusive();
119 WI_ASSERT(m_runStatus == RunStatus::Running);
120 m_runStatus = RunStatus::RanToCompletion;
121 m_internalError = error;
122 m_completionSignal.SetEvent();
123 }
124
125 void abort() noexcept override
126 {
127 const auto lock = m_lock.lock_exclusive();
128 // only override the error if we know we haven't started running their functor
129 if (m_runStatus == RunStatus::NotYetRun)
130 {
131 m_runStatus = RunStatus::Canceled;
132 m_internalError = ERROR_CANCELLED;
133 m_completionSignal.SetEvent();
134 }
135 }
136
137 std::function<TReturn(void)> m_function;
138 // a notification event
139 wil::unique_event m_completionSignal{wil::EventOptions::ManualReset};
140 mutable wil::srwlock m_lock;
141 TReturn result{};
142 DWORD m_internalError = NO_ERROR;
143
144 enum class RunStatus
145 {
146 NotYetRun,
147 Running,
148 RanToCompletion,
149 Canceled
150 } m_runStatus{RunStatus::NotYetRun};
151 };
152
153 class WslCoreMessageQueue
154 {
155 public:
156 WslCoreMessageQueue() : m_tpEnvironment(0, 1)
157 {
158 // create a single-threaded threadpool
159 m_tpHandle = m_tpEnvironment.create_tp(WorkCallback, this);
160 }
161
162 template <typename TReturn, typename FunctorType>
163 std::shared_ptr<WslThreadPoolWaitableResult<TReturn>> submit_with_results(FunctorType&& functor) noexcept
164 try
165 {
166 FAIL_FAST_IF(m_tpHandle.get() == nullptr);
167
168 const auto new_result = std::make_shared<WslThreadPoolWaitableResult<TReturn>>(std::forward<FunctorType>(functor));
169 // scope to the queue lock
170 {
171 const auto queueLock = m_lock.lock_exclusive();
172 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_CANCELLED), m_isCanceled);
173 m_workItems.emplace_back(new_result);
174
175 // always maintain a 1:1 ratio for calls to submit_with_results() and ::SubmitThreadpoolWork
176 SubmitThreadpoolWork(m_tpHandle.get());
177 }
178
179 return new_result;
180 }
181 catch (...)
182 {
183 LOG_CAUGHT_EXCEPTION();
184 return nullptr;
185 }
186
187 template <typename FunctorType>
188 bool submit(FunctorType&& functor) noexcept
189 try
190 {
191 FAIL_FAST_IF(m_tpHandle.get() == nullptr);
192
193 // scope to the queue lock
194 {
195 const auto queueLock = m_lock.lock_exclusive();
196 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_CANCELLED), m_isCanceled);
197 m_workItems.emplace_back(std::forward<SimpleFunction_t>(functor));
198
199 // always maintain a 1:1 ratio for calls to submit() and ::SubmitThreadpoolWork
200 SubmitThreadpoolWork(m_tpHandle.get());
201 }
202
203 return true;
204 }
205 catch (...)
206 {
207 LOG_CAUGHT_EXCEPTION();
208 return false;
209 }
210
211 // functors must return type HRESULT
212 template <typename FunctorType>
213 HRESULT submit_and_wait(FunctorType&& functor) noexcept
214 try
215 {
216 HRESULT hr = HRESULT_FROM_WIN32(ERROR_OUTOFMEMORY);
217 if (const auto waitableResult = submit_with_results<HRESULT>(std::forward<FunctorType>(functor)))
218 {
219 hr = HRESULT_FROM_WIN32(waitableResult->wait(INFINITE));
220 if (SUCCEEDED(hr))
221 {
222 hr = waitableResult->read_result();
223 }
224 }
225 return hr;
226 }
227 CATCH_RETURN()
228
229 // cancels anything queued to the TP - this WslCoreMessageQueue instance can no longer be used
230 void cancel() noexcept
231 try
232 {
233 if (m_tpHandle)
234 {
235 // immediately release anyone waiting for these workitems not yet run
236 {
237 const auto queueLock = m_lock.lock_exclusive();
238 m_isCanceled = true;
239
240 for (const auto& work : m_workItems)
241 {
242 // signal that these are canceled before we shutdown the TP which they could be scheduled
243 if (const auto* pWaitableWorkitem = std::get_if<WaitableFunction_t>(&work))
244 {
245 (*pWaitableWorkitem)->abort();
246 }
247 }
248
249 m_workItems.clear();
250 }
251
252 // force the m_tpHandle to wait and close the TP
253 m_tpHandle.reset();
254 m_tpEnvironment.reset();
255 }
256 }
257 CATCH_LOG()
258
259 bool isRunningInQueue() const noexcept
260 {
261 const auto currentThreadId = GetThreadId(GetCurrentThread());
262 return currentThreadId == static_cast<DWORD>(InterlockedCompareExchange64(&m_threadpoolThreadId, 0ll, 0ll));
263 }
264
265 ~WslCoreMessageQueue() noexcept
266 {
267 cancel();
268 }
269
270 WslCoreMessageQueue(const WslCoreMessageQueue&) = delete;
271 WslCoreMessageQueue& operator=(const WslCoreMessageQueue&) = delete;
272 WslCoreMessageQueue(WslCoreMessageQueue&&) = delete;
273 WslCoreMessageQueue& operator=(WslCoreMessageQueue&&) = delete;
274
275 private:
276 struct TPEnvironment
277 {
278 using unique_tp_env = wil::unique_struct<TP_CALLBACK_ENVIRON, decltype(&DestroyThreadpoolEnvironment), DestroyThreadpoolEnvironment>;
279 unique_tp_env m_tpEnvironment;
280
281 using unique_tp_pool = wil::unique_any<PTP_POOL, decltype(&CloseThreadpool), CloseThreadpool>;
282 unique_tp_pool m_threadPool;
283
284 TPEnvironment(DWORD countMinThread, DWORD countMaxThread)
285 {
286 InitializeThreadpoolEnvironment(&m_tpEnvironment);
287
288 m_threadPool.reset(CreateThreadpool(nullptr));
289 THROW_LAST_ERROR_IF_NULL(m_threadPool.get());
290
291 // Set min and max thread counts for custom thread pool
292 THROW_LAST_ERROR_IF(!::SetThreadpoolThreadMinimum(m_threadPool.get(), countMinThread));
293 SetThreadpoolThreadMaximum(m_threadPool.get(), countMaxThread);
294 SetThreadpoolCallbackPool(&m_tpEnvironment, m_threadPool.get());
295 }
296
297 wil::unique_threadpool_work create_tp(PTP_WORK_CALLBACK callback, void* pv)
298 {
299 wil::unique_threadpool_work newThreadpool(CreateThreadpoolWork(callback, pv, (m_threadPool) ? &m_tpEnvironment : nullptr));
300 THROW_LAST_ERROR_IF_NULL(newThreadpool.get());
301 return newThreadpool;
302 }
303
304 void reset()
305 {
306 m_threadPool.reset();
307 m_tpEnvironment.reset();
308 }
309 };
310
311 using SimpleFunction_t = std::function<void()>;
312 using WaitableFunction_t = std::shared_ptr<WslBaseThreadPoolWaitableResult>;
313 using FunctionVariant_t = std::variant<SimpleFunction_t, WaitableFunction_t>;
314
315 // the lock must be destroyed *after* the TP object (thus must be declared first)
316 // since the lock is used in the TP callback
317 // the lock is mutable to allow us to acquire the lock in const methods
318 mutable wil::srwlock m_lock;
319 TPEnvironment m_tpEnvironment;
320 wil::unique_threadpool_work m_tpHandle;
321 std::deque<FunctionVariant_t> m_workItems;
322 mutable LONG64 m_threadpoolThreadId{0}; // useful for callers to assert they are running within the queue
323 bool m_isCanceled{false};
324
325 static void CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE, void* Context, PTP_WORK) noexcept
326 try
327 {
328 auto* pThis = static_cast<WslCoreMessageQueue*>(Context);
329
330 FunctionVariant_t work;
331 {
332 const auto queueLock = pThis->m_lock.lock_exclusive();
333
334 if (pThis->m_workItems.empty())
335 {
336 // pThis object is being destroyed and the queue was cleared
337 return;
338 }
339
340 std::swap(work, pThis->m_workItems.front());
341 pThis->m_workItems.pop_front();
342
343 InterlockedExchange64(&pThis->m_threadpoolThreadId, GetThreadId(GetCurrentThread()));
344 }
345
346 // run the tasks outside the WslCoreMessageQueue lock
347 const auto resetThreadIdOnExit = wil::scope_exit([pThis] { InterlockedExchange64(&pThis->m_threadpoolThreadId, 0ll); });
348 if (work.index() == 0)
349 {
350 const auto& workItem = std::get<SimpleFunction_t>(work);
351 workItem();
352 }
353 else
354 {
355 const auto& waitableWorkItem = std::get<WaitableFunction_t>(work);
356 waitableWorkItem->run();
357 }
358 }
359 CATCH_LOG()
360 };
361 } // namespace wsl::core