Gavin Pugh - A Videogame Programming Blog

XNA/C# – [ThreadStatic] attribute is broken on Xbox 360

26 November, 2010 at 8:34pm | XNA / C#

[ThreadStatic]

Continuing on from my last XNA post, I had a go at using [ThreadStatic] on the Xbox. Specifically this attribute was not available with XNA 3.1, but was introduced in the new 4.0 version.

At first glace it worked fine, I threw in the same code I’d used on Windows. Unlike under 3.1 it compiles without issue. However, the attribute appears to actually do nothing it all. It behaves just like the attribute wasn’t there, just like a regular plain static variable.

I found this issue from attempting to use [ThreadStatic] within some code I had written for a future article here. The code was to profile various methods of implementing Thread-local storage on the Xbox 360. Switching the code to add a test case for [ThreadStatic] worked fine at first, but I was seeing very odd profile numbers coming back from it. On closer inspection in the debugger I could see that the TLS values weren’t thread-specific, they were getting written to at runtime by other executing threads.

Show me the code

So, I spliced out some code and came up with a contrived test to check how [ThreadStatic] is working. Here’s the code:

public class ThreadTest
{
    [ThreadStatic] private static int m_int_counter = 0;

    private Thread m_thread;
    private int m_affinity;
    private int m_thread_num;

    public ThreadTest( int thread_num, int affinity )
    {
        m_affinity = affinity;
        m_thread_num = thread_num;
        m_thread = new Thread( Run );
    }

    public void Start()
    {
        m_thread.Start( (object)this );
    }

    public static void Run( object passed_obj )
    {
        ThreadTest this_obj = (ThreadTest)passed_obj;

        Debug.WriteLine( "Started thread #" +
            this_obj.m_thread_num + " Affinity=" + this_obj.m_affinity );

#if XBOX
        Thread.CurrentThread.SetProcessorAffinity( this_obj.m_affinity );
#endif //XBOX

        Debug.WriteLine( 
            "Thread #" + this_obj.m_thread_num + 
            ": Before m_int_counter " + m_int_counter );

        m_int_counter++;

        Debug.WriteLine( 
            "Thread #" + this_obj.m_thread_num + 
            ": After m_int_counter " + m_int_counter );
    }
}

// The four affinities that are okay to use. See:
// http://msdn.microsoft.com/en-us/library/microsoft.xna.net_cf.system.threading.thread.setprocessoraffinity.aspx
private static readonly int[] m_affinities = new int[4] { 1, 3, 4, 5 };

private static bool m_started = false;

// Call this from anywhere, I just threw it in Update() in a new XNA game project
public void Test()
{
    if ( m_started )
    {
        // Just do it once...
        return;
    }
    m_started = true;

    const int NUM_THREADS = 10;

    ThreadTest[] threads = new ThreadTest[NUM_THREADS];

    // Create ten threads and kick them off near-enough at the same time.
    for ( int i = 0; i < NUM_THREADS; i++ )
    {
        int affinity = m_affinities[i % 4];
        threads[i] = new ThreadTest( i, affinity );
        threads[i].Start();
    }
}

What's up

Firstly, here's the output under Windows. The 'before' number is always zero, and the 'after' is always one. The [ThreadStatic] variable is indeed unique to each thread instance.

Windows

Started thread #0 Affinity=1
Started thread #6 Affinity=4
Started thread #4 Affinity=1
Started thread #7 Affinity=5
Started thread #8 Affinity=1
Started thread #2 Affinity=4
Started thread #9 Affinity=3
Thread #7: Before m_int_counter 0
Thread #8: Before m_int_counter 0
Thread #0: Before m_int_counter 0
Thread #6: Before m_int_counter 0
Thread #4: Before m_int_counter 0
Thread #2: Before m_int_counter 0
Thread #9: Before m_int_counter 0
Thread #8: After m_int_counter 1
Thread #7: After m_int_counter 1
Thread #0: After m_int_counter 1
Thread #6: After m_int_counter 1
Thread #4: After m_int_counter 1
Thread #9: After m_int_counter 1
Thread #2: After m_int_counter 1
Started thread #5 Affinity=3
Started thread #3 Affinity=5
Started thread #1 Affinity=3
Thread #1: Before m_int_counter 0
Thread #1: After m_int_counter 1
Thread #5: Before m_int_counter 0
Thread #5: After m_int_counter 1
Thread #3: Before m_int_counter 0
Thread #3: After m_int_counter 1

 
Disregard the affinity value for Windows, it's not used. Anyhow, here's the Xbox output.

Xbox 360

Started thread #0 Affinity=1
Thread #5: Before m_int_counter 3
Thread #6: Before m_int_counter 3
Thread #3: Before m_int_counter 3
Thread #2: Before m_int_counter 3
Thread #7: Before m_int_counter 3
Thread #9: Before m_int_counter 3
Thread #0: Before m_int_counter 3
Thread #5: After m_int_counter 4
Thread #6: After m_int_counter 5
Thread #3: After m_int_counter 6
Thread #2: After m_int_counter 7
Thread #7: After m_int_counter 8
Thread #9: After m_int_counter 9
Thread #0: After m_int_counter 10

 

The [ThreadStatic] variable is obviously not unique to each thread. It's just behaving like a regular static. If I comment out the line which sets the thread affinity (which is Xbox-specific code), I get similar output.

What's also concerning is that the "Debug.WriteLine()" function seems to be randomly failing. Each time I run it, I get different lines missed out from my expected output.

Perhaps on Xbox it's implementation is not completely thread-safe? I swear it was on XNA 3.1's Compact Framework, but maybe my memory is failing me.

Solutions?

I have an alternate way of implementing TLS with C#. It's very fast as long as you're not running a ridiculous number of threads (under a dozen is fine). When I last looked at the code under XNA 3.1 it outperformed the 'GetData/SetData()' method of TLS considerably. In fact when I tested the code under Windows and compared it to [ThreadStatic], it outperformed that too.

Will be the next article I post up, since I think I've encountered all the issues I can on the road to finishing it up.

References

Leave a Reply

Your email address will not be published. Required fields are marked *