Game Development Community

Thread problems...

by Stefan Beffy Moises · in Torque Game Engine · 02/22/2007 (2:46 pm) · 6 replies

Hi, I am trying to get a "threaded scheduler" working, so that I can schedule functions in a new thread which runs independently of the engine... the reason why I need that is that the "schedule()" function isn't reliable enough to measure a user's reaction time... the time when it's executed varies between 10 and 200 ms... the threaded scheduler gives me values between 10 and 20 ms for the most part... BUT, the thread is causing weird side effects and random crashes and I cant find the problem... seems to be some memory leak or something, but I really dont know what may cause it... maybe anybody can spot a problem in the code... I tried to use the Theora player class as an example, but I must be missing something important here... :(
#include "platform/platform.h"
#include "console/simBase.h"
#include "console/consoleTypes.h"
#include "platform/platformThread.h"
#include "platform/platformMutex.h"
#include "core/safeDelete.h"

// thread class
class TickThread
{
private:
   Thread* mThread;
   S32 mStartTime;
	S32 mTickRate;
   S32 mArgc;
   char **mArgv;

public:
   TickThread();
   ~TickThread();
	void setTickRate(S32 newRate);
	void start();
	void stop();
   void setScheduleFunc(S32 argc, const char **argv);

   static void playThread(void *udata);
	void runThread();
};

TickThread::TickThread()
{
   mThread = NULL;
   mStartTime = 0;
	mTickRate = 1000;
   mArgc = -1;
   mArgv = NULL;
}

TickThread::~TickThread()
{
   if(mThread)
   {
      Con::errorf("*** thread cleanup...");
      SAFE_DELETE(mThread);
   }
   
   dFree(mArgv);
}

void TickThread::setTickRate(S32 newRate)
{
	mTickRate = newRate;
}

void TickThread::start()
{
   mStartTime = Platform::getRealMilliseconds();
   // spawn thread...
   mThread = new Thread((ThreadRunFunction)playThread, (S32) this, 1);
   Con::printf("*** Thread started ...");
}
void TickThread::stop()
{
   Con::errorf("*** TickThread stopping...");
   mStartTime = 0;
   if(mThread)
   {
      Con::errorf("*** deleting thread...");
      SAFE_DELETE(mThread);
   }
}

// set a function name and its args
void TickThread::setScheduleFunc(S32 argc, const char **argv)
{
   mArgc = argc;
   U32 totalSize = 0;
   S32 i;
   for(i = 0; i < argc; i++)
      totalSize += dStrlen(argv[i]) + 1;
   totalSize += sizeof(char *) * argc;
   
   mArgv = (char **) dMalloc(totalSize);
   char *argBase = (char *) &mArgv[argc];
   
   for(i = 0; i < argc; i++)
   {
      mArgv[i] = argBase;
      dStrcpy(mArgv[i], argv[i]);
      argBase += dStrlen(argv[i]) + 1;
   }
}

// thread function
void TickThread::playThread(void *udata)
{
   TickThread* pThis = (TickThread *)udata;
   pThis->runThread();
}


// thread loop
void TickThread::runThread()
{
   Con::printf("*** Thread running...");

   while(mStartTime > 0)
   {
      S32 currTime = Platform::getRealMilliseconds(); 
      if(currTime > (mStartTime + mTickRate))
      {
         Con::printf("*** Thread time over ...");
         // run any scheduled function
         if(mArgc > 0)
         {
            Con::errorf("RUNNING THREADED SCHEDULE FUNCTION: %i %s", mArgc, mArgv);
            Con::execute(mArgc, const_cast<const char**>( mArgv ));
            mArgc = -1;
         }
         // we are done         
         mStartTime = 0;
         break;
	   }
   }
}

#1
02/22/2007 (2:50 pm)
... continued:
// console class
class TickableScriptObject : public SimObject
{
   typedef SimObject Parent;

   void *mMutex;

public:

   TickThread mTickThread;

   TickableScriptObject();
   ~TickableScriptObject();

	bool onAdd();
	void onRemove();

   DECLARE_CONOBJECT(TickableScriptObject);
   static void initPersistFields();
};

IMPLEMENT_CONOBJECT(TickableScriptObject);

void TickableScriptObject::initPersistFields()
{
}

TickableScriptObject::TickableScriptObject()
{
   //mMutex = Mutex::createMutex();
}

TickableScriptObject::~TickableScriptObject()
{
   //Mutex::destroyMutex(mMutex);
}

