Skip to content

Commit

Permalink
Add UIThreadExecutor class
Browse files Browse the repository at this point in the history
This is a custom concurrencpp executor and will be used to execute tasks
on the UI thread.
  • Loading branch information
derceg committed Oct 1, 2024
1 parent 4519b82 commit 91ad1b5
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Explorer++/Explorer++/Explorer++.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,7 @@
<ClCompile Include="GlobalHistoryMenu.cpp" />
<ClCompile Include="LocationVisitInfo.cpp" />
<ClCompile Include="MainMenuSubMenuView.cpp" />
<ClCompile Include="UIThreadExecutor.cpp" />
<ClCompile Include="MenuBase.cpp" />
<ClCompile Include="MenuView.cpp" />
<ClCompile Include="PasteSymLinksClient.cpp" />
Expand Down Expand Up @@ -1293,6 +1294,7 @@
<ClInclude Include="GlobalHistoryMenu.h" />
<ClInclude Include="LocationVisitInfo.h" />
<ClInclude Include="MainMenuSubMenuView.h" />
<ClInclude Include="UIThreadExecutor.h" />
<ClInclude Include="MenuBase.h" />
<ClInclude Include="MenuView.h" />
<ClInclude Include="PasteSymLinksClient.h" />
Expand Down
9 changes: 9 additions & 0 deletions Explorer++/Explorer++/Explorer++.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,9 @@
<ClCompile Include="ShellIconModel.cpp">
<Filter>Core</Filter>
</ClCompile>
<ClCompile Include="UIThreadExecutor.cpp">
<Filter>Executors</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Bookmarks\BookmarkHelper.h">
Expand Down Expand Up @@ -1443,6 +1446,9 @@
<ClInclude Include="ShellIconLoader.h">
<Filter>Core</Filter>
</ClInclude>
<ClInclude Include="UIThreadExecutor.h">
<Filter>Executors</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Explorer++.rc">
Expand Down Expand Up @@ -1605,6 +1611,9 @@
<Filter Include="Core\Resource Loading">
<UniqueIdentifier>{e7f03946-8121-4ea7-a8ea-894d7fc97109}</UniqueIdentifier>
</Filter>
<Filter Include="Executors">
<UniqueIdentifier>{931447cd-9040-4f5c-9726-28d9d840b1e5}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<Manifest Include="Explorer++.exe.manifest">
Expand Down
129 changes: 129 additions & 0 deletions Explorer++/Explorer++/UIThreadExecutor.cpp
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);
}
43 changes: 43 additions & 0 deletions Explorer++/Explorer++/UIThreadExecutor.h
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;
};
1 change: 1 addition & 0 deletions Explorer++/TestExplorer++/TestExplorer++.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@
<ClCompile Include="GlobalHistoryMenuTest.cpp" />
<ClCompile Include="HelperTest.cpp" />
<ClCompile Include="HistoryServiceTest.cpp" />
<ClCompile Include="UIThreadExecutorTest.cpp" />
<ClCompile Include="MenuHelperTest.cpp" />
<ClCompile Include="PasteSymLinksServerClientTest.cpp" />
<ClCompile Include="PopupMenuViewTest.cpp" />
Expand Down
6 changes: 6 additions & 0 deletions Explorer++/TestExplorer++/TestExplorer++.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@
<ClCompile Include="ShellIconLoaderFake.cpp">
<Filter>Core</Filter>
</ClCompile>
<ClCompile Include="UIThreadExecutorTest.cpp">
<Filter>Executors</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<Filter Include="Bookmarks">
Expand Down Expand Up @@ -309,6 +312,9 @@
<Filter Include="Core\Accelerators">
<UniqueIdentifier>{8573b1f7-785d-4e8a-8646-136d0fd51cbd}</UniqueIdentifier>
</Filter>
<Filter Include="Executors">
<UniqueIdentifier>{de6aab16-37e4-436f-817b-780f1e35ee98}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="TestExplorer++.rc">
Expand Down
96 changes: 96 additions & 0 deletions Explorer++/TestExplorer++/UIThreadExecutorTest.cpp
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);
}

0 comments on commit 91ad1b5

Please sign in to comment.