Using asynchronous code introduces a range of possible bugs that boil down to data corruption of mutable shared state.

This is because threads share the heap memory and therefore the state of reference object instances. So if you save data in a field and access it asynchronously it can lead to data corruption. This is because with asynchrony you possibly call operations that change the fields state concurrently, that is more than one thread accesses the same data simultaneously.

What this post will cover

  • Thread safety principiles
  • Alternative techniques to synchronization
  • How to prevent state corruption with synchronization
  • Exclusive Locking
  • Nonexclusive Locking

What this post will not cover

  • TPL in general
  • Concurrent data structures
  • Async debugging
  • Signaling
  • Other techniques like ImmutableState objects
  • Make operations atomic

We will look into this issue in this post and how to avoid the potential bugs this entails.

Sharing is Caring?

This might be true in some cases, yet absolutely not for asynchronous code. At the heart of every asynchronous bug lies mutable shared data. That is data like a field in a class that stores some information and can be accessed and changed in any way.

One obvious solution is to copy all data and then process it. This would solve the issue somewhat, but as you might have expected this is not the end all be all, because this has potentially severe performance costs.

Another aspect is Immutable shared state. That means if the data is readonly there is no issue with accessing it concurrently as long as no one can change the state. This obviously can not be achieved for every situation. Yet the idea is to isolate more parts of the algorithm so it works mostly on immutable data. Consider Linq for this type of behavior.
We will look at a way to utilize immutable state objects for this purpose in another post.

The next notion we need to look at is an atomic operation. Atomic operations are defined as transitions between two states from an observers perspective, with no invalid state inbetween of these transition. Because the CLR of .Net has references that match the bit-ness of the OS and atomic operations are equal to a single processor instruction, all references can be written atomically.

So if all operations are atomic, the concurrent operation on data is not an issue.

Issues arise when the operation is not atomic or you perform lots of atomic operations that semantically belong together. As an example the increment operator (++) is not atomic because it:

  1. copies the value of a variable to a register
  2. increments the register
  3. copies the value back to variable

Remember that local variables and structs etc are not subject to this problem, because each thread gets its own stack. So as mentioned in the introduction, only the heap memory is affected (fields, references, object instances etc.).

Before we look at how to solve this in .Net you should explore race conditions and dead locks either on Wikipedia (rc , dl) or in my explanation.

Thread Safety

To avoid the issues described above is covered by the topic of thread safety. This includes making operations atomic or synchronizing threads to avoid state corruption.

The basic idea is that every thread needs to gain access to a given construct (we will soon see what this is) a sort of token that allows or restricts the access to the state that the Thread wants to access. If this “token” is already taken, the thread needs to wait until this token is again free to use.

This obviously will slow the application down because all operations on this state are now performed serially . This needs to be done in a balancing act and goes well with the above mentioned optimization of your algorithm for maximum immutable shared state.

So next we look at some of the most important parts in the .Net toolkit for synchronizing access to mutable shared state:

Atomicity

As we saw above, atomic operations are inherently thread safe. In .Net you can use the static Interlocked class to make operations atomic. I have written another post with specific focus on atomicity and the Interlocked class.

Synchronization

Synchronization is the process I described above (token etc.) . This is especially needed when we encounter the problem of multistage transition, which means that more than one data item is changed in a method.

public class Business
{
private int _cash;
private int _receivables;
public void ReceiveIncome(int income)
{
_cash += income;
_receivables -= income;
}
}

I know it is a stupid example, yet it delivers the key message: atomicity will not help you here. Interlocked.Increment/Decrement could still lead to an invalid state.

To ensure that multistage transition you need some form of protocol this is the above mentioned acquiring of the “token”. In CLR terms those tokens are entries in the so called Sync Block table. Each object instance has a reference to this Sync Block table in its object header (CLR construct).

This reference is empty by default. If now a Sync Block entry is created, the object reference needs to have the reference to this Sync Block or else it must wait until it can acquire it.

Monitor – or the workhorse of .Net synchronization

The static Monitor class is the .Net construct that allows to reference this Sync Blocks. For this you call the Monitor.Enter method, this is called acquiring or getting the ownership of the Monitor. To release the reference of the Monitor you would call Monitor.Exit. Both methods get an object as input parameter. For this you commonly use a readonly object field, because you want the access to the instance that owns the Sync Block as narrow as possible to avoid unecessary lock contention and dead locks.

...
private readonly object stateGuard = new object();
public void ReceiveIncome(int income)
{
Monitor.Enter(stateGuard);
_cash += income;
_receivables -= income;
Monitor.Exit(stateGuard);
}

But what if an Exception were thrown inside the method? You probably guessed it, the Monitor would be in the locked state “forever”. So we need to wrap the stuff with try/finally. Besides that the Monitor.Enter method also could throw an exception. Therefore it needs to be inside the try catch block, too.

Now if the Monitor could throw an exception the question is, did it so before or after the ownership was taken? This can be tested with the overload of the Monitor.Enter like in the following snippet:

public void ReceiveIncome(int income)
{
bool flagIsTaken = false;
try
{
Monitor.Enter(stateGuard, ref flagIsTaken);
_cash += income;
_receivables -= income;
}
finally
{
if (flagIsTaken)
Monitor.Exit(stateGuard);
}
}

What you mostly will see in production is the use of the so called lock statement. The following snippet introduces the lock. This does internally the same things as in the snippet above.

...
public void ReceiveIncome(int income)
{
lock (stateGuard)
{
_cash += income;
_receivables -= income;
}
}
...

Timeouts

As I mentioned often in my series on asynchronous programming, timeouts are a crucial part for good asynchronous programming. But neither the above use of Monitor nor the lock allow for timeouts. So what to do?

The Monitor also offers the TryEnter method:

public void ReceiveIncome(int income)
{
try
{
bool flagIsTaken = false;
Monitor.TryEnter(stateGuard, TimeSpan.FromSeconds(15), ref flagIsTaken);
if (flagIsTaken)
{
_cash += income;
_receivables -= income;
}
else
throw new TimeoutException("Failed to acquire stateGuard");
}
finally
{
if (flagIsTaken)
Monitor.Exit(stateGuard);
}
}

see my github project: heitech.TplXt for a static MonitorTimeOut helper method that can conveniently be used with a using statement because it is IDisposable.

So all of the above techniques with Monitor/lock lets only one thread pass at a time and is therefore called exclusive locking. It lets the same thread enter the same lock reapetadly, which is called reentrancy.

Another way to go about synchronization, is to use NonExclusive Locking, where the access is restricted to a specific amount of incoming threads. A third way is to coordinate threads with the notion of signaling, which we will look at in another post.
On the next page we are going to look at the nonexclusive locking:

Categories: TPL

0 Comments

Leave a Reply

Avatar placeholder