Threading and Maya API
Plug-ins can utilize threaded code if certain guidelines are followed. Below are some notes that provide guidance for this issue:
Maya uses the following types of threads:
- Linux - pthreads
- Mac OS X - pthreads
- Windows - Native Windows API threads
The components of Maya that are available in the API are single threaded. It is always best to call into the Maya API from the main Maya thread. It is acceptable to thread your code as long as it is independent of calls to the Maya API. The exception to this rule is
MPxNode::compute()
can have threaded calls for software shaders. But this depends on the setup of the node and the software renderer.Although some operations in Maya have been threaded, they are not exposed in the API.
It is possible to call into Maya from an secondary thread using the
MGlobal::executeCommandOnIdle()
method. In Python, the equivalentMGlobal::executePythonCommandOnIdle()
method would be used. The command will not execute immediately; instead, the command will be added to the idle event queue and executed as idle processing allows. The result of the command will not be returned to the caller. This call can be useful for updating items such as the progress bar from another thread.There are four C++ API classes for threading:
MThreadPool
MThreadAsync
MSpinLock
MMutexLock
These classes can be used to implement threaded algorithms for non-Maya API functionality. Several examples that utilize these classes can be found in the developer kit. (These classes are not available in the Maya Python API.)
MThreadPool gives access to a pool of threads to which tasks can be assigned. The number of tasks does not have to equal the number of threads, in fact for load balancing it is usually better if the number of tasks exceeds the number of threads. Maya will internally balance the work among the threads for optimal efficiency. The number of threads in the pool is equal to the number of logical processors. It is not necessary to delete the thread pool after each usage, and for performance reasons it is better not to do so, since the threads will be put to sleep when a parallel region finishes, which means they can be restarted quickly.
MThreadAsync allows the creation of one of more threads that can run for a long time. They are not drawn from the thread pool created and managed by
MThreadPool
, but are independent threads. These threads can be used for longer running tasks. Since they are not created from the thread pool, the number and workload of such threads should be managed carefully to avoid oversubscription issues, where the number of busy threads exceeds the hardware resources available.MMutexLock is a locking primitive that can be used with both
MThreadPool
andMThreadAsync
threads. It allows standard mutex locking of threads.MSpinLock is a lock that spin-waits, so can be more efficient than a mutex lock in situations where the lock is likely to be held for a very short time. However since the lock spin waits, it is a heavy CPU consumer, and should not be used when locks are likely to be held for a long time.
Threading with Python is possible with the built-in thread module. The thread module can be used to implement threaded algorithms for non-Maya API functionality. Please see the Python and threading section of the Python Guide for more details.
The following example demonstrates how to find primes using a serial and a threaded approach. The threaded approach uses the MThreadPool
class.
#include <math.h>
#include <maya/MIOStream.h>
#include <maya/MSimple.h>
#include <maya/MTimer.h>
#include <maya/MGlobal.h>
#include <maya/MThreadPool.h>
DeclareSimpleCommand( threadTestCmd, PLUGIN_COMPANY, "2017");
typedef struct _threadDataTag
{
int threadNo;
long primesFound;
long start, end;
} threadData;
typedef struct _taskDataTag
{
long start, end, totalPrimes;
} taskData;
#define NUM_TASKS 16
// No global information used in function
static bool TestForPrime(int val)
{
int limit, factor = 3;
limit = (long)(sqrtf((float)val)+0.5f);
while( (factor <= limit) && (val % factor))
factor ++;
return (factor > limit);
}
// Primes finder. This function is called from multiple threads
MThreadRetVal Primes(void *data)
{
threadData *myData = (threadData *)data;
for( int i = myData->start + myData->threadNo*2; i <= myData->end; i += 2*NUM_TASKS )
{
if( TestForPrime(i) )
myData->primesFound++;
}
return (MThreadRetVal)0;
}
// Function to create thread tasks
void DecomposePrimes(void *data, MThreadRootTask *root)
{
taskData *taskD = (taskData *)data;
threadData tdata[NUM_TASKS];
for( int i = 0; i < NUM_TASKS; ++i )
{
tdata[i].threadNo = i;
tdata[i].primesFound = 0;
tdata[i].start = taskD->start;
tdata[i].end = taskD->end;
MThreadPool::createTask(Primes, (void *)&tdata[i], root);
}
MThreadPool::executeAndJoin(root);
for( int i = 0; i < NUM_TASKS; ++i )
{
taskD->totalPrimes += tdata[i].primesFound;
}
}
// Single threaded calculation
int SerialPrimes(int start, int end)
{
int primesFound = 0;
for( int i = start; i <= end; i+=2)
{
if( TestForPrime(i) )
primesFound++;
}
return primesFound;
}
// Set up and tear down parallel tasks
int ParallelPrimes(int start, int end)
{
MStatus stat = MThreadPool::init();
if( MStatus::kSuccess != stat ) {
MString str = MString("Error creating threadpool");
MGlobal::displayError(str);
return 0;
}
taskData tdata;
tdata.totalPrimes = 0;
tdata.start = start;
tdata.end = end;
MThreadPool::newParallelRegion(DecomposePrimes, (void *)&tdata);
// pool is reference counted. Release reference to current thread instance
MThreadPool::release();
// release reference to whole pool which deletes all threads
MThreadPool::release();
return tdata.totalPrimes;
}
// MSimple command that invokes the serial and parallel thread calculations
MStatus threadTestCmd::doIt( const MArgList& args )
{
MString introStr = MString("Computation of primes using the Maya API");
MGlobal::displayInfo(introStr);
if(args.length() != 2) {
MString str = MString("Invalid number of arguments, usage: threadTestCmd 1 10000");
MGlobal::displayError(str);
return MStatus::kFailure;
}
MStatus stat;
int start = args.asInt( 0, &stat );
if ( MS::kSuccess != stat ) {
MString str = MString("Invalid argument 1, usage: threadTestCmd 1 10000");
MGlobal::displayError(str);
return MStatus::kFailure;
}
int end = args.asInt( 1, &stat );
if ( MS::kSuccess != stat ) {
MString str = MString("Invalid argument 2, usage: threadTestCmd 1 10000");
MGlobal::displayError(str);
return MStatus::kFailure;
}
// start search on an odd number
if((start % 2) == 0 ) start++;
// run single threaded
MTimer timer;
timer.beginTimer();
int serialPrimes = SerialPrimes(start, end);
timer.endTimer();
double serialTime = timer.elapsedTime();
// run multithreaded
timer.beginTimer();
int parallelPrimes = ParallelPrimes(start, end);
timer.endTimer();
double parallelTime = timer.elapsedTime();
// check for correctness
if ( serialPrimes != parallelPrimes ) {
MString str("Error: Computations inconsistent");
MGlobal::displayError(str);
return MStatus::kFailure;
}
// print results
double ratio = serialTime/parallelTime;
MString str = MString("\nElapsed time for serial computation: ") + serialTime + MString("s\n");
str += MString("Elapsed time for parallel computation: ") + parallelTime + MString("s\n");
str += MString("Speedup: ") + ratio + MString("x\n");
MGlobal::displayInfo(str);
return MStatus::kSuccess;
}