Skip to content

Commit 91ad1b5

Browse files
committed
Add UIThreadExecutor class
This is a custom concurrencpp executor and will be used to execute tasks on the UI thread.
1 parent 4519b82 commit 91ad1b5

File tree

7 files changed

+286
-0
lines changed

7 files changed

+286
-0
lines changed

Explorer++/Explorer++/Explorer++.vcxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,7 @@
10251025
<ClCompile Include="GlobalHistoryMenu.cpp" />
10261026
<ClCompile Include="LocationVisitInfo.cpp" />
10271027
<ClCompile Include="MainMenuSubMenuView.cpp" />
1028+
<ClCompile Include="UIThreadExecutor.cpp" />
10281029
<ClCompile Include="MenuBase.cpp" />
10291030
<ClCompile Include="MenuView.cpp" />
10301031
<ClCompile Include="PasteSymLinksClient.cpp" />
@@ -1293,6 +1294,7 @@
12931294
<ClInclude Include="GlobalHistoryMenu.h" />
12941295
<ClInclude Include="LocationVisitInfo.h" />
12951296
<ClInclude Include="MainMenuSubMenuView.h" />
1297+
<ClInclude Include="UIThreadExecutor.h" />
12961298
<ClInclude Include="MenuBase.h" />
12971299
<ClInclude Include="MenuView.h" />
12981300
<ClInclude Include="PasteSymLinksClient.h" />

