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:

  1. Maya uses the following types of threads:
    • Linux - pthreads
    • Mac OS X - pthreads
    • Windows - Native Windows API threads
  2. 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.
  3. Although some operations in Maya have been threaded, they are not exposed in the API.
  4. It is possible to call into Maya from an secondary thread using the MGlobal::executeCommandOnIdle() method. In Python, the equivalent MGlobal::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.
  5. There are four C++ API classes for threading: 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 and MThreadAsync 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.

  6. 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;
}