Leaked source code of windows server 2003
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

572 lines
22 KiB

/******************************************************************************
*
* Copyright (C) 2001-2002 Microsoft Corporation. All Rights Reserved.
*
* File: work.h
*
* Content: DirectPlay Thread Pool work processing functions header file.
*
* History:
* Date By Reason
* ======== ======== =========
* 10/31/01 VanceO Created.
*
******************************************************************************/
#ifndef __WORK_H__
#define __WORK_H__
//=============================================================================
// Defines
//=============================================================================
#ifdef DPNBUILD_THREADPOOLSTATISTICS
#define MAX_TRACKED_CALLBACKSTATS 15 // maximum number of unique work callback functions to track
#endif // DPNBUILD_THREADPOOLSTATISTICS
//=============================================================================
// Forward declarations
//=============================================================================
typedef struct _DPTHREADPOOLOBJECT DPTHREADPOOLOBJECT;
//=============================================================================
// Structures
//=============================================================================
#ifdef DPNBUILD_THREADPOOLSTATISTICS
typedef struct _CALLBACKSTATS
{
PFNDPTNWORKCALLBACK pfnWorkCallback; // pointer to work callback function whose stats are being tracked
DWORD dwNumCreates; // number of times a work item with this callback was created
DWORD dwTotalCompletionTime; // total time from creation to completion for all I/O operations using this callback, total time from setting to firing for all timers using this callback
DWORD dwNumQueues; // number of times a work item with this callback was queued for completion
DWORD dwTotalQueueTime; // total time from queuing to callback execution for all work items using this callback
DWORD dwNumCalls; // number of times the callback was invoked
DWORD dwTotalCallbackTime; // total time spent in the callback for all work items using this callback that did not reschedule
DWORD dwNumNotRescheduled; // number of times the callback returned without rescheduling
} CALLBACKSTATS, * PCALLBACKSTATS;
#endif // DPNBUILD_THREADPOOLSTATISTICS
typedef struct _DPTPWORKQUEUE
{
//
// NOTE: NBQueueBlockInitial must be heap aligned, so it is first in the
// structure.
//
#ifndef DPNBUILD_USEIOCOMPLETIONPORTS
DNNBQUEUE_BLOCK NBQueueBlockInitial; // initial tracking info for the work queue or free node list (cast as DNSLIST_ENTRY for the latter) required by NB Queue implementation
#endif // ! DPNBUILD_USEIOCOMPLETIONPORTS
BYTE Sig[4]; // debugging signature ('WRKQ')
//
// Volatile data that can get tweaked simultaneously by multiple threads
//
CFixedPool * pWorkItemPool; // (work) pool of currently unused work items
#ifndef DPNBUILD_USEIOCOMPLETIONPORTS
DNSLIST_HEADER SlistFreeQueueNodes; // (work) pool of nodes used to track work items in non-blocking queue (because of Slist implementation, it can only hold sizeof(WORD) == 65,535 entries)
PVOID pvNBQueueWorkItems; // (work) header for list of work items needing execution (because of Slist implementation, it can only hold sizeof(WORD) == 65,535 entries)
#endif // ! DPNBUILD_USEIOCOMPLETIONPORTS
#ifndef DPNBUILD_ONLYONETHREAD
BOOL fTimerThreadNeeded; // (work) boolean indicated whether a worker thread is currently needed as a timer thread (there can be only one)
DWORD dwNumThreadsExpected; // (work) number of threads threads currently starting up/shutting down
DWORD dwNumBusyThreads; // (work) number of threads that are currently processing work items
DWORD dwNumRunningThreads; // (work) number of threads that are currently running
#endif // ! DPNBUILD_ONLYONETHREAD
DNSLIST_HEADER * paSlistTimerBuckets; // (timer) pointer to array of list headers for the timer buckets (because of Slist implementation, each bucket can only hold sizeof(WORD) == 65,535 entries, but that should be plenty)
#if ((! defined(DPNBUILD_DONTCHECKFORMISSEDTIMERS)) && (! defined(DPNBUILD_NOMISSEDTIMERSHINT)))
DWORD dwPossibleMissedTimerWindow; // (timer) cumulative hint to timer thread about short timers that were possibly missed
#endif // ! DPNBUILD_DONTCHECKFORMISSEDTIMERS and ! DPNBUILD_NOMISSEDTIMERSHINT
DNSLIST_HEADER SlistOutstandingIO; // (I/O) header for list of outstanding I/O waiting for completion (because of Slist implementation, it can only hold sizeof(WORD) == 65,535 entries)
//
// Regularly updated data, but it should only get modified by the one timer
// thread.
//
DWORD dwLastTimerProcessTime; // (timer) when we last handled timer entries
//
// "Constant" data that is read-only for all threads
//
#if ((! defined(DPNBUILD_ONLYONETHREAD)) && ((! defined(WINCE)) || (defined(DBG))))
DNCRITICAL_SECTION csListLock; // (work) lock protecting list of tracked handles (and list of threads owned by this work queue in debug)
#endif // ! DPNBUILD_ONLYONETHREAD and (! WINCE or DBG)
#ifndef DPNBUILD_ONLYONEPROCESSOR
DWORD dwCPUNum; // (work) the CPU number this queue represents
#endif // ! DPNBUILD_ONLYONEPROCESSOR
#ifdef DPNBUILD_USEIOCOMPLETIONPORTS
DNHANDLE hIoCompletionPort; // (work) I/O completion port used to track I/O and queue work items
#else // ! DPNBUILD_USEIOCOMPLETIONPORTS
DNHANDLE hAlertEvent; // (work) handle to the event used to wake up idle worker threads
#endif // ! DPNBUILD_USEIOCOMPLETIONPORTS
#ifndef DPNBUILD_ONLYONETHREAD
DNHANDLE hExpectedThreadsEvent; // (work) temporary handle to the event to be set when the desired number of threads are started/stopped
PFNDPNMESSAGEHANDLER pfnMsgHandler; // (work) user's message handler function, or NULL if none.
PVOID pvMsgHandlerContext; // (work) user's context for message handler function
DWORD dwWorkerThreadTlsIndex; // (work) Thread Local Storage index for storing the worker thread data
#endif // ! DPNBUILD_ONLYONETHREAD
CBilink blTrackedFiles; // (I/O) doubly linked list holding all files tracked by this CPU, protected by this work queue's list lock
#ifdef DPNBUILD_DYNAMICTIMERSETTINGS
DWORD dwTimerBucketGranularity; // (timer) the granularity in ms for each timer bucket
DWORD dwTimerBucketGranularityCeiling; // (timer) precalculated addend used when rounding time stamps up to the appropriate granularity, it also happens to be the module mask, but its currently never used that way
DWORD dwTimerBucketGranularityFloorMask; // (timer) precalculated pseudo-modulo bit mask for rounding down time stamps to the appropriate granularity
DWORD dwTimerBucketGranularityDivisor; // (timer) precalculated pseudo-divisor bit shift for converting time into buckets
DWORD dwNumTimerBuckets; // (timer) the number of timer buckets in the array
DWORD dwNumTimerBucketsModMask; // (timer) precalculated pseudo-modulo bit mask for wrapping around the array
#endif // DPNBUILD_DYNAMICTIMERSETTINGS
#if ((defined(WINNT)) || ((defined(WIN95)) && (! defined(DPNBUILD_NOWAITABLETIMERSON9X))))
DNHANDLE hTimer; // (timer) handle to the waitable timer object used to wake up a worker thread periodically
#endif // WINNT or (WIN95 AND ! DPNBUILD_NOWAITABLETIMERSON9X)
#ifdef DPNBUILD_THREADPOOLSTATISTICS
//
// Debugging/tuning statistics.
//
DWORD dwTotalNumWorkItems; // (work) total number of work items placed in this queue
#ifndef WINCE
DWORD dwTotalTimeSpentUnsignalled; // (work) total number of milliseconds spent waiting for the alert event to be fired
DWORD dwTotalTimeSpentInWorkCallbacks; // (work) total number of milliseconds spent in work item callbacks
#endif // ! WINCE
#ifndef DPNBUILD_ONLYONETHREAD
DWORD dwTotalNumTimerThreadAbdications; // (work) total number of times the existing timer thread allowed another thread to become the timer thread
#endif // ! DPNBUILD_ONLYONETHREAD
DWORD dwTotalNumWakesWithoutWork; // (work) total number of times a worker thread woke up but found nothing to do
DWORD dwTotalNumContinuousWork; // (work) total number of times a thread found another work item after completing a previous one
DWORD dwTotalNumDoWorks; // (work) total number of times DoWork was called
DWORD dwTotalNumDoWorksTimeLimit; // (work) total number of times DoWork stopped looping due to the time limit
DWORD dwTotalNumSimultaneousQueues; // (work) total number of times more than one work item was queued at the same time
CALLBACKSTATS aCallbackStats[MAX_TRACKED_CALLBACKSTATS]; // (work) array of stats for tracked callbacks
DWORD dwTotalNumTimerChecks; // (timer) total number of times any expired timer buckets have been handled
DWORD dwTotalNumBucketsProcessed; // (timer) total number of timer buckets that have been checked
DWORD dwTotalNumTimersScheduled; // (timer) total number of timers that were scheduled
DWORD dwTotalNumLongTimersRescheduled; // (timer) total number of long timers that were rescheduled back into a bucket
DWORD dwTotalNumSuccessfulCancels; // (timer) total number of CancelTimer calls that succeeded
DWORD dwTotalNumFailedCancels; // (timer) total number of CancelTimer calls that failed
#if ((! defined(DPNBUILD_DONTCHECKFORMISSEDTIMERS)) && (! defined(DPNBUILD_NOMISSEDTIMERSHINT)))
DWORD dwTotalPossibleMissedTimerWindows; // (timer) total of all hints to timer thread about possibly missed short timers
#endif // ! DPNBUILD_DONTCHECKFORMISSEDTIMERS and ! DPNBUILD_NOMISSEDTIMERSHINT
#endif // DPNBUILD_THREADPOOLSTATISTICS
#ifdef DBG
#ifndef DPNBUILD_ONLYONETHREAD
//
// Structures helpful for debugging.
//
CBilink blThreadList; // (work) list of all threads owned by this work queue, protected by this work queue's list lock
#endif // ! DPNBUILD_ONLYONETHREAD
#endif // DBG
} DPTPWORKQUEUE, * PDPTPWORKQUEUE;
#ifndef DPNBUILD_ONLYONETHREAD
typedef struct _DPTPWORKERTHREAD
{
BYTE Sig[4]; // debugging signature ('WKTD')
DPTPWORKQUEUE * pWorkQueue; // owning work queue
DWORD dwRecursionCount; // recursion count
BOOL fThreadIndicated; // whether CREATE_THREAD has returned and DESTROY_THREAD has not been started yet
#ifdef DBG
DWORD dwThreadID; // ID of thread
DWORD dwMaxRecursionCount; // maximum recursion count over life of thread
CBilink blList; // entry in work queue list of threads
#endif // DBG
} DPTPWORKERTHREAD, * PDPTPWORKERTHREAD;
#ifdef DPNBUILD_MANDATORYTHREADS
typedef struct _DPTPMANDATORYTHREAD
{
BYTE Sig[4]; // debugging signature ('MNDT')
DPTHREADPOOLOBJECT * pDPTPObject; // owning thread pool object
DNHANDLE hStartedEvent; // handle of event to set when thread has successfully started
PFNDPNMESSAGEHANDLER pfnMsgHandler; // user's message handler function, or NULL if none.
PVOID pvMsgHandlerContext; // user's context for message handler function
LPTHREAD_START_ROUTINE lpStartAddress; // user start address for thread
LPVOID lpParameter; // user parameter for thread
#ifdef DBG
DWORD dwThreadID; // ID of thread
CBilink blList; // entry in work queue list of threads
#endif // DBG
} DPTPMANDATORYTHREAD, * PDPTPMANDATORYTHREAD;
#endif // DPNBUILD_MANDATORYTHREADS
#endif // ! DPNBUILD_ONLYONETHREAD
//=============================================================================
// Classes
//=============================================================================
//
// It is critical to keep in mind that parts of this class must remain as valid
// memory, even when the item is returned to the pool. Particularly, the NB
// Queue code will use m_NBQueueBlock to track work item objects other than
// the one whose member is used. Also, late timers use m_uiUniqueID to detect
// that they are late.
//
// Basically this means the code needs to be revisited if pooling of CWorkItems
// is turned off, or if the pool code is modified to be able to shrink the pool
// (unlike the growth-only mechanism used now).
//
class CWorkItem
{
public:
#undef DPF_MODNAME
#define DPF_MODNAME "CWorkItem::FPM_Alloc"
static BOOL FPM_Alloc(void * pvItem, void * pvContext)
{
CWorkItem * pWorkItem = (CWorkItem*) pvItem;
pWorkItem->m_Sig[0] = 'W';
pWorkItem->m_Sig[1] = 'O';
pWorkItem->m_Sig[2] = 'R';
pWorkItem->m_Sig[3] = 'K';
//
// Remember the owning work queue.
//
pWorkItem->m_pWorkQueue = (DPTPWORKQUEUE*) pvContext;
#ifdef DBG
memset(&pWorkItem->m_Overlapped, 0x10, sizeof(pWorkItem->m_Overlapped));
#endif // DBG
//
// We will start the unique ID sequence at 0, but it really doesn't
// matter. It's technically safe for it to be stack garbage since
// we only use it for comparison.
//
pWorkItem->m_uiUniqueID = 0;
#ifndef DPNBUILD_USEIOCOMPLETIONPORTS
//
// Throw the embedded DNNBQUEUE_BLOCK structure into the free list
// for the queue. Remember that it may be used to track work items
// other than this object.
//
DNInterlockedPushEntrySList(&((DPTPWORKQUEUE*) pvContext)->SlistFreeQueueNodes,
(DNSLIST_ENTRY*) (&pWorkItem->m_NBQueueBlock));
#endif // ! DPNBUILD_USEIOCOMPLETIONPORTS
return TRUE;
}
#undef DPF_MODNAME
#define DPF_MODNAME "CWorkItem::FPM_Get"
static void FPM_Get(void * pvItem, void * pvContext)
{
CWorkItem * pWorkItem = (CWorkItem*) pvItem;
#ifdef DBG
memset(&pWorkItem->m_Overlapped, 0, sizeof(pWorkItem->m_Overlapped));
DNASSERT(pWorkItem->m_pWorkQueue == (DPTPWORKQUEUE*) pvContext);
#endif // DBG
//
// Make sure the object is ready to be cancelled. Really really
// late cancel attempts on a previous instance should have hit the
// m_dwUniqueID check.
//
pWorkItem->m_fCancelledOrCompleting = FALSE;
}
#undef DPF_MODNAME
#define DPF_MODNAME "CWorkItem::FPM_Release"
static void FPM_Release(void * pvItem)
{
CWorkItem * pWorkItem = (CWorkItem*) pvItem;
//
// Change the unique ID so that future late cancellation attempts
// don't bother us. If the late attempt occurred before we do
// this, it should have hit the m_fCancelledOrCompleting check.
// And for non-timer work items, in debug builds we set
// m_fCancelledOrCompleting to TRUE before queueing so that this
// assert succeeds as well.
//
DNASSERT(pWorkItem->m_fCancelledOrCompleting);
pWorkItem->m_uiUniqueID++;
#ifdef DBG
memset(&pWorkItem->m_Overlapped, 0x10, sizeof(pWorkItem->m_Overlapped));
#endif // DBG
}
/*
#undef DPF_MODNAME
#define DPF_MODNAME "CWorkItem::FPM_Dealloc"
static void FPM_Dealloc(void * pvItem)
{
CWorkItem * pWorkItem = (CWorkItem*) pvItem;
}
*/
#ifdef DBG
BOOL IsValid(void)
{
if ((m_Sig[0] == 'W') &&
(m_Sig[1] == 'O') &&
(m_Sig[2] == 'R') &&
(m_Sig[3] == 'K'))
{
return TRUE;
}
return FALSE;
}
#endif // DBG
//
// Generic work item information.
//
// NOTE: m_SlistEntry and m_NBQueueBlock must be heap aligned.
//
union
{
DNSLIST_ENTRY m_SlistEntry; // tracking info for the timer bucket list
#ifndef DPNBUILD_USEIOCOMPLETIONPORTS
BYTE Alignment[16]; // alignment padding
#endif // ! DPNBUILD_USEIOCOMPLETIONPORTS
};
#ifndef DPNBUILD_USEIOCOMPLETIONPORTS
DNNBQUEUE_BLOCK m_NBQueueBlock; // tracking info for the work queue or free node list (cast as DNSLIST_ENTRY for the latter)
#endif // ! DPNBUILD_USEIOCOMPLETIONPORTS
BYTE m_Sig[4]; // debugging signature ('WORK')
DPTPWORKQUEUE * m_pWorkQueue; // pointer to owning work queue
PFNDPTNWORKCALLBACK m_pfnWorkCallback; // pointer to function that should be called to perform the work
PVOID m_pvCallbackContext; // pointer to context for performing the work
//
// I/O specific information.
//
OVERLAPPED m_Overlapped; // overlapped structure use to identify I/O operation to the OS
//
// Timer specific information.
//
DWORD m_dwDueTime; // expiration time for the work item
BOOL m_fCancelledOrCompleting; // boolean set to TRUE if timer should be cancelled or it's queued to be processed
UINT m_uiUniqueID; // continually incrementing identifier so the user can cancel the intended timer
#ifdef DPNBUILD_THREADPOOLSTATISTICS
DWORD m_dwCreationTime; // time when work item was retrieved from the pool
DWORD m_dwQueueTime; // time when work item was queued to be completed
DWORD m_dwCallbackTime; // time when work item callback function began executing
CALLBACKSTATS * m_pCallbackStats; // pointer to callback stats slot, or NULL if none
#endif // DPNBUILD_THREADPOOLSTATISTICS
};
//=============================================================================
// Inline thread pool statistics helper function implementations
//=============================================================================
inline void ThreadpoolStatsCreate(CWorkItem * const pWorkItem)
{
#ifdef DPNBUILD_THREADPOOLSTATISTICS
CALLBACKSTATS * pCallbackStats;
PFNDPTNWORKCALLBACK pfnWorkCallback;
//
// Loop through all callback stats slots looking for the first one that
// matches our callback or is NULL. If we don't find one, m_pCallbackStats
// will remain NULL.
//
pWorkItem->m_pCallbackStats = NULL;
pCallbackStats = pWorkItem->m_pWorkQueue->aCallbackStats;
while (pCallbackStats < &pWorkItem->m_pWorkQueue->aCallbackStats[MAX_TRACKED_CALLBACKSTATS])
{
//
// Retrieve this slot's current callback pointer. If it was NULL,
// we'll fill it with our callback pointer in the process.
//
pfnWorkCallback = (PFNDPTNWORKCALLBACK) DNInterlockedCompareExchangePointer((PVOID*) (&pCallbackStats->pfnWorkCallback),
pWorkItem->m_pfnWorkCallback,
NULL);
//
// If the callback was already ours, or it was NULL (and thus got set
// to ours), we've got a slot.
//
if ((pfnWorkCallback == pWorkItem->m_pfnWorkCallback) ||
(pfnWorkCallback == NULL))
{
pWorkItem->m_pCallbackStats = pCallbackStats;
DNInterlockedIncrement((LPLONG) (&pCallbackStats->dwNumCreates));
break;
}
//
// Move to next slot.
//
pCallbackStats++;
}
//
// Remember the creation time.
//
pWorkItem->m_dwCreationTime = GETTIMESTAMP();
#endif // DPNBUILD_THREADPOOLSTATISTICS
}
inline void ThreadpoolStatsQueue(CWorkItem * const pWorkItem)
{
#ifdef DPNBUILD_THREADPOOLSTATISTICS
//
// Remember the queue time.
//
pWorkItem->m_dwQueueTime = GETTIMESTAMP();
//
// If we have a callback stats slot, update the additional info.
//
if (pWorkItem->m_pCallbackStats != NULL)
{
DNInterlockedIncrement((LPLONG) (&pWorkItem->m_pCallbackStats->dwNumQueues));
#ifndef WINCE
DNInterlockedExchangeAdd((LPLONG) (&pWorkItem->m_pCallbackStats->dwTotalCompletionTime),
(pWorkItem->m_dwQueueTime - pWorkItem->m_dwCreationTime));
#endif // ! WINCE
}
#endif // DPNBUILD_THREADPOOLSTATISTICS
}
inline void ThreadpoolStatsBeginExecuting(CWorkItem * const pWorkItem)
{
#ifdef DPNBUILD_THREADPOOLSTATISTICS
//
// Remember when the callback began executing.
//
pWorkItem->m_dwCallbackTime = GETTIMESTAMP();
//
// If we have a callback stats slot, update the additional info.
//
if (pWorkItem->m_pCallbackStats != NULL)
{
DNInterlockedIncrement((LPLONG) (&pWorkItem->m_pCallbackStats->dwNumCalls));
#ifndef WINCE
DNInterlockedExchangeAdd((LPLONG) (&pWorkItem->m_pCallbackStats->dwTotalQueueTime),
(pWorkItem->m_dwCallbackTime - pWorkItem->m_dwQueueTime));
#endif // ! WINCE
}
#endif // DPNBUILD_THREADPOOLSTATISTICS
}
inline void ThreadpoolStatsEndExecuting(CWorkItem * const pWorkItem)
{
#ifdef DPNBUILD_THREADPOOLSTATISTICS
#ifndef WINCE
DWORD dwCallbackTime;
dwCallbackTime = GETTIMESTAMP() - pWorkItem->m_dwCallbackTime;
//
// Track global stats on how long callbacks take.
//
DNInterlockedExchangeAdd((LPLONG) (&pWorkItem->m_pWorkQueue->dwTotalTimeSpentInWorkCallbacks),
dwCallbackTime);
#endif // WINCE
//
// If we have a callback stats slot, update the additional info.
//
if (pWorkItem->m_pCallbackStats != NULL)
{
DNInterlockedIncrement((LPLONG) (&pWorkItem->m_pCallbackStats->dwNumNotRescheduled));
#ifndef WINCE
DNInterlockedExchangeAdd((LPLONG) (&pWorkItem->m_pCallbackStats->dwTotalCallbackTime),
dwCallbackTime);
#endif // ! WINCE
}
#endif // DPNBUILD_THREADPOOLSTATISTICS
}
inline void ThreadpoolStatsEndExecutingRescheduled(CWorkItem * const pWorkItem)
{
//
// Right now, we can't update any stats because we lost the work item pointer.
//
}
//=============================================================================
// Function prototypes
//=============================================================================
#ifdef DPNBUILD_ONLYONETHREAD
#ifdef DPNBUILD_ONLYONEPROCESSOR
HRESULT InitializeWorkQueue(DPTPWORKQUEUE * const pWorkQueue);
#else // ! DPNBUILD_ONLYONEPROCESSOR
HRESULT InitializeWorkQueue(DPTPWORKQUEUE * const pWorkQueue,
const DWORD dwCPUNum);
#endif // ! DPNBUILD_ONLYONEPROCESSOR
#else // ! DPNBUILD_ONLYONETHREAD
HRESULT InitializeWorkQueue(DPTPWORKQUEUE * const pWorkQueue,
#ifndef DPNBUILD_ONLYONEPROCESSOR
const DWORD dwCPUNum,
#endif // ! DPNBUILD_ONLYONEPROCESSOR
const PFNDPNMESSAGEHANDLER pfnMsgHandler,
PVOID const pvMsgHandlerContext,
const DWORD dwWorkerThreadTlsIndex);
#endif // ! DPNBUILD_ONLYONETHREAD
void DeinitializeWorkQueue(DPTPWORKQUEUE * const pWorkQueue);
BOOL QueueWorkItem(DPTPWORKQUEUE * const pWorkQueue,
const PFNDPTNWORKCALLBACK pfnWorkCallback,
PVOID const pvCallbackContext);
#ifndef DPNBUILD_ONLYONETHREAD
HRESULT StartThreads(DPTPWORKQUEUE * const pWorkQueue,
const DWORD dwNumThreads);
HRESULT StopThreads(DPTPWORKQUEUE * const pWorkQueue,
const DWORD dwNumThreads);
#endif // ! DPNBUILD_ONLYONETHREAD
void DoWork(DPTPWORKQUEUE * const pWorkQueue,
const DWORD dwMaxDoWorkTime);
#ifndef DPNBUILD_ONLYONETHREAD
DWORD WINAPI DPTPWorkerThreadProc(PVOID pvParameter);
#ifdef DPNBUILD_MANDATORYTHREADS
DWORD WINAPI DPTPMandatoryThreadProc(PVOID pvParameter);
#endif // DPNBUILD_MANDATORYTHREADS
void DPTPWorkerLoop(DPTPWORKQUEUE * const pWorkQueue);
#endif // ! DPNBUILD_ONLYONETHREAD
#endif // __WORK_H__