Threadsafe Calls

The purpose of this post is to give an introduction to making thread-safe calls specifically within .NET/Mono.

Thread Safe Calls

A thread safe calls are a type of call that can be made from any thread that won’t interfere with the operations of another thread.  This is an important overhead to include because, as an example, if you have one thread reading a list and another thread writing to that list at the same time the first thread is most likely going to go beyond the bounds of the list.  One of the ways to achieve this in a thread-safe environment is to control access by using either a lock, mutex or semaphore.

Locks, Mutexes and Semaphores

The way a lock, mutex and semaphore work is to block the calling thread until they have been released.  For .NET/Mono there is very little difference between a lock and a mutex; a semaphore on the other day is essentially a more complicated lock, allowing for multiple threads to read/write.  Most of the you’ll use a mutex, but sometimes you may want to control read and writes in a far more complex way in which case you should use a semaphore.

How To Use Them

The way mutexes work is you enter the mutex before accessing the memory in question, perform your operations, then exit the mutex after you’ve completed.  However, the best one I have found for using across .NET and Mono is a semaphore called ReaderWriterLockSlim:

using System.Threading;

public class ThreadLock
{
    ReaderWriterLockSlim mLock;

    public static void Main()
    {
        /* Create the lock */
        mLock = new ReaderWriterLockSlim();

        /* Enter the lock */
        while (!mLock.TryEnterWriteLock(-1));

        /* TODO: Processing */
        throw new NotImplementedException("Do Processing!");

        /* Exit the lock */
        mLock.ExitWriteLock();
    }
}

The above TryEnterWriteLock() will attempt to enter the semaphore for an infinite number of milliseconds (-1). This is a fairly efficient and fast sempahore that supports both read and write locks, with multiple and combined cases for both. Personally I use it as a straight mutex instead of a SpinLock because at the time of porting Space Salvager it was not available in Mono.

When executing code that requires a mutex it’s fairly standard to run that inside a try and finally statement. What this ensures is that any synchronisation exceptions are caught and, in the slim but often frequent chance, any exception is thrown the mutex is always released. The below example shows the base object GameObject in Space Salvager handling it’s update call using a mutex:

public void Tick(float delta)
{
    try
    {
        /* Lock this object */
        Lock();

        /* Check the timer */
        if (Enabled)
        {
            /* Call update */
            Update(delta);
        }
    }
    catch (Exception e)
    {
        ComponentOwner.Game.Log(e.ToString());
    }
    finally
    {
        /* Unlock this object */
        Unlock();
    }
}

protected abstract void Update(float delta);

All objects inherit from this abstract class, overriding Update to implement their own update code. Because the call is made from within Tick all the calls made within Update to itself are thread-safe. If another object is accessed, you would call Lock on that object to ensure synchronisation. The issue to bare in mind, however, is not to cause a recursive lock. If a thread enters a mutex a second time before exiting this is known as a recursive lock. Some mutexes will allow this, for example a Monitor will, however the issue with this is when the subsequent exit is called the mutex is immediately released before it is expected to have been.

The solution to a recursive lock is simply to plan your architecture and ensure you don’t lock your mutex twice on the same thread. Sometimes this is very easy, but if you’re making a rather complex game then it can become very tricky.

Interlocked

Sometimes it isn’t necessary to use a mutex to access an object atomically.  .NET/Mono implements an atomic class called Interlocked, which allows integers and other numerical value types to be read and written to atomically.

private void BroadcastAddObject(GameObject obj, string sector)
{
    /* Find the object */
    long? identifier = GetTrackedObject(obj);
    if (!identifier.HasValue)
    {
        /* Get a new identifier */
        identifier = Interlocked.Increment(ref mTrackingIdentifier);

        /* Add to the tracked objects dictionary */
        AddTrackedObject(obj, identifier.Value);
    }
}

This is good when using an index or an ID number for example, to be honest however if you need to use an Interlocked you’ll know when.

2 thoughts on “Threadsafe Calls

  1. “The way to achieve this in a thread-safe environment is to control access by using either a lock, mutex or semaphore.” Those are by no means the only way to make something thread-safe, and I think you should clarify that. Compare-and-swap, is another type of thread-safe mechanism, besides there are other means to make something lock-free thread-safe. The methods you mention are merely the main blocking/lock based methods of thread-safe programming.

    Like

Leave a comment