source-engine/vstdlib/jobthread.cpp
2022-11-20 15:00:44 +02:00

1452 lines
33 KiB
C++

//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
//=============================================================================
#if defined( _WIN32 ) && !defined( _X360 )
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif
#include "tier0/dbg.h"
#include "tier0/tslist.h"
#include "tier0/icommandline.h"
#include "vstdlib/jobthread.h"
#include "vstdlib/random.h"
#include "tier1/functors.h"
#include "tier1/fmtstr.h"
#include "tier1/utlvector.h"
#include "tier1/generichash.h"
#include "tier0/vprof.h"
#if defined( _X360 )
#include "xbox/xbox_win32stubs.h"
#endif
#include "tier0/memdbgon.h"
class CJobThread;
//-----------------------------------------------------------------------------
inline void ServiceJobAndRelease( CJob *pJob, int iThread = -1 )
{
// TryLock() would only fail if another thread has entered
// Execute() or Abort()
if ( !pJob->IsFinished() && pJob->TryLock() )
{
// ...service the request
pJob->SetServiceThread( iThread );
pJob->Execute();
pJob->Unlock();
}
pJob->Release();
}
//-----------------------------------------------------------------------------
class ALIGN16 CJobQueue
{
public:
CJobQueue() :
m_nItems( 0 ),
m_nMaxItems( INT_MAX )
{
for ( int i = 0; i < ARRAYSIZE( m_pQueues ); i++ )
{
m_pQueues[i] = new CTSQueue<CJob *>;
}
}
~CJobQueue()
{
for ( int i = 0; i < ARRAYSIZE( m_pQueues ); i++ )
{
delete m_pQueues[i];
}
}
int Count()
{
return m_nItems;
}
int Count( JobPriority_t priority )
{
return m_pQueues[priority]->Count();
}
CJob *PrePush()
{
if ( m_nItems >= m_nMaxItems )
{
CJob *pOverflowJob;
if ( Pop( &pOverflowJob ) )
{
return pOverflowJob;
}
}
return NULL;
}
int Push( CJob *pJob, int iThread = -1 )
{
pJob->AddRef();
CJob *pOverflowJob;
int nOverflow = 0;
while ( ( pOverflowJob = PrePush() ) != NULL )
{
ServiceJobAndRelease( pJob );
nOverflow++;
}
m_pQueues[pJob->GetPriority()]->PushItem( pJob );
m_mutex.Lock();
if ( ++m_nItems == 1 )
{
m_JobAvailableEvent.Set();
}
m_mutex.Unlock();
return nOverflow;
}
bool Pop( CJob **ppJob )
{
m_mutex.Lock();
if ( !m_nItems )
{
m_mutex.Unlock();
*ppJob = NULL;
return false;
}
if ( --m_nItems == 0 )
{
m_JobAvailableEvent.Reset();
}
m_mutex.Unlock();
for ( int i = JP_HIGH; i >= 0; --i )
{
if ( m_pQueues[i]->PopItem( ppJob ) )
{
return true;
}
}
AssertMsg( 0, "Expected at least one queue item" );
*ppJob = NULL;
return false;
}
CThreadEvent &GetEventHandle()
{
return m_JobAvailableEvent;
}
void Flush()
{
// Only safe to call when system is suspended
m_mutex.Lock();
m_nItems = 0;
m_JobAvailableEvent.Reset();
CJob *pJob;
for ( int i = JP_HIGH; i >= 0; --i )
{
while ( m_pQueues[i]->PopItem( &pJob ) )
{
pJob->Abort();
pJob->Release();
}
}
m_mutex.Unlock();
}
private:
CTSQueue<CJob *> *m_pQueues[JP_HIGH + 1];
int m_nItems;
int m_nMaxItems;
CThreadMutex m_mutex;
CThreadManualEvent m_JobAvailableEvent;
} ALIGN16_POST;
//-----------------------------------------------------------------------------
//
// CThreadPool
//
//-----------------------------------------------------------------------------
class CThreadPool : public CRefCounted1<IThreadPool, CRefCountServiceMT>
{
public:
CThreadPool();
~CThreadPool();
//-----------------------------------------------------
// Thread functions
//-----------------------------------------------------
bool Start( const ThreadPoolStartParams_t &startParams = ThreadPoolStartParams_t() ) { return Start( startParams, NULL ); }
bool Start( const ThreadPoolStartParams_t &startParams, const char *pszNameOverride );
bool Stop( int timeout = TT_INFINITE );
void Distribute( bool bDistribute = true, int *pAffinityTable = NULL );
//-----------------------------------------------------
// Functions for any thread
//-----------------------------------------------------
unsigned GetJobCount() { return m_nJobs; }
int NumThreads();
int NumIdleThreads();
//-----------------------------------------------------
// Pause/resume processing jobs
//-----------------------------------------------------
int SuspendExecution();
int ResumeExecution();
//-----------------------------------------------------
// Offer the current thread to the pool
//-----------------------------------------------------
virtual int YieldWait( CThreadEvent **pEvents, int nEvents, bool bWaitAll = true, unsigned timeout = TT_INFINITE );
virtual int YieldWait( CJob **, int nJobs, bool bWaitAll = true, unsigned timeout = TT_INFINITE );
void Yield( unsigned timeout );
//-----------------------------------------------------
// Add a native job to the queue (master thread)
//-----------------------------------------------------
void AddJob( CJob * );
void InsertJobInQueue( CJob * );
//-----------------------------------------------------
// All threads execute pFunctor asap. Thread will either wake up
// and execute or execute pFunctor right after completing current job and
// before looking for another job.
//-----------------------------------------------------
// void ExecuteHighPriorityFunctor( CFunctor *pFunctor );
//-----------------------------------------------------
// Add an function object to the queue (master thread)
//-----------------------------------------------------
void AddFunctorInternal( CFunctor *, CJob ** = NULL, const char *pszDescription = NULL, unsigned flags = 0 );
//-----------------------------------------------------
// Remove a job from the queue (master thread)
//-----------------------------------------------------
virtual void ChangePriority( CJob *p, JobPriority_t priority );
//-----------------------------------------------------
// Bulk job manipulation (blocking)
//-----------------------------------------------------
int ExecuteToPriority( JobPriority_t toPriority, JobFilter_t pfnFilter = NULL );
int AbortAll();
virtual void Reserved1() {}
private:
enum
{
IO_STACKSIZE = ( 64 * 1024 ),
COMPUTATION_STACKSIZE = 0,
};
//-----------------------------------------------------
//
//-----------------------------------------------------
CJob *PeekJob();
CJob *GetDummyJob();
//-----------------------------------------------------
// Thread functions
//-----------------------------------------------------
int Run();
private:
friend class CJobThread;
CJobQueue m_SharedQueue;
CInterlockedInt m_nIdleThreads;
CUtlVector<CJobThread *> m_Threads;
CUtlVector<CThreadEvent *> m_IdleEvents;
CThreadMutex m_SuspendMutex;
int m_nSuspend;
CInterlockedInt m_nJobs;
// Some jobs should only be executed on the threadpool thread(s). Ie: the rendering thread has the GL context
// and the main thread coming in and "helping" with jobs breaks that pretty nicely. This flag states that
// only the threadpool threads should execute these jobs.
bool m_bExecOnThreadPoolThreadsOnly;
};
//-----------------------------------------------------------------------------
JOB_INTERFACE IThreadPool *CreateThreadPool1()
{
return new CThreadPool;
}
JOB_INTERFACE void DestroyThreadPool( IThreadPool *pPool )
{
delete pPool;
}
//-----------------------------------------------------------------------------
class CGlobalThreadPool : public CThreadPool
{
public:
virtual bool Start( const ThreadPoolStartParams_t &startParamsIn )
{
int nThreads = ( CommandLine()->ParmValue( "-threads", -1 ) - 1 );
ThreadPoolStartParams_t startParams = startParamsIn;
if ( nThreads >= 0 )
{
startParams.nThreads = nThreads;
}
else
{
// Cap the GlobPool threads at 4.
startParams.nThreadsMax = 4;
}
return CThreadPool::Start( startParams, "Glob" );
}
virtual bool OnFinalRelease()
{
AssertMsg( 0, "Releasing global thread pool object!" );
return false;
}
};
//-----------------------------------------------------------------------------
class CJobThread : public CWorkerThread
{
public:
CJobThread( CThreadPool *pOwner, int iThread ) :
m_SharedQueue( pOwner->m_SharedQueue ),
m_pOwner( pOwner ),
m_iThread( iThread )
{
}
CThreadEvent &GetIdleEvent()
{
return m_IdleEvent;
}
CJobQueue &AccessDirectQueue()
{
return m_DirectQueue;
}
private:
unsigned Wait()
{
unsigned waitResult;
tmZone( TELEMETRY_LEVEL0, TMZF_IDLE, "%s", __FUNCTION__ );
#ifdef WIN32
enum Event_t
{
CALL_FROM_MASTER,
SHARED_QUEUE,
DIRECT_QUEUE,
NUM_EVENTS
};
HANDLE waitHandles[NUM_EVENTS];
waitHandles[CALL_FROM_MASTER] = GetCallHandle().GetHandle();
waitHandles[SHARED_QUEUE] = m_SharedQueue.GetEventHandle().GetHandle();
waitHandles[DIRECT_QUEUE] = m_DirectQueue.GetEventHandle().GetHandle();
#ifdef _DEBUG
while ( ( waitResult = WaitForMultipleObjects( ARRAYSIZE(waitHandles), waitHandles, FALSE, 10 ) ) == WAIT_TIMEOUT )
{
waitResult = waitResult; // break here
}
#else
waitResult = WaitForMultipleObjects( ARRAYSIZE(waitHandles), waitHandles, FALSE, INFINITE );
#endif
#else // !win32
bool bSet = false;
int nWaitTime = 100;
while( !bSet )
{
// Jobs are typically enqueued to the shared job queue so wait on it first.
bSet = m_SharedQueue.GetEventHandle().Wait( nWaitTime );
if( !bSet )
bSet = m_DirectQueue.GetEventHandle().Wait( 10 );
if ( !bSet )
bSet = GetCallHandle().Wait( 0 );
}
if ( !bSet )
waitResult = WAIT_TIMEOUT;
else
waitResult = WAIT_OBJECT_0;
#endif
return waitResult;
}
int Run()
{
// Wait for either a call from the master thread, or an item in the queue...
unsigned waitResult;
bool bExit = false;
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ );
m_pOwner->m_nIdleThreads++;
m_IdleEvent.Set();
while (!bExit && ( ( waitResult = Wait() ) != WAIT_FAILED ) )
{
if ( PeekCall() )
{
CFunctor *pFunctor = NULL;
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s PeekCall():%d", __FUNCTION__, GetCallParam() );
switch ( GetCallParam() )
{
case TPM_EXIT:
Reply( true );
bExit = TRUE;
break;
case TPM_SUSPEND:
Reply( true );
Suspend();
break;
/* case TPM_RUNFUNCTOR:
if( pFunctor )
{
( *pFunctor )();
Reply( true );
}
else
{
Assert( pFunctor );
Reply( false );
}
break;*/
default:
AssertMsg( 0, "Unknown call to thread" );
Reply( false );
break;
}
}
else
{
tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s !PeekCall()", __FUNCTION__ );
CJob *pJob;
bool bTookJob = false;
do
{
if ( !m_DirectQueue.Pop( &pJob) )
{
if ( !m_SharedQueue.Pop( &pJob ) )
{
// Nothing to process, return to wait state
break;
}
}
if ( !bTookJob )
{
m_IdleEvent.Reset();
m_pOwner->m_nIdleThreads--;
bTookJob = true;
}
ServiceJobAndRelease( pJob, m_iThread );
m_pOwner->m_nJobs--;
} while ( !PeekCall() );
if ( bTookJob )
{
m_pOwner->m_nIdleThreads++;
m_IdleEvent.Set();
}
}
}
m_pOwner->m_nIdleThreads--;
m_IdleEvent.Reset();
return 0;
}
CJobQueue m_DirectQueue;
CJobQueue & m_SharedQueue;
CThreadPool * m_pOwner;
CThreadManualEvent m_IdleEvent;
int m_iThread;
};
//-----------------------------------------------------------------------------
CGlobalThreadPool g_ThreadPool;
IThreadPool *g_pThreadPool = &g_ThreadPool;
//-----------------------------------------------------------------------------
//
// CThreadPool
//
//-----------------------------------------------------------------------------
CThreadPool::CThreadPool() :
m_nIdleThreads( 0 ),
m_nJobs( 0 ),
m_nSuspend( 0 )
{
}
//---------------------------------------------------------
CThreadPool::~CThreadPool()
{
Stop();
}
//---------------------------------------------------------
//
//---------------------------------------------------------
int CThreadPool::NumThreads()
{
return m_Threads.Count();
}
//---------------------------------------------------------
//
//---------------------------------------------------------
int CThreadPool::NumIdleThreads()
{
return m_nIdleThreads;
}
/*void CThreadPool::ExecuteHighPriorityFunctor( CFunctor *pFunctor )
{
int i;
for ( i = 0; i < m_Threads.Count(); i++ )
{
m_Threads[i]->CallWorker( TPM_RUNFUNCTOR, 0, false, pFunctor );
}
for ( i = 0; i < m_Threads.Count(); i++ )
{
m_Threads[i]->WaitForReply();
}
}*/
//---------------------------------------------------------
// Pause/resume processing jobs
//---------------------------------------------------------
int CThreadPool::SuspendExecution()
{
AUTO_LOCK( m_SuspendMutex );
// If not already suspended
if ( m_nSuspend == 0 )
{
// Make sure state is correct
int i;
for ( i = 0; i < m_Threads.Count(); i++ )
{
m_Threads[i]->CallWorker( TPM_SUSPEND, 0 );
}
for ( i = 0; i < m_Threads.Count(); i++ )
{
m_Threads[i]->WaitForReply();
}
// Because worker must signal before suspending, we could reach
// here with the thread not actually suspended
for ( i = 0; i < m_Threads.Count(); i++ )
{
while ( !m_Threads[i]->IsSuspended() )
{
ThreadSleep();
}
}
}
return m_nSuspend++;
}
//---------------------------------------------------------
int CThreadPool::ResumeExecution()
{
AUTO_LOCK( m_SuspendMutex );
AssertMsg( m_nSuspend >= 1, "Attempted resume when not suspended");
int result = m_nSuspend--;
if (m_nSuspend == 0 )
{
for ( int i = 0; i < m_Threads.Count(); i++ )
{
m_Threads[i]->Resume();
}
}
return result;
}
//---------------------------------------------------------
int CThreadPool::YieldWait( CThreadEvent **pEvents, int nEvents, bool bWaitAll, unsigned timeout )
{
tmZone( TELEMETRY_LEVEL0, TMZF_IDLE, "%s(%d) SPINNING %t", __FUNCTION__, timeout, tmSendCallStack( TELEMETRY_LEVEL0, 0 ) );
Assert( timeout == TT_INFINITE ); // unimplemented
int result;
CJob *pJob;
// Always wait for zero milliseconds initially, to let us process jobs on this thread.
timeout = 0;
while ( ( result = CThreadEvent::WaitForMultiple( nEvents, pEvents, bWaitAll, timeout ) ) == TW_TIMEOUT )
{
if ( !m_bExecOnThreadPoolThreadsOnly && m_SharedQueue.Pop( &pJob ) )
{
ServiceJobAndRelease( pJob );
m_nJobs--;
}
else
{
// Since there are no jobs for the main thread set the timeout to infinite.
// The only disadvantage to this is that if a job thread creates a new job
// then the main thread will not be available to pick it up, but if that
// is a problem you can just create more worker threads. Debugging test runs
// of TF2 suggests that jobs are only ever added from the main thread which
// means that there is no disadvantage.
// Waiting on the events instead of busy spinning has multiple advantages.
// It avoids wasting CPU time/electricity, it makes it more obvious in profiles
// when the main thread is idle versus busy, and it allows ready thread analysis
// in xperf to find out what woke up a waiting thread.
// It also avoids unnecessary CPU starvation -- seen on customer traces of TF2.
timeout = TT_INFINITE;
}
}
return result;
}
//---------------------------------------------------------
int CThreadPool::YieldWait( CJob **ppJobs, int nJobs, bool bWaitAll, unsigned timeout )
{
CUtlVectorFixed<CThreadEvent *, 64> handles;
if ( nJobs > handles.NumAllocated() - 2 )
{
return TW_FAILED;
}
for ( int i = 0; i < nJobs; i++ )
{
handles.AddToTail( ppJobs[i]->AccessEvent() );
}
return YieldWait( handles.Base(), handles.Count(), bWaitAll, timeout);
}
//---------------------------------------------------------
void CThreadPool::Yield( unsigned timeout )
{
// @MULTICORE (toml 10/24/2006): not implemented
Assert( ThreadInMainThread() );
if ( !ThreadInMainThread() )
{
ThreadSleep( timeout );
return;
}
ThreadSleep( timeout );
}
//---------------------------------------------------------
// Add a job to the queue
//---------------------------------------------------------
void CThreadPool::AddJob( CJob *pJob )
{
if ( !pJob )
{
return;
}
if ( pJob->m_ThreadPoolData != JOB_NO_DATA )
{
Warning( "Cannot add a thread job already committed to another thread pool\n" );
return;
}
if ( m_Threads.Count() == 0 )
{
// So only threadpool jobs are supposed to execute the jobs, but there are no threadpool threads?
Assert( !m_bExecOnThreadPoolThreadsOnly );
pJob->Execute();
return;
}
int flags = pJob->GetFlags();
if ( !m_bExecOnThreadPoolThreadsOnly && ( ( flags & ( JF_IO | JF_QUEUE ) ) == 0 ) /* @TBD && !m_queue.Count() */ )
{
if ( !NumIdleThreads() )
{
pJob->Execute();
return;
}
pJob->SetPriority( JP_HIGH );
}
if ( !pJob->CanExecute() )
{
// Already handled
ExecuteOnce( Warning( "Attempted to add job to job queue that has already been completed\n" ) );
return;
}
pJob->m_pThreadPool = this;
pJob->m_status = JOB_STATUS_PENDING;
InsertJobInQueue( pJob );
++m_nJobs;
}
//---------------------------------------------------------
//
//---------------------------------------------------------
void CThreadPool::InsertJobInQueue( CJob *pJob )
{
CJobQueue *pQueue;
if ( !( pJob->GetFlags() & JF_SERIAL ) )
{
int iThread = pJob->GetServiceThread();
if ( iThread == -1 || !m_Threads.IsValidIndex( iThread ) )
{
pQueue = &m_SharedQueue;
}
else
{
pQueue = &(m_Threads[iThread]->AccessDirectQueue());
}
}
else
{
pQueue = &(m_Threads[0]->AccessDirectQueue());
}
m_nJobs -= pQueue->Push( pJob );
}
//---------------------------------------------------------
// Add an function object to the queue (master thread)
//---------------------------------------------------------
void CThreadPool::AddFunctorInternal( CFunctor *pFunctor, CJob **ppJob, const char *pszDescription, unsigned flags )
{
// Note: assumes caller has handled refcount
CJob *pJob = new CFunctorJob( pFunctor, pszDescription );
pJob->SetFlags( flags );
AddJob( pJob );
if ( ppJob )
{
*ppJob = pJob;
}
else
{
pJob->Release();
}
}
//---------------------------------------------------------
// Remove a job from the queue
//---------------------------------------------------------
void CThreadPool::ChangePriority( CJob *pJob, JobPriority_t priority )
{
// Right now, only support upping the priority
if ( pJob->GetPriority() < priority )
{
pJob->SetPriority( priority );
m_SharedQueue.Push( pJob );
}
else
{
ExecuteOnce( if ( pJob->GetPriority() != priority ) DevMsg( "CThreadPool::RemoveJob not implemented right now" ) );
}
}
//---------------------------------------------------------
// Execute to a specified priority
//---------------------------------------------------------
int CThreadPool::ExecuteToPriority( JobPriority_t iToPriority, JobFilter_t pfnFilter )
{
SuspendExecution();
CJob *pJob;
int nExecuted = 0;
int i;
int nJobsTotal = GetJobCount();
CUtlVector<CJob *> jobsToPutBack;
for ( int iCurPriority = JP_HIGH; iCurPriority >= iToPriority; --iCurPriority )
{
for ( i = 0; i < m_Threads.Count(); i++ )
{
CJobQueue &queue = m_Threads[i]->AccessDirectQueue();
while ( queue.Count( (JobPriority_t)iCurPriority ) )
{
queue.Pop( &pJob );
if ( pfnFilter && !(*pfnFilter)( pJob ) )
{
if ( pJob->CanExecute() )
{
jobsToPutBack.EnsureCapacity( nJobsTotal );
jobsToPutBack.AddToTail( pJob );
}
else
{
m_nJobs--;
pJob->Release(); // an already serviced job in queue, may as well ditch it (as in, main thread probably force executed)
}
continue;
}
ServiceJobAndRelease( pJob );
m_nJobs--;
nExecuted++;
}
}
while ( m_SharedQueue.Count( (JobPriority_t)iCurPriority ) )
{
m_SharedQueue.Pop( &pJob );
if ( pfnFilter && !(*pfnFilter)( pJob ) )
{
if ( pJob->CanExecute() )
{
jobsToPutBack.EnsureCapacity( nJobsTotal );
jobsToPutBack.AddToTail( pJob );
}
else
{
m_nJobs--;
pJob->Release(); // see above
}
continue;
}
ServiceJobAndRelease( pJob );
m_nJobs--;
nExecuted++;
}
}
for ( i = 0; i < jobsToPutBack.Count(); i++ )
{
InsertJobInQueue( jobsToPutBack[i] );
jobsToPutBack[i]->Release();
}
ResumeExecution();
return nExecuted;
}
//---------------------------------------------------------
//
//---------------------------------------------------------
int CThreadPool::AbortAll()
{
SuspendExecution();
CJob *pJob;
int iAborted = 0;
while ( m_SharedQueue.Pop( &pJob ) )
{
pJob->Abort();
pJob->Release();
iAborted++;
}
for ( int i = 0; i < m_Threads.Count(); i++ )
{
CJobQueue &queue = m_Threads[i]->AccessDirectQueue();
while ( queue.Pop( &pJob ) )
{
pJob->Abort();
pJob->Release();
iAborted++;
}
}
m_nJobs = 0;
ResumeExecution();
return iAborted;
}
//---------------------------------------------------------
// CThreadPool thread functions
//---------------------------------------------------------
bool CThreadPool::Start( const ThreadPoolStartParams_t &startParams, const char *pszName )
{
int nThreads = startParams.nThreads;
m_bExecOnThreadPoolThreadsOnly = startParams.bExecOnThreadPoolThreadsOnly;
if ( nThreads < 0 )
{
const CPUInformation &ci = *GetCPUInformation();
if ( startParams.bIOThreads )
{
nThreads = ci.m_nLogicalProcessors;
}
else
{
nThreads = ( ci.m_nLogicalProcessors / (( ci.m_bHT ) ? 2 : 1) ) - 1; // One per
if ( IsPC() )
{
if ( nThreads > 3 )
{
DevMsg( "Defaulting to limit of 3 worker threads, use -threads on command line if want more\n" ); // Current >4 processor configs don't really work so well, probably due to cache issues? (toml 7/12/2007)
nThreads = 3;
}
}
}
if ( ( startParams.nThreadsMax >= 0 ) && ( nThreads > startParams.nThreadsMax ) )
{
nThreads = startParams.nThreadsMax;
}
}
if ( nThreads <= 0 )
{
return true;
}
int nStackSize = startParams.nStackSize;
if ( nStackSize < 0 )
{
if ( startParams.bIOThreads )
{
nStackSize = IO_STACKSIZE;
}
else
{
nStackSize = COMPUTATION_STACKSIZE;
}
}
int priority = startParams.iThreadPriority;
if ( priority == SHRT_MIN )
{
if ( startParams.bIOThreads )
{
priority = THREAD_PRIORITY_HIGHEST;
}
else
{
priority = ThreadGetPriority();
}
}
bool bDistribute;
if ( startParams.fDistribute != TRS_NONE )
{
bDistribute = ( startParams.fDistribute == TRS_TRUE );
}
else
{
bDistribute = !startParams.bIOThreads;
}
//--------------------------------------------------------
m_Threads.EnsureCapacity( nThreads );
m_IdleEvents.EnsureCapacity( nThreads );
if ( !pszName )
{
pszName = ( startParams.bIOThreads ) ? "IOJobX" : "CmpJobX";
}
while ( nThreads-- )
{
int iThread = m_Threads.AddToTail();
m_IdleEvents.AddToTail();
m_Threads[iThread] = new CJobThread( this, iThread );
m_IdleEvents[iThread] = &m_Threads[iThread]->GetIdleEvent();
m_Threads[iThread]->SetName( CFmtStr( "%s%d", pszName, iThread ) );
m_Threads[iThread]->Start( nStackSize );
m_Threads[iThread]->GetIdleEvent().Wait();
#ifdef WIN32
ThreadSetPriority( (ThreadHandle_t)m_Threads[iThread]->GetThreadHandle(), priority );
#endif
}
Distribute( bDistribute, startParams.bUseAffinityTable ? (int *)startParams.iAffinityTable : NULL );
return true;
}
//---------------------------------------------------------
void CThreadPool::Distribute( bool bDistribute, int *pAffinityTable )
{
if ( bDistribute )
{
const CPUInformation &ci = *GetCPUInformation();
int nHwThreadsPer = (( ci.m_bHT ) ? 2 : 1);
if ( ci.m_nLogicalProcessors > 1 )
{
if ( !pAffinityTable )
{
#if defined( IS_WINDOWS_PC )
// no affinity table, distribution is cycled across all available
HINSTANCE hInst = LoadLibrary( "kernel32.dll" );
if ( hInst )
{
typedef DWORD (WINAPI *SetThreadIdealProcessorFn)(ThreadHandle_t hThread, DWORD dwIdealProcessor);
SetThreadIdealProcessorFn Thread_SetIdealProcessor = (SetThreadIdealProcessorFn)GetProcAddress( hInst, "SetThreadIdealProcessor" );
if ( Thread_SetIdealProcessor )
{
ThreadHandle_t hMainThread = ThreadGetCurrentHandle();
Thread_SetIdealProcessor( hMainThread, 0 );
int iProc = 0;
for ( int i = 0; i < m_Threads.Count(); i++ )
{
iProc += nHwThreadsPer;
if ( iProc >= ci.m_nLogicalProcessors )
{
iProc %= ci.m_nLogicalProcessors;
if ( nHwThreadsPer > 1 )
{
iProc = ( iProc + 1 ) % nHwThreadsPer;
}
}
Thread_SetIdealProcessor((ThreadHandle_t)m_Threads[i]->GetThreadHandle(), iProc);
}
}
FreeLibrary( hInst );
}
#else
// no affinity table, distribution is cycled across all available
int iProc = 0;
for ( int i = 0; i < m_Threads.Count(); i++ )
{
iProc += nHwThreadsPer;
if ( iProc >= ci.m_nLogicalProcessors )
{
iProc %= ci.m_nLogicalProcessors;
if ( nHwThreadsPer > 1 )
{
iProc = ( iProc + 1 ) % nHwThreadsPer;
}
}
#ifdef WIN32
ThreadSetAffinity( (ThreadHandle_t)m_Threads[i]->GetThreadHandle(), 1 << iProc );
#endif
}
#endif
}
else
{
// distribution is from affinity table
for ( int i = 0; i < m_Threads.Count(); i++ )
{
#ifdef WIN32
ThreadSetAffinity( (ThreadHandle_t)m_Threads[i]->GetThreadHandle(), pAffinityTable[i] );
#endif
}
}
}
}
else
{
#ifdef WIN32
DWORD_PTR dwProcessAffinity, dwSystemAffinity;
if ( GetProcessAffinityMask( GetCurrentProcess(), &dwProcessAffinity, &dwSystemAffinity ) )
{
for ( int i = 0; i < m_Threads.Count(); i++ )
{
ThreadSetAffinity( (ThreadHandle_t)m_Threads[i]->GetThreadHandle(), dwProcessAffinity );
}
}
#endif
}
}
//---------------------------------------------------------
bool CThreadPool::Stop( int timeout )
{
for ( int i = 0; i < m_Threads.Count(); i++ )
{
m_Threads[i]->CallWorker( TPM_EXIT );
}
for ( int i = 0; i < m_Threads.Count(); ++i )
{
while( m_Threads[i]->IsAlive() )
{
ThreadSleep( 0 );
}
delete m_Threads[i];
}
m_nJobs = 0;
m_SharedQueue.Flush();
m_nIdleThreads = 0;
m_Threads.RemoveAll();
m_IdleEvents.RemoveAll();
return true;
}
//---------------------------------------------------------
CJob *CThreadPool::GetDummyJob()
{
class CDummyJob : public CJob
{
public:
CDummyJob()
{
Execute();
}
virtual JobStatus_t DoExecute() { return JOB_OK; }
};
static CDummyJob dummyJob;
dummyJob.AddRef();
return &dummyJob;
}
//-----------------------------------------------------------------------------
namespace ThreadPoolTest
{
int g_iSleep;
CThreadEvent g_done;
int g_nTotalToComplete;
CThreadPool *g_pTestThreadPool;
class CCountJob : public CJob
{
public:
virtual JobStatus_t DoExecute()
{
m_nCount++;
ThreadPause();
if ( g_iSleep >= 0)
ThreadSleep( g_iSleep );
if ( bDoWork )
{
byte pMemory[1024];
int i;
for ( i = 0; i < 1024; i++ )
{
pMemory[i] = rand();
}
for ( i = 0; i < 50; i++ )
{
sqrt( (float)HashBlock( pMemory, 1024 ) + HashBlock( pMemory, 1024 ) + 10.0 );
}
bDoWork = false;
}
if ( m_nCount == g_nTotalToComplete )
g_done.Set();
return 0;
}
static CInterlockedInt m_nCount;
bool bDoWork;
};
CInterlockedInt CCountJob::m_nCount;
int g_nTotalAtFinish;
void Test( bool bDistribute, bool bSleep = true, bool bFinishExecute = false, bool bDoWork = false )
{
for ( int bInterleavePushPop = 0; bInterleavePushPop < 2; bInterleavePushPop++ )
{
for ( g_iSleep = -10; g_iSleep <= 10; g_iSleep += 10 )
{
Msg( "ThreadPoolTest: Testing! Sleep %d, interleave %d \n", g_iSleep, bInterleavePushPop );
int nMaxThreads = ( IsX360() ) ? 6 : 8;
int nIncrement = ( IsX360() ) ? 1 : 2;
for ( int i = 1; i <= nMaxThreads; i += nIncrement )
{
CCountJob::m_nCount = 0;
g_nTotalAtFinish = 0;
ThreadPoolStartParams_t params;
params.nThreads = i;
params.fDistribute = ( bDistribute) ? TRS_TRUE : TRS_FALSE;
g_pTestThreadPool->Start( params, "Tst" );
if ( !bInterleavePushPop )
{
g_pTestThreadPool->SuspendExecution();
}
CCountJob jobs[4000];
g_nTotalToComplete = ARRAYSIZE(jobs);
CFastTimer timer, suspendTimer;
suspendTimer.Start();
timer.Start();
for ( int j = 0; j < ARRAYSIZE(jobs); j++ )
{
jobs[j].SetFlags( JF_QUEUE );
jobs[j].bDoWork = bDoWork;
g_pTestThreadPool->AddJob( &jobs[j] );
if ( bSleep && j % 16 == 0 )
{
ThreadSleep( 0 );
}
}
if ( !bInterleavePushPop )
{
g_pTestThreadPool->ResumeExecution();
}
if ( bFinishExecute && g_iSleep <= 1 )
{
g_done.Wait();
}
g_nTotalAtFinish = CCountJob::m_nCount;
timer.End();
g_pTestThreadPool->SuspendExecution();
suspendTimer.End();
g_pTestThreadPool->ResumeExecution();
g_pTestThreadPool->Stop();
g_done.Reset();
int counts[8] = { 0 };
for ( int j = 0; j < ARRAYSIZE(jobs); j++ )
{
if ( jobs[j].GetServiceThread() != -1 )
{
counts[jobs[j].GetServiceThread()]++;
jobs[j].ClearServiceThread();
}
}
Msg( "ThreadPoolTest: %d threads -- %d (%d) jobs processed in %fms, %fms to suspend (%f/%f) [%d, %d, %d, %d, %d, %d, %d, %d]\n",
i, g_nTotalAtFinish, (int)CCountJob::m_nCount, timer.GetDuration().GetMillisecondsF(), suspendTimer.GetDuration().GetMillisecondsF() - timer.GetDuration().GetMillisecondsF(),
timer.GetDuration().GetMillisecondsF() / (float)CCountJob::m_nCount, (suspendTimer.GetDuration().GetMillisecondsF())/(float)g_nTotalAtFinish,
counts[0], counts[1], counts[2], counts[3], counts[4], counts[5], counts[6], counts[7] );
}
}
}
}
bool g_bOutputError;
volatile int g_ReadyToExecute;
CInterlockedInt g_nReady;
class CExecuteTestJob : public CJob
{
public:
virtual JobStatus_t DoExecute()
{
byte pMemory[1024];
int i;
for ( i = 0; i < 1024; i++ )
{
pMemory[i] = rand();
}
for ( i = 0; i < 50; i++ )
{
sqrt( (float)HashBlock( pMemory, 1024 ) + HashBlock( pMemory, 1024 ) + 10.0 );
}
if ( AccessEvent()->Check() || IsFinished() )
{
if ( !g_bOutputError )
{
Msg( "Forced execute test failed!\n" );
DebuggerBreakIfDebugging();
}
}
return 0;
}
};
class CExecuteTestExecuteJob : public CJob
{
public:
virtual JobStatus_t DoExecute()
{
bool bAbort = ( RandomInt( 1, 10 ) == 1 );
g_nReady++;
while ( !g_ReadyToExecute )
{
ThreadPause();
}
if ( !bAbort )
m_pTestJob->Execute();
else
m_pTestJob->Abort();
g_nReady--;
return 0;
}
CExecuteTestJob *m_pTestJob;
};
void TestForcedExecute()
{
Msg( "TestForcedExecute\n" );
for ( int tests = 0; tests < 30; tests++ )
{
for ( int i = 1; i <= 5; i += 2 )
{
g_nReady = 0;
ThreadPoolStartParams_t params;
params.nThreads = i;
params.fDistribute = TRS_TRUE;
g_pTestThreadPool->Start( params, "Tst" );
static CExecuteTestJob jobs[4000];
for ( int j = 0; j < ARRAYSIZE(jobs); j++ )
{
g_ReadyToExecute = false;
for ( int k = 0; k < i; k++ )
{
CExecuteTestExecuteJob *pJob = new CExecuteTestExecuteJob;
pJob->SetFlags( JF_QUEUE );
pJob->m_pTestJob = &jobs[j];
g_pTestThreadPool->AddJob( pJob );
pJob->Release();
}
while ( g_nReady < i )
{
ThreadPause();
}
g_ReadyToExecute = true;
ThreadSleep();
jobs[j].Execute();
while ( g_nReady > 0 )
{
ThreadPause();
}
}
g_pTestThreadPool->Stop();
}
}
Msg( "TestForcedExecute DONE\n" );
}
} // namespace ThreadPoolTest
void RunThreadPoolTests()
{
CThreadPool pool;
ThreadPoolTest::g_pTestThreadPool = &pool;
RunTSQueueTests(10000);
RunTSListTests(10000);
#ifdef _WIN32
DWORD_PTR mask1 = 0;
--mask1;
DWORD_PTR mask2 = 0;
--mask2;
GetProcessAffinityMask( GetCurrentProcess(), &mask1, &mask2 );
#else
int32 mask1=-1;
#endif
Msg( "ThreadPoolTest: Job distribution speed\n" );
for ( int i = 0; i < 2; i++ )
{
bool bToCompletion = ( i % 2 != 0 );
if ( !IsX360() )
{
Msg( "ThreadPoolTest: Non-distribute\n" );
ThreadPoolTest::Test( false, true, bToCompletion );
}
Msg( "ThreadPoolTest: Distribute\n" );
ThreadPoolTest::Test( true, true, bToCompletion );
Msg( "ThreadPoolTest: One core\n" );
ThreadSetAffinity( 0, 1 );
ThreadPoolTest::Test( false, true, bToCompletion );
ThreadSetAffinity( 0, mask1 );
Msg( "ThreadPoolTest: NO Sleep\n" );
ThreadPoolTest::Test( false, false, bToCompletion );
Msg( "ThreadPoolTest: Distribute\n" );
ThreadPoolTest::Test( true, false, bToCompletion );
Msg( "ThreadPoolTest: One core\n" );
ThreadSetAffinity( 0, 1 );
ThreadPoolTest::Test( false, false, bToCompletion );
ThreadSetAffinity( 0, mask1 );
}
Msg( "ThreadPoolTest: Jobs doing work\n" );
for ( int i = 0; i < 2; i++ )
{
bool bToCompletion = true;// = ( i % 2 != 0 );
if ( !IsX360() )
{
Msg( "ThreadPoolTest: Non-distribute\n" );
ThreadPoolTest::Test( false, true, bToCompletion, true );
}
Msg( "ThreadPoolTest: Distribute\n" );
ThreadPoolTest::Test( true, true, bToCompletion, true );
Msg( "ThreadPoolTest: One core\n" );
ThreadSetAffinity( 0, 1 );
ThreadPoolTest::Test( false, true, bToCompletion, true );
ThreadSetAffinity( 0, mask1 );
Msg( "ThreadPoolTest: NO Sleep\n" );
ThreadPoolTest::Test( false, false, bToCompletion, true );
Msg( "ThreadPoolTest: Distribute\n" );
ThreadPoolTest::Test( true, false, bToCompletion, true );
Msg( "ThreadPoolTest: One core\n" );
ThreadSetAffinity( 0, 1 );
ThreadPoolTest::Test( false, false, bToCompletion, true );
ThreadSetAffinity( 0, mask1 );
}
#ifdef _WIN32
GetProcessAffinityMask( GetCurrentProcess(), &mask1, &mask2 );
#endif
ThreadPoolTest::TestForcedExecute();
}