Practical .NET

Sharing Information Between Asynchronous Processes

When you need to share a list of specific information between asynchronous processes, you probably need the ConcurrentDictionary. Except, of course, when you don't.

In previous columns I've discussed a simple and reliable way to create asynchronous applications -- applications that are more responsive to the user and more likely to take advantage of multi-core processors. I discussed how the BlockingCollection provides the easiest way to link two (or more) asynchronous processes and, in a later column I showed how to use the ConcurrentQueue, ConcurrentStack, and ConcurrentBag collections to improve performance (while, admittedly, giving up some useful functionality).

In reality, a real asynchronous application would probably take advantage of several of these tools. The case study that I used was an application with a UI that accepted inputs from the user in one process (the "producer") and passed those objects to another, asynchronous process that handled all updates (the "consumer") through a BlockingCollection. In that application, the update process returned errors to the UI process to notify the user of problems -- the best choice for passing the error objects back to the UI would probably be the ConcurrentQueue collection.

There's another Concurrent* collection that asynchronous applications might require: The ConcurrentDictionary. The ConcurrentDictionary allows you to store items under a key and share that data among asynchronous processes without requiring locks. The ConcurrentDictionary isn't your best choice for my previous scenarios where the producer was providing a steady stream of data to the consumer. The ConcurrentDictionary is the tool you need when you need to share a set of named values between processes.

If, for example, my UI process was handling sales orders for customers with each customer having multiple sales orders, I would prefer not to repeatedly retrieve information for each customer from my database. Instead, I'd prefer to retrieve a Customer object once (with that customer's first sales order) and then store that Customer object in a dictionary under the customer's Id. After that, whenever I was processing a sales order, I'd first check to see if the relevant Customer object was already in my dictionary before I would fetch new information from the database.

When Not To Use the ConcurrentDictionary
To use a ConcurrentDictionary, your first step is to declare a ConcurrentDictionary at the class level (so that it can be used by multiple processes) and specify the data type of both the key and the item to be stored. This example specifies that Customer objects are to be stored under a key of type string:

Dim dictCustomer As New ConcurrentDictionary(Of String, Customer)

I can now add and remove items from the collection by key value. This example stores a Customer object using its Id property as the key:

Dim cust As Customer
cust = New Customer
cust.Id = "A123"
cust.FirstName = Me.FirstName.Text

dictCustomers(cust.Id) = cust

And this code retrieves that Person object:

cust = dictCustomer("A123")

However, if all you're doing is adding and retrieving items from the collection, then you don't need the ConcurrentDictionary, even if you're using it from asynchronous processes. In that scenario, the performance of any other dictionary object (a HashTable, for example) will be about the same as the ConcurrentDictionary. Where the ConcurrentDictionary shines is when you're changing the item associated with a particular key and doing it simultaneously from multiple processes.

As an example, I can extend my case study to have a notification process in place that lets me know that a customer's information has been changed at the database. In that case, when I received a notification I would want to re-fetch the customer data and update the Customer object stored in the dictionary under the customer’s Id. Now that I’m updating a customer already in the dictionary, it’s time to use a ConcurrentDictionary.

Adding and Retrieving Items from the Dictionary
At this point, rather than using the methods that the ConcurrentDictionary shares with other dictionaries, I also want to start using the ConcurrentDictionary’s unique methods: TryGetValue and TryAdd. Neither of these methods can ever fail or pause execution.

For example, the simplest way to retrieve an item from the dictionary is to use TryGetValue. Passed a key as its first parameter, TryGetValue populates its second parameter with the item from the dictionary and returns True; if there is no matching item in the dictionary, TryGetValue returns False and doesn’t populate the second parameter. Either way, no exception is thrown.

This code attempts to retrieve the current value for the key "A123":

If dictCustomer.TryGetValue("A123", cust) Then
  '...working with the cust variable
End If

Similarly, to add an item to the dictionary, the simplest method to use is TryAdd. Passed two parameters (a key and an item), the method adds the item to the dictionary and returns True. If the key is already present in the dictionary, the item isn't added and the method returns False. This code checks to see if it can add an item and, if it can't, uses TryGetValue to retrieve the item for the key:

If Not dictCustomer.TryAdd("A123", cust) Then
  If dictCustomer.TryGetValue("A123", cust) Then
    '...working with the cust variable
  End If
End If   

However, if multiple processes can add or remove items then things can get, well, complicated. In my previous example, imagine that the TryAdd method finds an existing item and returns False. In the next statement, TryGetValue attempts to retrieve the value that TryAdd found … but it's possible that in between those two statements some other process might have removed the item from the dictionary. It's possible, therefore, that inside the two nested Ifs I might still not have a Customer object with which to work.

One solution to this problem is architectural: limit functionality in the processes so that there is no process that can remove an item (that way, if TryAdd fails, TryGetValue is guaranteed to find an item with the key). Unfortunately, more complex applications may require processes that can both add and remove items. But, as I've implied, TryAdd and TryGetValue are just the simplest methods for working with CurrentDictionary. In my next column, I'll look at some more sophisticated tools for working with ConcurrentDictionary to share data between asynchronous processes.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube