1

The following sample code snippet implements the async multitasking functionality, namely: reading the list of Stock Prices (in particular, corresponding to Dow Jones Industrial Average index) online and storing them asynchronously in 3 various Collection.Generics:

Dictionary<string,double>

SortedDictionary<string,double> and

ConcurrentDictionary<string,double>.

The app has been tested and all 3 Generics seem to perform normally without any concurrency (racing condition) issues observed so far.

My question: even though the quick test resulted in all positive outcomes, is it considered safe to use a Dictionary<string,double> or SortedDictionary<string,double> in this type of async/await multitasking WPF implementation, or it still requires the use of ConcurrentDictionary<string,double> in order to avoid possible concurrency/race conditions issues?

Important: for added clarity, please notice that this question is specific to the async/await multitasking implementation (not just general TPL stuff). In the await implementation it’s perfectly fine, for example, to access directly any UI Control (e.g. TextBox) with NO risk of concurrency/racing issues whatsoever. I would assume that the same consideration will apply to the Windows class variables as well (in particular, to any Generics data structures like the Dictionaries used in this sample) making it safe to use them in the same manner without any need for additional thread synchronization/interlocking (even though the regular Collections/Dictionary per se are not thread-safe, but await is possibly taking care of the synchronization issue).

Listing 1. Sample app reading stock quotes online via multiple async Tasks

using System;
using System.Windows;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Collections.Concurrent;

// sample stock quote project implementing multiple async Tasks
// and various Dictionary objects to store results
namespace QuoteServer
{
    public partial class MainWindow : Window
    {
        // quote web service root Url
        private const string _baseUrl = @"http://finance.yahoo.com/d/quotes.csv?f=l1&s=";

        /// <summary>
        /// Sample List of 30 Stock Symbols (Dow Jones Industrial Average) 
        /// </summary>
        List<string> ListDJIA = new List<string>
        {
            "AAPL", "AXP",  "BA",   "CAT",  "CSCO", "CVX",  "DD",   "DIS",  "GE",   "GS",
            "HD",   "IBM",  "INTC", "JNJ",  "JPM",  "KO",   "MCD",  "MMM",  "MRK",  "MSFT",
            "NKE",  "PFE",  "PG",   "TRV",  "UNH",  "UTX",  "V",    "VZ",   "WMT",  "XOM"
        };

        // store results (multiple stock Px) of asynchronous Tasks in various Dictionary objects
        private Dictionary<string, double> _dQuote = new Dictionary<string,double>();
        private ConcurrentDictionary<string, double> _cdQuote = new ConcurrentDictionary<string,double>();
        private SortedDictionary<string, double> _sdQuote = new SortedDictionary<string,double>();

        private CancellationTokenSource _CTS;

        public MainWindow(){ InitializeComponent();}

        /// <summary>start multiple async Tasks to get quote list</summary>
        private async void startButton_Click(object sender, RoutedEventArgs e) {
            _CTS = new CancellationTokenSource();
            try{
                await AccessWebAsync(_CTS.Token);
                resultsTextBox.Text += "\r\nDownloads complete.";
            }
            catch (OperationCanceledException){
                resultsTextBox.Text += "\r\nDownloads canceled.\r\n";
            }
            catch { resultsTextBox.Text += "\r\nDownloads failed.\r\n"; }
            finally { _CTS = null; }
        }

        /// <summary>cancel async Tasks</summary>
        private void cancelButton_Click(object sender, RoutedEventArgs e) {
            if (_CTS != null) _CTS.Cancel();
         }

        /// <summary>
        /// access web service in async mode to run multiple Tasks
        /// </summary>
        /// <param name="CT">CancellationToken</param>
        /// <returns>Task</returns>
        async Task AccessWebAsync(CancellationToken CT)
        {
            _dQuote.Clear();
            _cdQuote.Clear();
            _sdQuote.Clear();
            resultsTextBox.Clear();

            HttpClient _httpClient = new HttpClient();
            List<Task> _taskList = new List<Task>();
            foreach (string _symbol in ListDJIA)
            {
                _taskList.Add(GetQuoteAsync(_symbol, _httpClient, CT));
            }
            await Task.WhenAll(_taskList);

            // test if count is 30 (DJIA stocks)
            resultsTextBox.Text += String.Concat("Dictionary Items: ", _dQuote.Count, Environment.NewLine);
            resultsTextBox.Text += String.Concat("Dictionary Items: ", _cdQuote.Count, Environment.NewLine);
            resultsTextBox.Text += String.Concat("Dictionary Items: ", _sdQuote.Count, Environment.NewLine); 
        }