Explorer++/Explorer++/Explorer++.vcxproj.filters

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,9 @@
700700
<ClCompile Include="ShellIconModel.cpp">
701701
<Filter>Core</Filter>
702702
</ClCompile>
703+
<ClCompile Include="UIThreadExecutor.cpp">
704+
<Filter>Executors</Filter>
705+
</ClCompile>
703706
</ItemGroup>
704707
<ItemGroup>
705708
<ClInclude Include="Bookmarks\BookmarkHelper.h">
@@ -1443,6 +1446,9 @@
14431446
<ClInclude Include="ShellIconLoader.h">
14441447
<Filter>Core</Filter>
14451448
</ClInclude>
1449+
<ClInclude Include="UIThreadExecutor.h">
1450+
<Filter>Executors</Filter>
1451+
</ClInclude>
14461452
</ItemGroup>
14471453
<ItemGroup>
14481454
<ResourceCompile Include="Explorer++.rc">
@@ -1605,6 +1611,9 @@
16051611
<Filter Include="Core\Resource Loading">
16061612
<UniqueIdentifier>{e7f03946-8121-4ea7-a8ea-894d7fc97109}</UniqueIdentifier>
16071613
</Filter>
1614+
<Filter Include="Executors">
1615+
<UniqueIdentifier>{931447cd-9040-4f5c-9726-28d9d840b1e5}</UniqueIdentifier>
1616+
</Filter>
16081617
</ItemGroup>
16091618
<ItemGroup>
16101619
<Manifest Include="Explorer++.exe.manifest">
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright (C) Explorer++ Project
2+
// SPDX-License-Identifier: GPL-3.0-only
3+
// See LICENSE in the top level directory
4+
5+
#include "stdafx.h"
6+
#include "UIThreadExecutor.h"
7+
#include <CommCtrl.h>
8+
9+
UIThreadExecutor::UIThreadExecutor() :
10+
concurrencpp::derivable_executor<UIThreadExecutor>("UIThreadExecutor"),
11+
m_hwnd(CreateMessageOnlyWindow())
12+
{
13+
m_windowSubclasses.push_back(std::make_unique<WindowSubclassWrapper>(m_hwnd,
14+
std::bind_front(&UIThreadExecutor::WndProc, this)));
15+
}
16+
17+
void UIThreadExecutor::enqueue(concurrencpp::task task)
18+
{
19+
std::span<concurrencpp::task> taskSpan(&task, 1);
20+
enqueue(taskSpan);
21+
}
22+
23+
void UIThreadExecutor::enqueue(std::span<concurrencpp::task> tasks)
24+
{
25+
if (m_shutdownRequested)
26+
{
27+
throw concurrencpp::errors::runtime_shutdown("UI thread executor already shut down");
28+
}
29+
30+
std::unique_lock<std::mutex> lock(m_mutex);
31+
32+
for (auto &task : tasks)
33+
{
34+
m_queue.emplace(std::move(task));
35+
}
36+
37+
lock.unlock();
38+
39+
PostMessage(m_hwnd, WM_USER_TASK_QUEUED, 0, 0);
40+
}
41+
42+
int UIThreadExecutor::max_concurrency_level() const noexcept
43+
{
44+
return 1;
45+
}
46+
47+
bool UIThreadExecutor::shutdown_requested() const noexcept
48+
{
49+
return m_shutdownRequested;
50+
}
51+
52+
void UIThreadExecutor::shutdown() noexcept
53+
{
54+
if (m_shutdownRequested)
55+
{
56+
return;
57+
}
58+
59+
m_shutdownRequested = true;
60+
61+
std::unique_lock<std::mutex> lock(m_mutex);
62+
m_queue = {};
63+
lock.unlock();
64+
65+
auto res = SendMessage(m_hwnd, WM_USER_DESTROY_WINDOW, 0, 0);
66+
DCHECK_EQ(res, 1);
67+
}
68+
69+
HWND UIThreadExecutor::CreateMessageOnlyWindow()
70+
{
71+
WNDCLASS windowClass = {};
72+
windowClass.lpfnWndProc = DefWindowProc;
73+
windowClass.hCursor = LoadCursor(nullptr, IDC_ARROW);
74+
windowClass.lpszClassName = MESSAGE_CLASS_NAME;
75+
windowClass.hInstance = GetModuleHandle(nullptr);
76+
windowClass.style = CS_HREDRAW | CS_VREDRAW;
77+
RegisterClass(&windowClass);
78+
79+
HWND hwnd = CreateWindow(MESSAGE_CLASS_NAME, MESSAGE_CLASS_NAME, WS_DISABLED, CW_USEDEFAULT,
80+
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, HWND_MESSAGE, nullptr,
81+
GetModuleHandle(nullptr), nullptr);
82+
CHECK(hwnd);
83+
84+
return hwnd;
85+
}
86+
87+
LRESULT UIThreadExecutor::WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
88+
{
89+
switch (msg)
90+
{
91+
case WM_USER_TASK_QUEUED:
92+
OnTaskQueued();
93+
return 1;
94+
95+
case WM_USER_DESTROY_WINDOW:
96+
OnDestroyWindow();
97+
return 1;
98+
}
99+
100+
return DefSubclassProc(hwnd, msg, wParam, lParam);
101+
}
102+
103+
void UIThreadExecutor::OnTaskQueued()
104+
{
105+
std::queue<concurrencpp::task> localQueue;
106+
107+
std::unique_lock<std::mutex> lock(m_mutex);
108+
std::swap(localQueue, m_queue);
109+
lock.unlock();
110+
111+
while (!localQueue.empty())
112+
{
113+
if (m_shutdownRequested)
114+
{
115+
return;
116+
}
117+
118+
auto task = std::move(localQueue.front());
119+
localQueue.pop();
120+
121+
task();
122+
}
123+
}
124+
125+
void UIThreadExecutor::OnDestroyWindow()
126+
{
127+
BOOL res = DestroyWindow(m_hwnd);
128+
DCHECK(res);
129+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (C) Explorer++ Project
2+
// SPDX-License-Identifier: GPL-3.0-only
3+
// See LICENSE in the top level directory
4+
5+
#pragma once
6+
7+
#include "../Helper/WindowSubclassWrapper.h"
8+
#include <concurrencpp/concurrencpp.h>
9+
#include <atomic>
10+
#include <memory>
11+
#include <mutex>
12+
#include <queue>
13+
#include <vector>
14+
15+
class UIThreadExecutor : public concurrencpp::derivable_executor<UIThreadExecutor>
16+
{
17+
public:
18+
UIThreadExecutor();
19+
20+
void enqueue(concurrencpp::task task) override;
21+
void enqueue(std::span<concurrencpp::task> tasks) override;
22+
int max_concurrency_level() const noexcept override;
23+
bool shutdown_requested() const noexcept override;
24+
void shutdown() noexcept override;
25+
26+
private:
27+
static constexpr UINT WM_USER_TASK_QUEUED = WM_USER;
28+
static constexpr UINT WM_USER_DESTROY_WINDOW = WM_USER + 1;
29+
30+
static constexpr WCHAR MESSAGE_CLASS_NAME[] = L"MessageClass";
31+
32+
static HWND CreateMessageOnlyWindow();
33+
34+
LRESULT WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
35+
void OnTaskQueued();
36+
void OnDestroyWindow();
37+
38+
const HWND m_hwnd;
39+
std::vector<std::unique_ptr<WindowSubclassWrapper>> m_windowSubclasses;
40+
std::mutex m_mutex;
41+
std::queue<concurrencpp::task> m_queue;
42+
std::atomic_bool m_shutdownRequested = false;
43+
};

Explorer++/TestExplorer++/TestExplorer++.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@
223223
<ClCompile Include="GlobalHistoryMenuTest.cpp" />
224224
<ClCompile Include="HelperTest.cpp" />
225225
<ClCompile Include="HistoryServiceTest.cpp" />
226+
<ClCompile Include="UIThreadExecutorTest.cpp" />
226227
<ClCompile Include="MenuHelperTest.cpp" />
227228
<ClCompile Include="PasteSymLinksServerClientTest.cpp" />
228229
<ClCompile Include="PopupMenuViewTest.cpp" />

Explorer++/TestExplorer++/TestExplorer++.vcxproj.filters

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@
223223
<ClCompile Include="ShellIconLoaderFake.cpp">
224224
<Filter>Core</Filter>
225225
</ClCompile>
226+
<ClCompile Include="UIThreadExecutorTest.cpp">
227+
<Filter>Executors</Filter>
228+
</ClCompile>
226229
</ItemGroup>
227230
<ItemGroup>
228231
<Filter Include="Bookmarks">
@@ -309,6 +312,9 @@
309312
<Filter Include="Core\Accelerators">
310313
<UniqueIdentifier>{8573b1f7-785d-4e8a-8646-136d0fd51cbd}</UniqueIdentifier>
311314
</Filter>
315+
<Filter Include="Executors">
316+
<UniqueIdentifier>{de6aab16-37e4-436f-817b-780f1e35ee98}</UniqueIdentifier>
317+
</Filter>
312318
</ItemGroup>
313319
<ItemGroup>
314320
<ResourceCompile Include="TestExplorer++.rc">
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright (C) Explorer++ Project
2+
// SPDX-License-Identifier: GPL-3.0-only
3+
// See LICENSE in the top level directory
4+
5+
#include "pch.h"
6+
#include "UIThreadExecutor.h"
7+
#include <gtest/gtest.h>
8+
9+
using namespace testing;
10+
11+
class UIThreadExecutorTest : public Test
12+
{
13+
protected:
14+
~UIThreadExecutorTest()
15+
{
16+
// A test may call this method, but that's not an issue, since it's explicitly safe to call
17+
// the method multiple times.
18+
m_executor.shutdown();
19+
}
20+
21+
void PumpMessageLoopUntilIdle()
22+
{
23+
MSG msg;
24+
25+
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
26+
{
27+
TranslateMessage(&msg);
28+
DispatchMessage(&msg);
29+
}
30+
}
31+
32+
UIThreadExecutor m_executor;
33+
};
34+
35+
TEST_F(UIThreadExecutorTest, Submit)
36+
{
37+
MockFunction<void()> task1;
38+
m_executor.submit(task1.AsStdFunction());
39+
EXPECT_CALL(task1, Call());
40+
41+
MockFunction<void()> task2;
42+
m_executor.submit(task2.AsStdFunction());
43+
EXPECT_CALL(task2, Call());
44+
45+
PumpMessageLoopUntilIdle();
46+
}
47+
48+
TEST_F(UIThreadExecutorTest, BulkSubmit)
49+
{
50+
std::vector<MockFunction<void()>> tasks(4);
51+
std::vector<std::function<void()>> tasksAsFunctions;
52+
53+
for (auto &task : tasks)
54+
{
55+
EXPECT_CALL(task, Call());
56+
57+
tasksAsFunctions.push_back(task.AsStdFunction());
58+
}
59+
60+
m_executor.bulk_submit<std::function<void()>>(tasksAsFunctions);
61+
62+
PumpMessageLoopUntilIdle();
63+
}
64+
65+
TEST_F(UIThreadExecutorTest, ShutdownRequested)
66+
{
67+
EXPECT_FALSE(m_executor.shutdown_requested());
68+
69+
m_executor.shutdown();
70+
EXPECT_TRUE(m_executor.shutdown_requested());
71+
}
72+
73+
TEST_F(UIThreadExecutorTest, ShutdownDuringTaskLoop)
74+
{
75+
// If shutdown() is called while a task is being run, any remaining tasks should be skipped.
76+
MockFunction<void()> task1;
77+
m_executor.submit(task1.AsStdFunction());
78+
EXPECT_CALL(task1, Call()).WillOnce([this] { m_executor.shutdown(); });
79+
80+
MockFunction<void()> task2;
81+
m_executor.submit(task2.AsStdFunction());
82+
EXPECT_CALL(task2, Call()).Times(0);
83+
84+
PumpMessageLoopUntilIdle();
85+
}
86+
87+
TEST_F(UIThreadExecutorTest, EnqueueAfterShutdown)
88+
{
89+
m_executor.shutdown();
90+
91+
EXPECT_THROW(m_executor.enqueue(concurrencpp::task()), concurrencpp::errors::runtime_shutdown);
92+
93+
concurrencpp::task tasks[4];
94+
std::span<concurrencpp::task> tasksSpan = tasks;
95+
EXPECT_THROW(m_executor.enqueue(tasksSpan), concurrencpp::errors::runtime_shutdown);
96+
}

0 commit comments

Comments
 (0)