-
-
Notifications
You must be signed in to change notification settings - Fork 364
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This is a custom concurrencpp executor and will be used to execute tasks on the UI thread.
- Loading branch information
Showing
7 changed files
with
286 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
// Copyright (C) Explorer++ Project | ||
// SPDX-License-Identifier: GPL-3.0-only | ||
// See LICENSE in the top level directory | ||
|
||
#include "stdafx.h" | ||
#include "UIThreadExecutor.h" | ||
#include <CommCtrl.h> | ||
|
||
UIThreadExecutor::UIThreadExecutor() : | ||
concurrencpp::derivable_executor<UIThreadExecutor>("UIThreadExecutor"), | ||
m_hwnd(CreateMessageOnlyWindow()) | ||
{ | ||
m_windowSubclasses.push_back(std::make_unique<WindowSubclassWrapper>(m_hwnd, | ||
std::bind_front(&UIThreadExecutor::WndProc, this))); | ||
} | ||
|
||
void UIThreadExecutor::enqueue(concurrencpp::task task) | ||
{ | ||
std::span<concurrencpp::task> taskSpan(&task, 1); | ||
enqueue(taskSpan); | ||
} | ||
|
||
void UIThreadExecutor::enqueue(std::span<concurrencpp::task> tasks) | ||
{ | ||
if (m_shutdownRequested) | ||
{ | ||
throw concurrencpp::errors::runtime_shutdown("UI thread executor already shut down"); | ||
} | ||
|
||
std::unique_lock<std::mutex> lock(m_mutex); | ||
|
||
for (auto &task : tasks) | ||
{ | ||
m_queue.emplace(std::move(task)); | ||
} | ||
|
||
lock.unlock(); | ||
|
||
PostMessage(m_hwnd, WM_USER_TASK_QUEUED, 0, 0); | ||
} | ||
|
||
int UIThreadExecutor::max_concurrency_level() const noexcept | ||
{ | ||
return 1; | ||
} | ||
|
||
bool UIThreadExecutor::shutdown_requested() const noexcept | ||
{ | ||
return m_shutdownRequested; | ||
} | ||
|
||
void UIThreadExecutor::shutdown() noexcept | ||
{ | ||
if (m_shutdownRequested) | ||
{ | ||
return; | ||
} | ||
|
||
m_shutdownRequested = true; | ||
|
||
std::unique_lock<std::mutex> lock(m_mutex); | ||
m_queue = {}; | ||
lock.unlock(); | ||
|
||
auto res = SendMessage(m_hwnd, WM_USER_DESTROY_WINDOW, 0, 0); | ||
DCHECK_EQ(res, 1); | ||
} | ||
|
||
HWND UIThreadExecutor::CreateMessageOnlyWindow() | ||
{ | ||
WNDCLASS windowClass = {}; | ||
windowClass.lpfnWndProc = DefWindowProc; | ||
windowClass.hCursor = LoadCursor(nullptr, IDC_ARROW); | ||
windowClass.lpszClassName = MESSAGE_CLASS_NAME; | ||
windowClass.hInstance = GetModuleHandle(nullptr); | ||
windowClass.style = CS_HREDRAW | CS_VREDRAW; | ||
RegisterClass(&windowClass); | ||
|
||
HWND hwnd = CreateWindow(MESSAGE_CLASS_NAME, MESSAGE_CLASS_NAME, WS_DISABLED, CW_USEDEFAULT, | ||
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, HWND_MESSAGE, nullptr, | ||
GetModuleHandle(nullptr), nullptr); | ||
CHECK(hwnd); | ||
|
||
return hwnd; | ||
} | ||
|
||
LRESULT UIThreadExecutor::WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) | ||
{ | ||
switch (msg) | ||
{ | ||
case WM_USER_TASK_QUEUED: | ||
OnTaskQueued(); | ||
return 1; | ||
|
||
case WM_USER_DESTROY_WINDOW: | ||
OnDestroyWindow(); | ||
return 1; | ||
} | ||
|
||
return DefSubclassProc(hwnd, msg, wParam, lParam); | ||
} | ||
|
||
void UIThreadExecutor::OnTaskQueued() | ||
{ | ||
std::queue<concurrencpp::task> localQueue; | ||
|
||
std::unique_lock<std::mutex> lock(m_mutex); | ||
std::swap(localQueue, m_queue); | ||
lock.unlock(); | ||
|
||
while (!localQueue.empty()) | ||
{ | ||
if (m_shutdownRequested) | ||
{ | ||
return; | ||
} | ||
|
||
auto task = std::move(localQueue.front()); | ||
localQueue.pop(); | ||
|
||
task(); | ||
} | ||
} | ||
|
||
void UIThreadExecutor::OnDestroyWindow() | ||
{ | ||
BOOL res = DestroyWindow(m_hwnd); | ||
DCHECK(res); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
// Copyright (C) Explorer++ Project | ||
// SPDX-License-Identifier: GPL-3.0-only | ||
// See LICENSE in the top level directory | ||
|
||
#pragma once | ||
|
||
#include "../Helper/WindowSubclassWrapper.h" | ||
#include <concurrencpp/concurrencpp.h> | ||
#include <atomic> | ||
#include <memory> | ||
#include <mutex> | ||
#include <queue> | ||
#include <vector> | ||
|
||
class UIThreadExecutor : public concurrencpp::derivable_executor<UIThreadExecutor> | ||
{ | ||
public: | ||
UIThreadExecutor(); | ||
|
||
void enqueue(concurrencpp::task task) override; | ||
void enqueue(std::span<concurrencpp::task> tasks) override; | ||
int max_concurrency_level() const noexcept override; | ||
bool shutdown_requested() const noexcept override; | ||
void shutdown() noexcept override; | ||
|
||
private: | ||
static constexpr UINT WM_USER_TASK_QUEUED = WM_USER; | ||
static constexpr UINT WM_USER_DESTROY_WINDOW = WM_USER + 1; | ||
|
||
static constexpr WCHAR MESSAGE_CLASS_NAME[] = L"MessageClass"; | ||
|
||
static HWND CreateMessageOnlyWindow(); | ||
|
||
LRESULT WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); | ||
void OnTaskQueued(); | ||
void OnDestroyWindow(); | ||
|
||
const HWND m_hwnd; | ||
std::vector<std::unique_ptr<WindowSubclassWrapper>> m_windowSubclasses; | ||
std::mutex m_mutex; | ||
std::queue<concurrencpp::task> m_queue; | ||
std::atomic_bool m_shutdownRequested = false; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
// Copyright (C) Explorer++ Project | ||
// SPDX-License-Identifier: GPL-3.0-only | ||
// See LICENSE in the top level directory | ||
|
||
#include "pch.h" | ||
#include "UIThreadExecutor.h" | ||
#include <gtest/gtest.h> | ||
|
||
using namespace testing; | ||
|
||
class UIThreadExecutorTest : public Test | ||
{ | ||
protected: | ||
~UIThreadExecutorTest() | ||
{ | ||
// A test may call this method, but that's not an issue, since it's explicitly safe to call | ||
// the method multiple times. | ||
m_executor.shutdown(); | ||
} | ||
|
||
void PumpMessageLoopUntilIdle() | ||
{ | ||
MSG msg; | ||
|
||
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) | ||
{ | ||
TranslateMessage(&msg); | ||
DispatchMessage(&msg); | ||
} | ||
} | ||
|
||
UIThreadExecutor m_executor; | ||
}; | ||
|
||
TEST_F(UIThreadExecutorTest, Submit) | ||
{ | ||
MockFunction<void()> task1; | ||
m_executor.submit(task1.AsStdFunction()); | ||
EXPECT_CALL(task1, Call()); | ||
|
||
MockFunction<void()> task2; | ||
m_executor.submit(task2.AsStdFunction()); | ||
EXPECT_CALL(task2, Call()); | ||
|
||
PumpMessageLoopUntilIdle(); | ||
} | ||
|
||
TEST_F(UIThreadExecutorTest, BulkSubmit) | ||
{ | ||
std::vector<MockFunction<void()>> tasks(4); | ||
std::vector<std::function<void()>> tasksAsFunctions; | ||
|
||
for (auto &task : tasks) | ||
{ | ||
EXPECT_CALL(task, Call()); | ||
|
||
tasksAsFunctions.push_back(task.AsStdFunction()); | ||
} | ||
|
||
m_executor.bulk_submit<std::function<void()>>(tasksAsFunctions); | ||
|
||
PumpMessageLoopUntilIdle(); | ||
} | ||
|
||
TEST_F(UIThreadExecutorTest, ShutdownRequested) | ||
{ | ||
EXPECT_FALSE(m_executor.shutdown_requested()); | ||
|
||
m_executor.shutdown(); | ||
EXPECT_TRUE(m_executor.shutdown_requested()); | ||
} | ||
|
||
TEST_F(UIThreadExecutorTest, ShutdownDuringTaskLoop) | ||
{ | ||
// If shutdown() is called while a task is being run, any remaining tasks should be skipped. | ||
MockFunction<void()> task1; | ||
m_executor.submit(task1.AsStdFunction()); | ||
EXPECT_CALL(task1, Call()).WillOnce([this] { m_executor.shutdown(); }); | ||
|
||
MockFunction<void()> task2; | ||
m_executor.submit(task2.AsStdFunction()); | ||
EXPECT_CALL(task2, Call()).Times(0); | ||
|
||
PumpMessageLoopUntilIdle(); | ||
} | ||
|
||
TEST_F(UIThreadExecutorTest, EnqueueAfterShutdown) | ||
{ | ||
m_executor.shutdown(); | ||
|
||
EXPECT_THROW(m_executor.enqueue(concurrencpp::task()), concurrencpp::errors::runtime_shutdown); | ||
|
||
concurrencpp::task tasks[4]; | ||
std::span<concurrencpp::task> tasksSpan = tasks; | ||
EXPECT_THROW(m_executor.enqueue(tasksSpan), concurrencpp::errors::runtime_shutdown); | ||
} |