        /// <summary>
        /// Get quote from web in async mode
        /// </summary>
        /// <param name="Symbol">string</param>
        /// <param name="client">HttpClient</param>
        /// <param name="CT">CancellationToken</param>
        /// <returns>Task</returns>
        async Task GetQuoteAsync(string Symbol, HttpClient client, CancellationToken CT) {
            try {
                HttpResponseMessage _response = await client.GetAsync(_baseUrl + Symbol, CT);
                // bytes array
                byte[] _arrContentBytes = await _response.Content.ReadAsByteArrayAsync();
                // bytes array to string
                string quote = System.Text.Encoding.UTF8.GetString(_arrContentBytes);

                // try parse Stock Px as double
                double _dbl;
                if (double.TryParse(quote,out _dbl))
                {
                    // add item to dictionary (simplest option)
                    _dQuote.Add(Symbol, _dbl);
                    // add item to sorted dictionary (desirable option)
                    _sdQuote.Add(Symbol, _dbl);
                    // add item to concurrent dictionary (guaranteed thread-safe option)
                    _cdQuote.TryAdd(Symbol, _dbl);
                }
            }
            catch { throw; }
        }
    }
}
Alexander Bell
  • 7,842
  • 3
  • 26
  • 42

3 Answers3

2

Dictionary and SortedDictionary are not thread-safe except for read-only access. If an instance of either can be modified by any thread concurrently with another thread using it, then you must synchronize. Otherwise, you need not.

In your example, each use of await is in the context of the UI thread, and thus the continuation after the await (i.e. the code that appears after each await) will also occur in the UI thread. This means that each collection instance is only ever used in the UI thread, so modifications and accesses necessarily cannot occur concurrently. They are serialized by the use of await to execute the continuation code within a single thread.

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • Per your explanation, it seems that its OK to use ANY of the aforementioned Dictionaries (in particular, SortedDictionary) in this code snippet as the 'await' functionality will provide the necessary synchronization. Please confirm that this interpretation is correct. Thanks and regards, – Alexander Bell Apr 09 '16 at 03:06
  • 1
    Yes. Since the dictionary object is only ever accessed on the one thread, it doesn't matter which one you use. – Peter Duniho Apr 09 '16 at 03:12
0

If the use is concurrent I would definitely use ConcurrentDictionary. You can test countless times and get positive outcomes, and then once in 100 or 1000 times it will do something unpredictable. That it usually works is actually a problem, because when it doesn't you'll be unable to reproduce the error.

Here's a thread from a few days ago. I did a simple example of a concurrent operation, and sometimes i could run it over a hundred times with no issues. The scenario was slightly different (it involved sequences.) But the underlying concept is still the same. I deal with production applications that run most of the time and then lock up or misbehave every other week because of issues like that.

Community
  • 1
  • 1
Scott Hannen
  • 27,588
  • 3
  • 45
  • 62
0

From MSDN

A Dictionary can support multiple readers concurrently, as long as the collection is not modified. Even so, enumerating through a collection is intrinsically not a thread-safe procedure. In the rare case where an enumeration contends with write accesses, the collection must be locked during the entire enumeration. To allow the collection to be accessed by multiple threads for reading and writing, you must implement your own synchronization.

The ConcurrentDictionary<TKey,TValue> is the thread-safe counterpart to the generic Dictionary<TKey, TValue> collection, both are designed for O(1) – lookups of data based on a key.

In genral when we need a concurrent dictionary we are interleaving both reads and updates. It achieves its thread-safety with no common lock to improve efficiency. It actually uses a series of locks to provide concurrent updates, and has lockless reads.

Last but not least, SortedDictionary,A sorted implementations of IDictionary, which are stored as an ordered tree. While these are not as fast as the non-sorted dictionaries – they are O(log2 n) A combination of both speed and ordering.

Hari Prasad
  • 16,716
  • 4
  • 21
  • 35
  • Many thanks for your detailed response. Pertinent to this particular implementation (utilizing async/await), please refer to the insightful answer given by @PeterDuniho (in brief, the "await" part is implicitly taking care of the Tasks synchronization, so it's OK to use ANY type of Dictionaries in this code snippet). My best, – Alexander Bell Apr 09 '16 at 03:25