// NeL - MMORPG Framework
// Copyright (C) 2010 Winch Gate Property Limited
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
#include "stdmisc.h"
#include "nel/misc/co_task.h"
#include "nel/misc/tds.h"
#include "nel/misc/time_nl.h"
// Flag to use thread instead of coroutine primitives (i.e windows fibers or gcc context)
#ifndef NL_OS_WINDOWS
#define NL_USE_THREAD_COTASK
#endif
// flag to activate debug message
//#define NL_GEN_DEBUG_MSG
#ifdef NL_GEN_DEBUG_MSG
#define NL_CT_DEBUG nldebug
#else
#define NL_CT_DEBUG while(0)nldebug
#endif
#if defined(NL_USE_THREAD_COTASK)
#ifndef __GNUC__
#pragma message(NL_LOC_MSG "Using threaded coroutine")
#endif
# include "nel/misc/thread.h"
#else //NL_USE_THREAD_COTASK
// some platform specifics
#if defined (NL_OS_WINDOWS)
//# define _WIN32_WINNT 0x0500
# define NL_WIN_CALLBACK CALLBACK
// Visual .NET won't allow Fibers for a Windows version older than 2000. However the basic features are sufficient for us, we want to compile them for all Windows >= 95
# if !defined(_WIN32_WINNT) || (_WIN32_WINNT < 0x0400)
# ifdef _WIN32_WINNT
# undef _WIN32_WINNT
# endif
# define _WIN32_WINNT 0x0400
# endif
# define NOMINMAX
# include
#elif defined (NL_OS_UNIX)
# define NL_WIN_CALLBACK
# include
#else
# error "Coroutine task are not supported yet by your platform, do it ?"
#endif
#endif //NL_USE_THREAD_COTASK
namespace NLMISC
{
// platform specific data
#if defined(NL_USE_THREAD_COTASK)
struct TCoTaskData : public IRunnable
#else //NL_USE_THREAD_COTASK
struct TCoTaskData
#endif //NL_USE_THREAD_COTASK
{
#if defined(NL_USE_THREAD_COTASK)
/// The thread id for the co task
// TThreadId *_TaskThreadId;
/// The parent thread id
// TThreadId *_ParentThreadId;
// the thread of the task
IThread *_TaskThread;
/// The mutex of the task task
CFastMutex _TaskMutex;
CCoTask *_CoTask;
// set by master, cleared by task
volatile bool _ResumeTask;
// set by task, cleared by master
volatile bool _TaskHasYield;
TCoTaskData(CCoTask *task)
: _TaskThread(NULL),
_CoTask(task),
_ResumeTask(false),
_TaskHasYield(false)
{
}
virtual ~TCoTaskData()
{
NL_CT_DEBUG("CoTaskData : ~TCoTaskData %p : deleting cotask data", this);
if (_TaskThread != NULL)
{
NL_CT_DEBUG("CoTask : ~TCoTaskData (%p) waiting for thread termination", this);
// waiting for thread to terminate
_TaskThread->wait();
delete _TaskThread;
_TaskThread = NULL;
}
}
void run();
#else //NL_USE_THREAD_COTASK
#if defined (NL_OS_WINDOWS)
/// The fiber pointer for the task fiber
LPVOID _Fiber;
/// The fiber pointer of the main (or master, or parent, as you want)
LPVOID _ParentFiber;
#elif defined (NL_OS_UNIX)
/// The coroutine stack pointer (allocated memory)
uint8 *_Stack;
/// The task context
ucontext_t _Ctx;
/// The main (or master or parent, as you want) task context
ucontext_t _ParentCtx;
#endif
#endif //NL_USE_THREAD_COTASK
#if !defined(NL_USE_THREAD_COTASK)
/** task bootstrap function
* NB : this function is in this structure because of the
* NL_WIN_CALLBACK symbol that need to be defined, so
* to remove it from the header, I moved the function here
* (otherwise, it should be declared in the CCoTask class as
* a private member)
*/
static void NL_WIN_CALLBACK startFunc(void* param)
{
CCoTask *task = reinterpret_cast(param);
NL_CT_DEBUG("CoTask : task %p start func called", task);
try
{
// run the task
task->run();
}
catch(...)
{
nlwarning("CCoTask::startFunc : the task has generated an unhandled exeption and will terminate");
}
task->_Finished = true;
NL_CT_DEBUG("CoTask : task %p finished, entering infinite yield loop (waiting destruction)", task);
// nothing more to do
for (;;)
// return to parent task
task->yield();
}
#endif //NL_USE_THREAD_COTASK
};
/** Management of current task in a thread.
* This class is used to store and retrieve the current
* CCoTask pointer in the current thread.
* It is build upon the SAFE_SINGLETON paradigm, making it
* safe to use with NeL DLL.
* For windows platform, this singleton also hold the
* fiber pointer of the current thread. This is needed because
* of the bad design of the fiber API before Windows XP.
*/
class CCurrentCoTask
{
NLMISC_SAFE_SINGLETON_DECL(CCurrentCoTask);
/// A thread dependent storage to hold by thread coroutine info
CTDS _CurrentTaskTDS;
#if defined (NL_OS_WINDOWS)
/// A Thread dependent storage to hold fiber pointer.
CTDS _ThreadMainFiber;
#endif
CCurrentCoTask()
{}
public:
/// Set the current task for the calling thread
void setCurrentTask(CCoTask *task)
{
NL_CT_DEBUG("CoTask : setting current co task to %p", task);
_CurrentTaskTDS.setPointer(task);
}
/// retrieve the current task for the calling thread
CCoTask *getCurrentTask()
{
return reinterpret_cast(_CurrentTaskTDS.getPointer());
}
#if defined (NL_OS_WINDOWS) && !defined(NL_USE_THREAD_COTASK)
void setMainFiber(LPVOID fiber)
{
_ThreadMainFiber.setPointer(fiber);
}
/** Return the main fiber for the calling thread. Return NULL if
* the thread has not been converted to fiber.
*/
LPVOID getMainFiber()
{
return _ThreadMainFiber.getPointer();
}
#endif
};
NLMISC_SAFE_SINGLETON_IMPL(CCurrentCoTask);
CCoTask *CCoTask::getCurrentTask()
{
return CCurrentCoTask::getInstance().getCurrentTask();
}
CCoTask::CCoTask(uint stackSize)
: _Started(false),
_TerminationRequested(false),
_Finished(false)
{
NL_CT_DEBUG("CoTask : creating task %p", this);
#if defined(NL_USE_THREAD_COTASK)
// allocate platform specific data storage
_PImpl = new TCoTaskData(this);
// _PImpl->_TaskThreadId = 0;
// _PImpl->_ParentThreadId = 0;
#else //NL_USE_THREAD_COTASK
// allocate platform specific data storage
_PImpl = new TCoTaskData;
#if defined (NL_OS_WINDOWS)
_PImpl->_Fiber = NULL;
_PImpl->_ParentFiber = NULL;
#elif defined(NL_OS_UNIX)
// allocate the stack
_PImpl->_Stack = new uint8[stackSize];
#endif
#endif //NL_USE_THREAD_COTASK
}
CCoTask::~CCoTask()
{
NL_CT_DEBUG("CoTask : deleting task %p", this);
_TerminationRequested = true;
if (_Started)
{
while (!_Finished)
resume();
}
#if defined(NL_USE_THREAD_COTASK)
#else //NL_USE_THREAD_COTASK
#if defined (NL_OS_WINDOWS)
if (_PImpl->_Fiber)
{
DeleteFiber(_PImpl->_Fiber);
}
#elif defined(NL_OS_UNIX)
// free the stack
delete [] _PImpl->_Stack;
#endif
#endif //NL_USE_THREAD_COTASK
// free platform specific storage
delete _PImpl;
}
void CCoTask::start()
{
NL_CT_DEBUG("CoTask : Starting task %p", this);
nlassert(!_Started);
_Started = true;
#if defined(NL_USE_THREAD_COTASK)
// create the thread
_PImpl->_TaskThread = IThread::create(_PImpl);
NL_CT_DEBUG("CoTask : start() task %p entering mutex", this);
// get the mutex
_PImpl->_TaskMutex.enter();
NL_CT_DEBUG("CoTask : start() task %p mutex entered", this);
// set the resume flag to true
_PImpl->_ResumeTask = true;
// start the thread
_PImpl->_TaskThread->start();
NL_CT_DEBUG("CoTask : start() task %p leaving mutex", this);
// leave the mutex
_PImpl->_TaskMutex.leave();
// wait until the task has yield
for (;;)
{
// give up the time slice to the co task
nlSleep(0);
NL_CT_DEBUG("CoTask : start() task %p entering mutex", this);
// get the mutex
_PImpl->_TaskMutex.enter();
NL_CT_DEBUG("CoTask : start() task %p mutex entered", this);
if (!_PImpl->_TaskHasYield)
{
// not finished
NL_CT_DEBUG("CoTask : start() task %p has not yield, leaving mutex", this);
// leave the mutex
_PImpl->_TaskMutex.leave();
}
else
{
break;
}
}
// clear the yield flag
_PImpl->_TaskHasYield = false;
NL_CT_DEBUG("CoTask : start() task %p has yield", this);
// in the treaded mode, there is no need to call resume() inside start()
#else //NL_USE_THREAD_COTASK
#if defined (NL_OS_WINDOWS)
LPVOID mainFiber = CCurrentCoTask::getInstance().getMainFiber();
if (mainFiber == NULL)
{
// we need to convert this thread to a fiber
mainFiber = ConvertThreadToFiber(NULL);
if (mainFiber == NULL)
{
DWORD dw = GetLastError();
#if defined(ERROR_ALREADY_FIBER)
if (dw == ERROR_ALREADY_FIBER) nlerror("ConvertThreadToFiber ERROR_ALREADY_FIBER: "
"If you are using nel in dynamic libraries, you should have a 'pure "
"nel library' entry point, see definition of NLMISC_DECL_PURE_LIB");
else
#endif
nlerror("ConvertThreadToFiber error %u", dw);
}
CCurrentCoTask::getInstance().setMainFiber(mainFiber);
}
_PImpl->_ParentFiber = mainFiber;
_PImpl->_Fiber = CreateFiber(NL_TASK_STACK_SIZE, TCoTaskData::startFunc, this);
nlassert(_PImpl->_Fiber != NULL);
#elif defined (NL_OS_UNIX)
// store the parent ctx
nlverify(getcontext(&_PImpl->_ParentCtx) == 0);
// build the task context
nlverify(getcontext(&_PImpl->_Ctx) == 0);
// change the task context
_PImpl->_Ctx.uc_stack.ss_sp = _PImpl->_Stack;
_PImpl->_Ctx.uc_stack.ss_size = NL_TASK_STACK_SIZE;
_PImpl->_Ctx.uc_link = NULL;
_PImpl->_Ctx.uc_stack.ss_flags = 0;
makecontext(&_PImpl->_Ctx, reinterpret_cast(TCoTaskData::startFunc), 1, this);
#endif
resume();
#endif //NL_USE_THREAD_COTASK
}
void CCoTask::yield()
{
NL_CT_DEBUG("CoTask : task %p yield", this);
nlassert(_Started);
nlassert(CCurrentCoTask::getInstance().getCurrentTask() == this);
#if defined(NL_USE_THREAD_COTASK)
// set the yield flag
_PImpl->_TaskHasYield = true;
// release the mutex
NL_CT_DEBUG("CoTask : yield() task %p leaving mutex", this);
_PImpl->_TaskMutex.leave();
// now, wait until the resume flag is set
for (;;)
{
// give up the time slice to the master thread
nlSleep(0);
// And get back the mutex for waiting for next resume (this should lock)
NL_CT_DEBUG("CoTask : yield() task %p entering mutex", this);
_PImpl->_TaskMutex.enter();
NL_CT_DEBUG("CoTask : yield() task %p mutex entered", this);
if (!_PImpl->_ResumeTask)
{
// not time to resume, release the mutex and sleep
NL_CT_DEBUG("CoTask : yield() task %p not time to resume, leaving mutex", this);
_PImpl->_TaskMutex.leave();
// nlSleep(0);
}
else
break;
}
// clear the resume flag
_PImpl->_ResumeTask = false;
#else //NL_USE_THREAD_COTASK
CCurrentCoTask::getInstance().setCurrentTask(NULL);
#if defined (NL_OS_WINDOWS)
SwitchToFiber(_PImpl->_ParentFiber);
#elif defined (NL_OS_UNIX)
// swap to the parent context
nlverify(swapcontext(&_PImpl->_Ctx, &_PImpl->_ParentCtx) == 0);
#endif
#endif //NL_USE_THREAD_COTASK
NL_CT_DEBUG("CoTask : task %p have been resumed", this);
}
void CCoTask::resume()
{
NL_CT_DEBUG("CoTask : resuming task %p", this);
nlassert(CCurrentCoTask::getInstance().getCurrentTask() != this);
if (!_Started)
start();
else if (!_Finished)
{
nlassert(_Started);
#if defined(NL_USE_THREAD_COTASK)
// set the resume flag to true
_PImpl->_ResumeTask = true;
_PImpl->_TaskHasYield = false;
// Release the mutex
NL_CT_DEBUG("CoTask : resume() task %p leaving mutex", this);
_PImpl->_TaskMutex.leave();
// wait that the task has started
while (_PImpl->_ResumeTask)
nlSleep(0);
NL_CT_DEBUG("CoTask : resume() task %p is started, waiting yield", this);
// ok the task has started
// now wait for task to yield
for (;;)
{
// give up the time slice to the co task
nlSleep(0);
// acquire the mutex
NL_CT_DEBUG("CoTask : resume() task %p entering mutex", this);
_PImpl->_TaskMutex.enter();
NL_CT_DEBUG("CoTask : resume() task %p mutex entered", this);
if (!_PImpl->_TaskHasYield)
{
NL_CT_DEBUG("CoTask : resume() task %p still not yielding, leaving mutex", this);
_PImpl->_TaskMutex.leave();
// give the focus to another thread before acquiring the mutex
// nlSleep(0);
}
else
{
// the task has yield
break;
}
}
// clear the yield flag
_PImpl->_TaskHasYield = false;
#else // NL_USE_THREAD_COTASK
CCurrentCoTask::getInstance().setCurrentTask(this);
#if defined (NL_OS_WINDOWS)
SwitchToFiber(_PImpl->_Fiber);
#elif defined (NL_OS_UNIX)
// swap to the parent context
nlverify(swapcontext(&_PImpl->_ParentCtx, &_PImpl->_Ctx) == 0);
#endif
#endif //NL_USE_THREAD_COTASK
}
NL_CT_DEBUG("CoTask : task %p has yield", this);
}
/// wait until the task terminate
void CCoTask::wait()
{
NL_CT_DEBUG("CoTask : waiting for task %p to terminate", this);
// resume the task until termination
while (!_Finished)
resume();
}
#if defined(NL_USE_THREAD_COTASK)
void TCoTaskData::run()
{
NL_CT_DEBUG("CoTask : entering TCoTaskData::run for task %p", _CoTask);
// set the current task
CCurrentCoTask::getInstance().setCurrentTask(_CoTask);
// Set the task as running
// _Running = true;
NL_CT_DEBUG("CoTask : TCoTaskData::run() task %p entering mutex", this);
// Acquire the task mutex
_TaskMutex.enter();
NL_CT_DEBUG("CoTask : TCoTaskData::run mutex aquired, calling '_CoTask->run()' for task %p", _CoTask);
// clear the resume flag
_CoTask->_PImpl->_ResumeTask = false;
// run the task
_CoTask->run();
// mark the task has yielding
_CoTask->_PImpl->_TaskHasYield = true;
// mark the task has finished
_CoTask->_Finished = true;
// nothing more to do, just return to terminate the thread
NL_CT_DEBUG("CoTask : leaving TCoTaskData::run for task %p", _CoTask);
NL_CT_DEBUG("CoTask : TCoTaskData::run() task %p leaving mutex", this);
// Release the parent mutex
_TaskMutex.leave();
}
#endif //NL_USE_THREAD_COTASK
void CCoTask::requestTerminate()
{
_TerminationRequested = true;
}
void CCoTask::sleep(uint milliseconds)
{
nlassert(getCurrentTask() == this); // called outside run() !
TTime startTime = CTime::getLocalTime();
while(!isTerminationRequested())
{
TTime currTime = CTime::getLocalTime();
if (currTime - startTime >= milliseconds) break;
yield();
}
}
} // namespace NLMISC