February 2019

Volume 34 Number 2

[C#]

Minimize Complexity in Multithreaded C# Code

By Thomas Hansen | February 2019

Forks, or multithreaded programming, are among the most difficult things to get right when programming. This is due to their parallel nature, which requires a completely different mindset than linear programming with a single thread. A good analogy for the problem is a juggler, who must keep multiple balls in the air without having them negatively interfere with each other. It’s a major challenge. However, with the right tools and the right mindset, it’s manageable.

In this article, I dive into some of the tools I’ve crafted to simplify multithreaded programming, and to avoid problems such as race conditions, deadlocks and other issues. The toolchain is based, arguably, on syntactic sugar and magic delegates. However, to quote the great jazz musician Miles Davis, “In music, silence is more important than sound.” The magic happens between the noise.

Put another way, it’s not necessarily about what you can code, but rather what you can but choose not to, because you’d rather create a bit of magic between the lines. A quote from Bill Gates comes to mind: “To measure quality of work according to the number of lines of code, is like measuring the quality of an airplane by its weight.” So, instead of teaching you how to code more, I hope to help you code less.

The Synchronization Challenge

The first problem you’ll encounter with multithreaded programming is synchronizing access to a shared resource. Problems occur when two or more threads share access to an object, and both might potentially try to modify the object at the same time. When C# was first released, the lock statement implemented a basic way to ensure that only one thread could access a specified resource, such as a data file, and it worked well. The lock keyword in C# is so easily understood, that it single-handedly revolutionized the way we thought about this problem.

However, a simple lock suffers from a major flaw: It doesn’t discriminate read-only access from write access. For instance, you might have 10 different threads that want to read from a shared object, and these threads can be given simultaneous access to your instance without causing problems via the ReaderWriterLockSlim class in the System.Threading namespace. Unlike the lock statement, this class allows you to specify if your code is writing to the object or simply reading from the object. This enables multiple readers entrance at the same time, but denies any write code access until all other read and write threads are done doing their stuff.

Now the problem: The syntax when consuming the ReaderWriterLock class becomes tedious, with lots of repetitive code that reduces readability and complicates maintenance over time, and your code often becomes scattered with multiple try and finally blocks. A simple typo can also produce disastrous effects that are sometimes extremely difficult to spot later. 

By encapsulating the ReaderWriterLockSlim into a simple class, all of a sudden it solves the problem without repetitive code, while reducing the risk that a minor typo will spoil your day. The class, as shown in Figure 1, is entirely based on lambda trickery. It’s arguably just syntactic sugar around some delegates, assuming the existence of a couple interfaces. Most important, it can help make your code much more DRY (as in, “Don’t Repeat Yourself”).

Figure 1 Encapsulating ReaderWriterLockSlim

public class Synchronizer<TImpl, TIRead, TIWrite> where TImpl : TIWrite, TIRead
{
  ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
  TImpl _shared;

  public Synchronizer(TImpl shared)
  {
    _shared = shared;
  }

  public void Read(Action<TIRead> functor)
  {
    _lock.EnterReadLock();
    try {
      functor(_shared);
    } finally {
      _lock.ExitReadLock();
    }
  }

  public void Write(Action<TIWrite> functor)
  {
    _lock.EnterWriteLock();
    try {
      functor(_shared);
    } finally {
      _lock.ExitWriteLock();
    }
  }
}

There are just 27 lines of code in Figure 1, providing an elegant and concise way to ensure that objects are synchronized across multiple threads. The class assumes you have a read interface and a write interface on your type. You can also use it by repeating the template class itself three times, if for some reason you can’t change the implementation of the underlying class to which you need to synchronize access. Basic usage might be something like that shown in Figure 2.

Figure 2 Using the Synchronizer Class

interface IReadFromShared
{
  string GetValue();
}

interface IWriteToShared
{
  void SetValue(string value);
}

class MySharedClass : IReadFromShared, IWriteToShared
{
  string _foo;

  public string GetValue()
  {
    return _foo;
  }

  public void SetValue(string value)
  {
    _foo = value;
  }
}

void Foo(Synchronizer<MySharedClass, IReadFromShared, IWriteToShared> sync)
{
  sync.Write(x => {
    x.SetValue("new value");
  });
  sync.Read(x => {
    Console.WriteLine(x.GetValue());
  })
}

In the code in Figure 2, regardless of how many threads are executing your Foo method, no Write method will be invoked as long as another Read or Write method is being executed. However, multiple Read methods can be invoked simultaneously, without having to scatter your code with multiple try/catch/finally statements, or repeating the same code over and over. For the record, consuming it with a simple string is meaningless, because System.String is immutable. I use a simple string object here to simplify the example.

The basic idea is that all methods that can modify the state of your instance must be added to the IWriteToShared interface. At the same time, all methods that only read from your instance should be added to the IReadFromShared interface. By separating your concerns like this into two distinct interfaces, and implementing both interfaces on your underlying type, you can then use the Synchronizer class to synchronize access to your instance. Just like that, the art of synchronizing access to your code becomes much simpler, and you can do it for the most part in a much more declarative manner.

When it comes to multithreaded programming, syntactic sugar might be the difference between success and failure. Debugging multithreaded code is often extremely difficult, and creating unit tests for synchronization objects can be an exercise in futility.

If you want, you can create an overloaded type with only one generic argument, inheriting from the original Synchronizer class and passing on its single generic argument as the type argument three times to its base class. Doing this, you won’t need the read or write interfaces, since you can simply use the concrete implementation of your type. However, this approach requires that you manually take care of those parts that need to use either the Write or Read method. It’s also slightly less safe, but does allow you to wrap classes you cannot change into a Synchronizer instance.

Lambda Collections for Your Forks

Once you’ve taken the first steps into the magic of lambdas (or delegates, as they’re called in C#), it’s not difficult to imagine that you can do more with them. For instance, a common recurring theme in multithreading is to have multiple threads reach out to other servers to fetch data and return the data back to the caller.

The most basic example would be an application that reads data from 20 Web pages, and when complete returns the HTML back to a single thread that creates some sort of aggregated result based on the content of all the pages. Unless you create one thread for each of your retrieval methods, this code will be much slower than desired—99 percent of all execution time would likely be spent waiting for the HTTP request to return.

Running this code on a single thread is inefficient, and the syntax for creating a thread is difficult to get right. The challenge compounds as you support multiple threads and their attendant objects, forcing developers to repeat themselves as they write the code. Once you realize that you can create a collection of delegates, and a class to wrap them, you can then create all your threads with a single method invocation. Just like that, creating threads becomes much less painful.

In Figure 3 you’ll find a piece of code that creates two such lambdas that run in parallel. Notice that this code is actually from the unit tests of my first release of the Lizzie scripting language, which you can find at bit.ly/2FfH5y8.

Figure 3 Creating Lambdas

public void ExecuteParallel_1()
{
  var sync = new Synchronizer<string, string, string>("initial_");

  var actions = new Actions();
  actions.Add(() => sync.Assign((res) => res + "foo"));
  actions.Add(() => sync.Assign((res) => res + "bar"));

  actions.ExecuteParallel();

  string result = null;
  sync.Read(delegate (string val) { result = val; });
  Assert.AreEqual(true, "initial_foobar" == result || result == "initial_barfoo");
}

If you look carefully at this code, you’ll notice that the result of the evaluation doesn’t assume that any one of my lambdas is being executed before the other. The order of execution is not explicitly specified, and these lambdas are being executed on separate threads. This is because the Actions class in Figure 3lets you add delegates to it, so you can later decide if you want to execute the delegates in parallel or sequentially.

To do this, you must create a bunch of lambdas and execute them using your preferred mechanism. You can see the previously mentioned Synchronizer class in Figure 3, synchronizing access to the shared string resource. However, it uses a new method on the Synchronizer, called Assign, which I didn’t include in the listing in Figure 1 for my Synchronizer class. The Assign method uses the same “lambda trickery” that I described in the Write and Read methods earlier.

If you’d like to study the implementation of the Actions class, note that it’s important to download version 0.1 of Lizzie, as I completely rewrote the code to become a standalone programming language in later versions.

Functional Programming in C#

Most developers tend to think of C# as being nearly synonymous with, or least closely related to, object-oriented programming (OOP)—and obviously it is. However, by rethinking how you consume C#, and by diving into its functional aspects, it becomes much easier to solve some problems. OOP in its current form is simply not very reuse friendly, and a lot of the reason for this is that it’s strongly typed.

For instance, reusing a single class forces you to reuse every single class that the initial class references—both those used through composition and through inheritance. In addition, class reuse forces you to reuse all classes that these third-party classes reference, and so on. And if these classes are implemented in different assemblies, you must include a whole range of assemblies simply to gain access to a single method on a single type.

I once read an analogy that illustrates this problem: “You want a banana, but you end up with a gorilla, holding a banana, and the rainforest where the gorilla lives.” Compare this situation with reuse in a more dynamic language, such as JavaScript, which doesn’t care about your type, as long as it implements the functions your functions are themselves consuming. A slightly more loosely typed approach yields code that is both more flexible and more easily reused. Delegates allow you to do that.

You can work with C# in a way that improves reuse of code across multiple projects. You just have to realize that a function or a delegate can also be an object, and that you can manipulate collections of these objects in a weakly typed manner.

The ideas around delegates present in this article build on those articulated in an earlier article I wrote,  “Create Your Own Script Language with Symbolic Delegates,” in the November 2018 issue of MSDN Magazine (msdn.com/magazine/mt830373). This article also introduced Lizzie, my homebrew scripting language that owes its existence to this delegate-centric mindset. If I had created Lizzie using OOP rules, my opinion is that it would probably be at least an order of magnitude larger in size.

Of course, OOP and strongly typing is in such a dominant position today that it’s virtually impossible to find a job description that doesn’t mention it as its primary required skill. For the record, I’ve created OOP code for more than 25 years, so I’ve been as guilty as anyone of a strongly typed bias. Today, however, I’m more pragmatic in my approach to coding, and less interested in how my class hierarchy ends up looking.

It’s not that I don’t appreciate a beautiful class hierarchy, but there are diminishing returns. The more classes you add to a hierarchy, the less elegant it becomes, until it collapses under its own weight. Sometimes, the superior design has few methods, fewer classes and mostly loosely coupled functions, allowing the code to be easily extended, without having to “bring in the gorilla and the rainforest.”

I return to the recurring theme of this article, inspired by Miles Davis’ approach to music, where less is more and “silence is more important than sound.” Code is like that, too. The magic often lives between the lines, and the best solutions can be measured more by what you don’t code, rather than what you do. Any idiot can blow a trumpet and make noise, but few can create music from it. And fewer still can make magic, the way Miles did.


Thomas Hansen works in the FinTech and ForEx industry as a software developer and lives in Cyprus.


Discuss this article in the MSDN Magazine forum