SpinWait.SpinUntil for unit testing

SpinWait.SpinUntil for unit testing

  • Comments 3

One of the hidden gems in .NET 4 is the System.Threading.SpinWait type.  This type is typically used for implementing lock-free solutions, and is used heavily throughout the rest of the threading and parallelism support in .NET 4.  That’s why I call it “hidden”, because most folks don’t implement their own lock-free algorithms, and rightfully so.  However, one very useful method on SpinWait that escapes the realm of lock-free programming is SpinUntil.

In .NET 4, SpinWait exposes three overloads of the static SpinUntil method:

public static void SpinUntil(Func<bool> condition);
public static bool SpinUntil(Func<bool> condition, TimeSpan timeout);
public static bool SpinUntil(Func<bool> condition, int millisecondsTimeout);

You provide a Func<bool> to SpinUntil, and it’ll invoke it over and over and over, with a nice mix of spinning, yielding, and sleeping in between invocations, until the condition returns true, or until a provided timeout occurs.  This makes SpinUntil very useful in writing unit tests, in particular for testing asynchronous and otherwise concurrent components. 

Consider a component with an API as follows:

public void AddAsync(T item);
public int Count { get; }

You’d like to verify in a unit test that added items end up increasing the count.  However, since the add method stores the data asynchronously, you can’t count on (no pun intended) the Count method being updated by the time AddAsync returns.  There are a variety of solutions one might take here.  For example, you might put the current thread to sleep for several seconds in hopes that the value will have changed by then:

int prevCount = obj.Count;
AddAsync(data);
Thread.Sleep(4000);
Assert.AreEqual(prevCount + 1, obj.Count);

However, in many such APIs, it’s very likely that the effect of the operation will be almost immediately visible, and yet here we’re sleeping for four seconds regardless of whether the condition has already become true.  Instead, we can use SpinWait.SpinUntil:

int prevCount = obj.Count;
AddAsync(data);
SpinWait.SpinUntil(() => prevCount != obj.Count, 4000);
Assert.AreEqual(prevCount + 1, obj.Count);

With this version, we’ll still wait up to four seconds if necessary, but give or take a few milliseconds as soon as the property has updated, SpinUntil will return.  Thus, with very concise code we can avoid unnecessarily prolonging the execution of our unit tests.

Revolutionary? Certainly not.  But it is a nice addition to a bag of tricks.

ps If the AddAsync method in the previous example had returned Task to represent the asynchronous operation’s completion, you could also just Wait on that returned task, again providing a timeout if you wish.  Not so coincidentally, Task.Wait internally uses SpinWait before falling back to a true blocking wait.

pps As with spinning in general, this approach is only appropriate in production code if the condition you're spinning for will be true imminently, since spinning is typically only valuable if the amount of cycles you'd waste spinning are less than what you'd waste by an unnecessary kernel transition or context switch.  Things can be a bit more lenient for test code, such as that which I have above.

Leave a Comment
  • Please add 4 and 2 and type the answer here:
  • Post
  • Thanks for the post. Can you please give me your opinion on this: Sometimes when I need to start a thread I explicitly wait for it to be started, by doing this (pseudo code):

    var t = new Thread();

    t.Start();

    while (t.State == Unstarted)

      Thread.Sleep(50);

    What do you think of that ij general? Would the following be better then?

    var t = new Thread();

    t.Start();

    SpinWait.SpingUntil(() => t.State != Unstarted);

    Please enlighten me, Felix

  • Hi Felix-

    The call to Start won't return until the thread is out of the Unstarted state, so neither of those is necessary.   What are you trying to achieve?

  • Really liked the unit testing angle on your article.

    Thanks.

Page 1 of 1 (3 items)