bool TickableScriptObject::onAdd()
{
   if (!Parent::onAdd())
      return false;

   return true;
}

void TickableScriptObject::onRemove()
{
   Parent::onRemove();
}


//************************************
// CONSOLE METHODS
//************************************

ConsoleMethod(TickableScriptObject, threaded_schedule, void, 3, 0, "obj.threaded_schedule(time, command, <arg1...argN>)")
{
   S32 timeDelta = S32(dAtoi(argv[2]));
   // set function to call
   object->mTickThread.setScheduleFunc(argc - 3, argv + 3);
   
   // set time until call
   object->mTickThread.setTickRate(timeDelta);
   // start processing with thread
   object->mTickThread.start();
}
ConsoleMethod(TickableScriptObject, stop_schedule, void, 0, 0, "obj.stop_schedule()")
{
   // stop processing of thread
   object->mTickThread.stop();
}

I've tried to use the thread with the console class as well, and later changed it to use a separate class, but the problem remained... the stop_schedule() function was also added later, originally I was stopping it automatically after my function has been executed.
Here is how I use this from script:
new TickableScriptObject(iTick){
   tickRate = 1000;
};
iTick.threaded_schedule(%triggerReactionTime, checkIfNoReactionToPreviousStimulus, %obj);
...
iTick.stop_schedule();
#2
02/22/2007 (3:19 pm)
There's a few problems here, the biggest of which is that everything in the Con namespace ("Con::printf", etc.) is not thread-safe. Particularly "Con::execute".

I had written some other stuff, here, but decided it wasn't really that relevant to your question.

You probably want to post an event to a queue instead of Con::execute, but that sort of brings you back to square one, because that won't get handled until the main thread gets an opporunity to handle it, which gives you the exact same problem as "schedule".
#3
02/23/2007 (12:07 am)
Doh, of course, you are right! I tried using Sim::postEvent() now, and the memory leak is gone, but, as you said, I'm back to square one, since the time now isn't as constant as it should be... if I schedule an event in e.g. 400ms, the realtime (using getRealTime() in script before and after the function has been executed) varies between 407 and 578, the Simtime passed varies between 267 and 437ms... using Con::execute() the real time was almost constant (404 to 408ms)... but that causes those weird side effects and memory leaks... :(
Maybe I should try using ITickable again, but that gave me the same results as schedule() when I last tried... :(
I guess there just isn't a way to get a constant schedule.... :/ Thanks for your help though!
#4
02/23/2007 (8:57 am)
There is a way, just not in the manner you are using.

Currently, when a SimEvent is posted to the queue, the basic algorithm is this:

--Take the ms duration and multiply it by ticks/ms
--post the event to the queue at that tick time

Instead, if you want an "adjustable" SimEvent maturation time, implement the following:

--store the time (as opposed to the tick count) when the event should mature
--each tick, re-calculate the tick time to mature, and if it's past a certain threshold, unpost the event and repost it with an updated tick count

Of course, this is more performance intensive, but it will allow your events to manage themselves and repost as necessary if delays are causing them to get really ugly. You'll also of course probably want to implement some sort of event priority, since iirc the event queue is simply FIFO currently.
#5
02/23/2007 (2:04 pm)
Thx for your input, Stephen!
Hm, so you mean I should post the event with target time x and check on each tick (every 32ms) how much time has passed (using getRealtime()?) and if the realtime wasn't *really* 32ms, I should adjust the target time accordingly? So a "tick" is measured in Sim time, right? And it varies depending on engine load etc.? And "real time" is really the time that has passed in the OS, independently of the engine, so to speak?

Just having some basic problems understanding the "Sim Time" versus "Real Time" concept I guess... :P
Thanks for any input and pointers!
#6
02/23/2007 (3:16 pm)
Pretty close, yes. What happens is basically that when a schedule is called, it says "hey, our ticks per millisecond rate is defined as xxx, and we have yyy milliseconds to wait. Let's multiply the two, get a tick number, add it to the current tick number, and post the event to occur at that tick number".

Of course, that ticks per millisecond is a constant, and the more milliseconds you want to wait, the higher probability there is that the estimated/multiplied tick is further off from the "real time elapsed" than what you originally wanted.

What I'm suggesting basically is a more optimized solution than saying every tick "hey, any of you events want to post now?"--that would be HUGE overhead...instead, say maybe every 10 ticks you see how many milliseconds have elapsed, how many milliseconds you think should have elapsed, and adjust if necessary.

It won't be perfect, and will require additional overhead, but it approaches more accurate "schedules as elapsed time" than the current system does.