Skip to content

Commit 2143c99

Browse files
committed
1.6: Fix some threadpool flaws, including:
- Memory leaks - Infinite loop - Race condition - Server/resource shutdown
1 parent 9273b0d commit 2143c99

File tree

3 files changed

+95
-26
lines changed

3 files changed

+95
-26
lines changed

Server/mods/deathmatch/logic/CGame.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,9 @@ CGame::~CGame()
365365
// Stop async task scheduler
366366
SAFE_DELETE(m_pAsyncTaskScheduler);
367367

368+
// Shutdown thread pool before destroying resources to prevent tasks being enqueued during shutdown
369+
CThreadPool::getDefaultThreadPool().shutdown();
370+
368371
// Destroy our stuff
369372
SAFE_DELETE(m_pResourceManager);
370373

@@ -423,7 +426,6 @@ CGame::~CGame()
423426
SAFE_DELETE(m_pASE);
424427
SAFE_RELEASE(m_pHqComms);
425428
CSimControl::Shutdown();
426-
CThreadPool::getDefaultThreadPool().shutdown();
427429

428430
// Clear our global pointer
429431
g_pGame = NULL;

Server/mods/deathmatch/logic/CResource.cpp

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -582,11 +582,28 @@ bool CResource::GenerateChecksums()
582582

583583
for (auto& task : checksumTasks)
584584
{
585-
const auto& result = task.get();
586-
if (!result.empty())
585+
try
587586
{
588-
m_strFailureReason = result;
589-
CLogger::LogPrintf(result);
587+
const auto& result = task.get();
588+
if (!result.empty())
589+
{
590+
m_strFailureReason = result;
591+
CLogger::LogPrintf(result);
592+
bOk = false;
593+
}
594+
}
595+
catch (const std::future_error& e)
596+
{
597+
// Became invalid (e.g., during shutdown)
598+
m_strFailureReason = SString("Checksum task failed: %s", e.what());
599+
CLogger::LogPrintf(m_strFailureReason);
600+
bOk = false;
601+
}
602+
catch (const std::exception& e)
603+
{
604+
// Task threw
605+
m_strFailureReason = SString("Checksum error: %s", e.what());
606+
CLogger::LogPrintf(m_strFailureReason);
590607
bOk = false;
591608
}
592609
}

Shared/sdk/SharedUtil.ThreadPool.h

Lines changed: 71 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <future>
1515
#include <vector>
1616
#include <algorithm>
17+
#include <cassert>
1718
#include "SharedUtil.Misc.h"
1819

1920
namespace SharedUtil
@@ -39,8 +40,17 @@ namespace SharedUtil
3940
task = std::move(m_tasks.front());
4041
m_tasks.pop();
4142
}
42-
// Run the task
43-
task(false);
43+
// Run the task (catch exceptions to prevent thread death)
44+
try
45+
{
46+
task(false);
47+
}
48+
catch (...)
49+
{
50+
// Exception is automatically captured by std::packaged_task
51+
// and will be re-thrown when future.get() is called.
52+
// We must catch here to prevent the worker thread from terminating.
53+
}
4454
}
4555
});
4656
}
@@ -49,22 +59,33 @@ namespace SharedUtil
4959
template <typename Func, typename... Args>
5060
auto enqueue(Func&& f, Args&&... args)
5161
{
52-
using ReturnT = std::invoke_result_t<Func, Args...>;
53-
auto ff = std::bind(std::forward<Func>(f), std::forward<Args>(args)...);
54-
auto* task = new std::packaged_task<ReturnT()>(ff);
5562

56-
// Package the task in a wrapper with a common void result
57-
// plus a skip flag for destruction without running the task
58-
std::packaged_task<void(bool)> resultTask([task](bool skip) {
59-
if (!skip)
60-
(*task)();
61-
delete task;
62-
});
63+
#if __cplusplus < 201703L // C++17
64+
using ReturnT = typename std::result_of<Func(Args...)>::type;
65+
#else
66+
using ReturnT = std::invoke_result_t<Func, Args...>;
67+
#endif
68+
69+
auto ff = std::bind(std::forward<Func>(f), std::forward<Args>(args)...);
70+
auto task = std::make_shared<std::packaged_task<ReturnT()>>(ff);
71+
72+
// Package the task in a wrapper with a common void result
73+
// plus a skip flag for destruction without running the task
74+
std::packaged_task<void(bool)> resultTask([task](bool skip) {
75+
if (!skip)
76+
(*task)();
77+
// task automatically deleted when shared_ptr goes out of scope
78+
});
6379

6480
// Add task to queue and return future
6581
std::future<ReturnT> res = task->get_future();
6682
{
6783
std::unique_lock<std::mutex> lock(m_mutex);
84+
if (m_exit)
85+
{
86+
// Pool is shutting down - reject new tasks
87+
throw std::runtime_error("Cannot enqueue task: thread pool is shutting down");
88+
}
6889
m_tasks.emplace(std::move(resultTask));
6990
}
7091
m_cv.notify_one();
@@ -73,19 +94,36 @@ namespace SharedUtil
7394

7495
void shutdown()
7596
{
76-
if (m_exit)
77-
return;
78-
79-
// Ensure every thread receives the exit state, and discard all remaining tasks.
8097
{
8198
std::unique_lock<std::mutex> lock(m_mutex);
99+
100+
// Already shutting down or shut down
101+
if (m_exit)
102+
return;
103+
82104
m_exit = true;
83105

106+
// Discard all remaining tasks
84107
while (!m_tasks.empty())
85108
{
86-
// Run each task but skip execution of the actual function (-> just delete the task)
109+
// Run each task with skip flag to clean up without executing
87110
auto task = std::move(m_tasks.front());
88-
task(true);
111+
m_tasks.pop(); // Important: Remove from queue to avoid infinite loop
112+
113+
// Execute cleanup outside the critical section to reduce lock contention
114+
lock.unlock();
115+
try
116+
{
117+
task(true); // Cleanup the shared_ptr
118+
}
119+
catch (...)
120+
{
121+
// Exceptions during cleanup indicate a serious bug (e.g., corrupted lambda)
122+
// We cannot propagate this exception as we're mid-shutdown with the lock released.
123+
// In debug builds, this should be logged/asserted.
124+
dassert(false && "Exception during thread pool task cleanup");
125+
}
126+
lock.lock();
89127
}
90128
}
91129

@@ -94,12 +132,24 @@ namespace SharedUtil
94132

95133
// Wait for threads to end
96134
for (std::thread& worker : m_vecThreads)
97-
worker.join();
135+
{
136+
if (worker.joinable())
137+
worker.join();
138+
}
98139
}
99140

100-
~CThreadPool()
141+
~CThreadPool() noexcept
101142
{
102-
shutdown();
143+
try
144+
{
145+
shutdown();
146+
}
147+
catch (...)
148+
{
149+
// Must suppress exceptions to prevent std::terminate().
150+
// This should only happen if mutex operations fail (system error).
151+
dassert(false && "Exception during thread pool destruction");
152+
}
103153
}
104154

105155
static CThreadPool& getDefaultThreadPool()

0 commit comments

Comments
 (0)