3c30f7b971
Setting the Thread_AutoRelease flag (default when using IThreader::MakeThread) caused a use-after-free after running the thread body.
554 lines
13 KiB
C++
554 lines
13 KiB
C++
/**
|
|
* vim: set ts=4 sw=4 tw=99 noet :
|
|
* =============================================================================
|
|
* SourceMod
|
|
* Copyright (C) 2004-2009 AlliedModders LLC. All rights reserved.
|
|
* =============================================================================
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it under
|
|
* the terms of the GNU General Public License, version 3.0, as published by the
|
|
* Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
* details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along with
|
|
* this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* As a special exception, AlliedModders LLC gives you permission to link the
|
|
* code of this program (as well as its derivative works) to "Half-Life 2," the
|
|
* "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
|
|
* by the Valve Corporation. You must obey the GNU General Public License in
|
|
* all respects for all other code used. Additionally, AlliedModders LLC grants
|
|
* this exception to all derivative works. AlliedModders LLC defines further
|
|
* exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
|
|
* or <http://www.sourcemod.net/license.php>.
|
|
*
|
|
* Version: $Id$
|
|
*/
|
|
#include <sm_platform.h>
|
|
#include <amtl/am-deque.h>
|
|
#include <amtl/am-maybe.h>
|
|
#include <amtl/am-thread.h>
|
|
#include <atomic>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <thread>
|
|
#include "BaseWorker.h"
|
|
#include "ThreadSupport.h"
|
|
#include "common_logic.h"
|
|
|
|
static constexpr unsigned int DEFAULT_THINK_TIME_MS = 20;
|
|
|
|
class CompatWorker final : public IThreadWorker
|
|
{
|
|
public:
|
|
explicit CompatWorker(IThreadWorkerCallbacks* callbacks);
|
|
~CompatWorker();
|
|
|
|
void MakeThread(IThread *pThread) override;
|
|
IThreadHandle *MakeThread(IThread *pThread, ThreadFlags flags) override;
|
|
IThreadHandle *MakeThread(IThread *pThread, const ThreadParams *params) override;
|
|
void GetPriorityBounds(ThreadPriority &max, ThreadPriority &min) override;
|
|
unsigned int RunFrame() override;
|
|
bool Pause() override;
|
|
bool Unpause() override;
|
|
bool Start() override;
|
|
bool Stop(bool flush) override;
|
|
WorkerState GetStatus(unsigned int *numThreads) override;
|
|
void SetMaxThreadsPerFrame(unsigned int threads) override;
|
|
void SetThinkTimePerFrame(unsigned int thinktime) override;
|
|
|
|
private:
|
|
void Flush();
|
|
void Worker();
|
|
void RunWork(SWThreadHandle* handle);
|
|
void RunWorkLocked(std::unique_lock<std::mutex>* lock, SWThreadHandle* handle);
|
|
|
|
private:
|
|
IThreadWorkerCallbacks* callbacks_;
|
|
WorkerState state_;
|
|
std::mutex mutex_;
|
|
std::condition_variable work_cv_;
|
|
std::deque<SWThreadHandle*> work_;
|
|
std::unique_ptr<std::thread> thread_;
|
|
std::atomic<unsigned int> jobs_per_wakeup_;
|
|
std::atomic<unsigned int> wait_between_jobs_;
|
|
};
|
|
|
|
CompatWorker::CompatWorker(IThreadWorkerCallbacks* callbacks)
|
|
: callbacks_(callbacks),
|
|
state_(Worker_Stopped),
|
|
jobs_per_wakeup_(SM_DEFAULT_THREADS_PER_FRAME),
|
|
wait_between_jobs_(DEFAULT_THINK_TIME_MS)
|
|
{
|
|
}
|
|
|
|
CompatWorker::~CompatWorker()
|
|
{
|
|
Stop(false /* ignored */);
|
|
Flush();
|
|
}
|
|
|
|
bool CompatWorker::Start()
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (state_ != Worker_Stopped)
|
|
return false;
|
|
|
|
thread_ = ke::NewThread("SM CompatWorker Thread", [this]() -> void {
|
|
Worker();
|
|
});
|
|
|
|
state_ = Worker_Running;
|
|
return true;
|
|
}
|
|
|
|
bool CompatWorker::Stop(bool)
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
if (state_ <= Worker_Stopped)
|
|
return false;
|
|
|
|
state_ = Worker_Stopped;
|
|
work_cv_.notify_all();
|
|
}
|
|
|
|
thread_->join();
|
|
thread_ = nullptr;
|
|
|
|
Flush();
|
|
return true;
|
|
}
|
|
|
|
bool CompatWorker::Pause()
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (state_ != Worker_Running)
|
|
return false;
|
|
|
|
state_ = Worker_Paused;
|
|
work_cv_.notify_all();
|
|
return true;
|
|
}
|
|
|
|
bool CompatWorker::Unpause()
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (state_ != Worker_Paused)
|
|
return false;
|
|
|
|
state_ = Worker_Running;
|
|
work_cv_.notify_all();
|
|
return true;
|
|
}
|
|
|
|
void CompatWorker::Flush()
|
|
{
|
|
while (!work_.empty()) {
|
|
auto handle = ke::PopFront(&work_);
|
|
handle->GetThread()->OnTerminate(handle, true);
|
|
if (handle->m_params.flags & Thread_AutoRelease)
|
|
delete handle;
|
|
}
|
|
}
|
|
|
|
void CompatWorker::Worker()
|
|
{
|
|
// Note: this must be first to ensure an ordering between Worker() and
|
|
// Start(). It must also be outside of the loop to ensure the lock is
|
|
// held across wakeup and retesting the predicates.
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
|
|
if (callbacks_) {
|
|
lock.unlock();
|
|
callbacks_->OnWorkerStart(this);
|
|
lock.lock();
|
|
}
|
|
|
|
typedef std::chrono::system_clock Clock;
|
|
typedef std::chrono::time_point<Clock> TimePoint;
|
|
|
|
auto can_work = [this]() -> bool {
|
|
return state_ == Worker_Running && !work_.empty();
|
|
};
|
|
|
|
ke::Maybe<TimePoint> wait;
|
|
unsigned int work_in_frame = 0;
|
|
for (;;) {
|
|
if (state_ == Worker_Stopped)
|
|
break;
|
|
|
|
if (!can_work()) {
|
|
// Wait for work or a Stop.
|
|
work_cv_.wait(lock);
|
|
continue;
|
|
}
|
|
|
|
if (wait.isValid()) {
|
|
// Wait until the specified time has passed. If we wake up with a
|
|
// timeout, then the wait has elapsed, so reset the holder.
|
|
if (work_cv_.wait_until(lock, wait.get()) == std::cv_status::timeout)
|
|
wait = ke::Nothing();
|
|
continue;
|
|
}
|
|
|
|
assert(state_ == Worker_Running);
|
|
assert(!work_.empty());
|
|
|
|
SWThreadHandle* handle = ke::PopFront(&work_);
|
|
RunWorkLocked(&lock, handle);
|
|
work_in_frame++;
|
|
|
|
// If we've reached our max jobs per "frame", signal that the next
|
|
// immediate job must be delayed. We retain the old ThreadWorker
|
|
// behavior by checking if the queue has more work. Thus, a delay
|
|
// only occurs if two jobs would be processed in the same wakeup.
|
|
if (work_in_frame >= jobs_per_wakeup_ && wait_between_jobs_ && can_work())
|
|
wait = ke::Some(Clock::now() + std::chrono::milliseconds(wait_between_jobs_));
|
|
}
|
|
|
|
assert(lock.owns_lock());
|
|
|
|
while (!work_.empty()) {
|
|
SWThreadHandle* handle = ke::PopFront(&work_);
|
|
RunWorkLocked(&lock, handle);
|
|
}
|
|
}
|
|
|
|
unsigned int CompatWorker::RunFrame()
|
|
{
|
|
unsigned int nprocessed = 0;
|
|
for (unsigned int i = 1; i <= jobs_per_wakeup_; i++) {
|
|
SWThreadHandle* handle;
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
if (work_.empty())
|
|
break;
|
|
handle = ke::PopFront(&work_);
|
|
}
|
|
|
|
RunWork(handle);
|
|
nprocessed++;
|
|
}
|
|
return nprocessed;
|
|
}
|
|
|
|
void CompatWorker::RunWorkLocked(std::unique_lock<std::mutex>* lock, SWThreadHandle* handle)
|
|
{
|
|
lock->unlock();
|
|
RunWork(handle);
|
|
lock->lock();
|
|
}
|
|
|
|
void CompatWorker::RunWork(SWThreadHandle* handle)
|
|
{
|
|
bool autorelease = !!(handle->m_params.flags & Thread_AutoRelease);
|
|
handle->m_state = Thread_Running;
|
|
handle->GetThread()->RunThread(handle);
|
|
handle->m_state = Thread_Done;
|
|
handle->GetThread()->OnTerminate(handle, false);
|
|
if (autorelease)
|
|
delete handle;
|
|
}
|
|
|
|
void CompatWorker::MakeThread(IThread *pThread)
|
|
{
|
|
ThreadParams params;
|
|
params.flags = Thread_AutoRelease;
|
|
MakeThread(pThread, ¶ms);
|
|
}
|
|
|
|
IThreadHandle *CompatWorker::MakeThread(IThread *pThread, ThreadFlags flags)
|
|
{
|
|
ThreadParams params;
|
|
params.flags = flags;
|
|
return MakeThread(pThread, ¶ms);
|
|
}
|
|
|
|
IThreadHandle *CompatWorker::MakeThread(IThread *pThread, const ThreadParams *params)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
ThreadParams def_params;
|
|
if (!params)
|
|
params = &def_params;
|
|
|
|
if (state_ <= Worker_Stopped)
|
|
return nullptr;
|
|
|
|
SWThreadHandle* handle = new SWThreadHandle(this, params, pThread);
|
|
work_.push_back(handle);
|
|
work_cv_.notify_one();
|
|
return handle;
|
|
}
|
|
|
|
void CompatWorker::GetPriorityBounds(ThreadPriority &max, ThreadPriority &min)
|
|
{
|
|
min = ThreadPrio_Normal;
|
|
max = ThreadPrio_Normal;
|
|
}
|
|
|
|
void CompatWorker::SetMaxThreadsPerFrame(unsigned int threads)
|
|
{
|
|
jobs_per_wakeup_ = threads;
|
|
}
|
|
|
|
void CompatWorker::SetThinkTimePerFrame(unsigned int thinktime)
|
|
{
|
|
wait_between_jobs_ = thinktime;
|
|
}
|
|
|
|
WorkerState CompatWorker::GetStatus(unsigned int *numThreads)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
|
|
// This number is meaningless and the status is racy.
|
|
if (numThreads)
|
|
*numThreads = jobs_per_wakeup_;
|
|
return state_;
|
|
}
|
|
|
|
class CompatThread final : public IThreadHandle
|
|
{
|
|
public:
|
|
CompatThread(IThread* callbacks, const ThreadParams* params);
|
|
|
|
bool WaitForThread() override;
|
|
void DestroyThis() override;
|
|
IThreadCreator *Parent() override;
|
|
void GetParams(ThreadParams *ptparams) override;
|
|
ThreadPriority GetPriority() override;
|
|
bool SetPriority(ThreadPriority prio) override;
|
|
ThreadState GetState() override;
|
|
bool Unpause() override;
|
|
|
|
private:
|
|
void Run();
|
|
|
|
private:
|
|
IThread* callbacks_;
|
|
ThreadParams params_;
|
|
std::unique_ptr<std::thread> thread_;
|
|
std::mutex mutex_;
|
|
std::condition_variable check_cv_;
|
|
std::atomic<bool> finished_;
|
|
};
|
|
|
|
CompatThread::CompatThread(IThread* callbacks, const ThreadParams* params)
|
|
: callbacks_(callbacks),
|
|
params_(*params)
|
|
{
|
|
if (!(params_.flags & Thread_CreateSuspended))
|
|
Unpause();
|
|
}
|
|
|
|
bool CompatThread::Unpause()
|
|
{
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
if (thread_)
|
|
return false;
|
|
|
|
thread_ = ke::NewThread("SM CompatThread", [this]() -> void {
|
|
Run();
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
void CompatThread::Run()
|
|
{
|
|
// Create an ordering between when the thread runs and when thread_ is assigned.
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
|
|
lock.unlock();
|
|
callbacks_->RunThread(this);
|
|
finished_ = true;
|
|
callbacks_->OnTerminate(this, false);
|
|
|
|
if (params_.flags & Thread_AutoRelease) {
|
|
// There should be no handles outstanding, so it's safe to self-destruct.
|
|
thread_->detach();
|
|
delete this;
|
|
return;
|
|
}
|
|
|
|
lock.lock();
|
|
callbacks_ = nullptr;
|
|
check_cv_.notify_all();
|
|
}
|
|
|
|
bool CompatThread::WaitForThread()
|
|
{
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
for (;;) {
|
|
// When done, callbacks are unset. If paused, this will deadlock.
|
|
if (!callbacks_)
|
|
break;
|
|
check_cv_.wait(lock);
|
|
}
|
|
|
|
thread_->join();
|
|
return true;
|
|
}
|
|
|
|
ThreadState CompatThread::GetState()
|
|
{
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
if (!thread_)
|
|
return Thread_Paused;
|
|
return finished_ ? Thread_Done : Thread_Running;
|
|
}
|
|
|
|
void CompatThread::DestroyThis()
|
|
{
|
|
delete this;
|
|
}
|
|
|
|
ThreadPriority CompatThread::GetPriority()
|
|
{
|
|
return ThreadPrio_Normal;
|
|
}
|
|
|
|
bool CompatThread::SetPriority(ThreadPriority prio)
|
|
{
|
|
return prio == ThreadPrio_Normal;
|
|
}
|
|
|
|
IThreadCreator *CompatThread::Parent()
|
|
{
|
|
return g_pThreader;
|
|
}
|
|
|
|
void CompatThread::GetParams(ThreadParams *ptparams)
|
|
{
|
|
*ptparams = params_;
|
|
}
|
|
|
|
class CompatMutex : public IMutex
|
|
{
|
|
public:
|
|
bool TryLock() {
|
|
return mutex_.try_lock();
|
|
}
|
|
void Lock() {
|
|
mutex_.lock();
|
|
}
|
|
void Unlock() {
|
|
mutex_.unlock();
|
|
}
|
|
void DestroyThis() {
|
|
delete this;
|
|
}
|
|
private:
|
|
std::mutex mutex_;
|
|
};
|
|
|
|
class CompatThreader final : public IThreader
|
|
{
|
|
public:
|
|
void MakeThread(IThread *pThread) override;
|
|
IThreadHandle *MakeThread(IThread *pThread, ThreadFlags flags) override;
|
|
IThreadHandle *MakeThread(IThread *pThread, const ThreadParams *params) override;
|
|
void GetPriorityBounds(ThreadPriority &max, ThreadPriority &min) override;
|
|
IMutex *MakeMutex() override;
|
|
void ThreadSleep(unsigned int ms) override;
|
|
IEventSignal *MakeEventSignal() override;
|
|
IThreadWorker *MakeWorker(IThreadWorkerCallbacks *hooks, bool threaded) override;
|
|
void DestroyWorker(IThreadWorker *pWorker) override;
|
|
} sCompatThreader;
|
|
|
|
void CompatThreader::MakeThread(IThread *pThread)
|
|
{
|
|
ThreadParams params;
|
|
params.flags = Thread_AutoRelease;
|
|
MakeThread(pThread, ¶ms);
|
|
}
|
|
|
|
IThreadHandle *CompatThreader::MakeThread(IThread *pThread, ThreadFlags flags)
|
|
{
|
|
ThreadParams params;
|
|
params.flags = flags;
|
|
return MakeThread(pThread, ¶ms);
|
|
}
|
|
|
|
IThreadHandle *CompatThreader::MakeThread(IThread *pThread, const ThreadParams *params)
|
|
{
|
|
ThreadParams def_params;
|
|
if (!params)
|
|
params = &def_params;
|
|
|
|
return new CompatThread(pThread, params);
|
|
}
|
|
|
|
void CompatThreader::GetPriorityBounds(ThreadPriority &max, ThreadPriority &min)
|
|
{
|
|
min = ThreadPrio_Normal;
|
|
max = ThreadPrio_Normal;
|
|
}
|
|
|
|
IMutex *CompatThreader::MakeMutex()
|
|
{
|
|
return new CompatMutex();
|
|
}
|
|
|
|
void CompatThreader::ThreadSleep(unsigned int ms)
|
|
{
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
|
|
}
|
|
|
|
class CompatEventSignal final : public IEventSignal
|
|
{
|
|
public:
|
|
void Wait() override {
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
cv_.wait(lock);
|
|
}
|
|
void Signal() override {
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
cv_.notify_all();
|
|
}
|
|
void DestroyThis() override {
|
|
delete this;
|
|
}
|
|
|
|
private:
|
|
std::mutex mutex_;
|
|
std::condition_variable cv_;
|
|
};
|
|
|
|
IEventSignal *CompatThreader::MakeEventSignal()
|
|
{
|
|
return new CompatEventSignal();
|
|
}
|
|
|
|
IThreadWorker *CompatThreader::MakeWorker(IThreadWorkerCallbacks *hooks, bool threaded)
|
|
{
|
|
if (!threaded)
|
|
return new BaseWorker(hooks);
|
|
return new CompatWorker(hooks);
|
|
}
|
|
|
|
void CompatThreader::DestroyWorker(IThreadWorker *pWorker)
|
|
{
|
|
delete pWorker;
|
|
}
|
|
|
|
IThreader *g_pThreader = &sCompatThreader;
|
|
|
|
class RegThreadStuff : public SMGlobalClass
|
|
{
|
|
public:
|
|
void OnSourceModAllInitialized()
|
|
{
|
|
sharesys->AddInterface(NULL, g_pThreader);
|
|
}
|
|
} s_RegThreadStuff;
|
|
|