Is this use of ConcurrentDictionary truly thread safe? I'm working on some teaching examples showing the perils of accessing the Dictionary class concurrently.  In the code below each function is designed to do a word count across a number of files.  (This isn't the best way of doing the count but it's there to make a point about Dictionary.)

The first example, using a Dictionary and no parallelism works fine.

The second example, using a Dictionary and Array.parallel can fail with an exception and also produces different results each time.  This is deliberate and is there to make the point.

The final example uses ConcurrentDictionary and seems to me to work fine.  My concern is: given this uses a delegate to do the count addition, is this version genuinely thread safe?

// Simple version:
let WordCount dirPath wildCard =

   let wordCounts = Dictionary&lt;string, int&gt;()

   let ProcessFile fileName =
      let text = File.ReadAllText(fileName)

      text.Split([|'.'; ' '; '\r'|], StringSplitOptions.RemoveEmptyEntries)
      |&gt; Array.map (fun w -&gt; w.Trim())
      |&gt; Array.filter (fun w -&gt; w.Length &gt; 2)
      |&gt; Array.iter (fun w -&gt; 
         let ok, count = wordCounts.TryGetValue(w)
         if ok then
            wordCounts.[w] &lt;- count+1
         else
            wordCounts.[w] &lt;- 1)

   Directory.EnumerateFiles(dirPath, wildCard)
   |&gt; Seq.iter ProcessFile

   wordCounts
   // |&gt; Seq.sumBy (fun kv -&gt; kv.Value)
   |&gt; Seq.sortBy (fun kv -&gt; -kv.Value)

// Naive concurrent version:
let WordCount2 dirPath wildCard =

   let wordCounts = Dictionary&lt;string, int&gt;()

   let ProcessFile fileName =
      let text = File.ReadAllText(fileName)

      text.Split([|'.'; ' '; '\r'|], StringSplitOptions.RemoveEmptyEntries)
      |&gt; Array.map (fun w -&gt; w.Trim())
      |&gt; Array.filter (fun w -&gt; w.Length &gt; 2)
      |&gt; Array.iter (fun w -&gt; 
         let ok, count = wordCounts.TryGetValue(w)
         if ok then
            wordCounts.[w] &lt;- count+1
         else
            wordCounts.[w] &lt;- 1)

   Directory.EnumerateFiles(dirPath, wildCard)
   |&gt; Array.ofSeq
   |&gt; Array.Parallel.iter ProcessFile

   wordCounts   
   |&gt; Seq.sumBy (fun kv -&gt; kv.Value)
   // |&gt; Seq.sortBy (fun kv -&gt; -kv.Value)

// Safe concurrent version:
let WordCount3 dirPath wildCard =

   let wordCounts = ConcurrentDictionary&lt;string, int&gt;()

   let ProcessFile fileName =
      let text = File.ReadAllText(fileName)

      text.Split([|'.'; ' '; '\r'|], StringSplitOptions.RemoveEmptyEntries)
      |&gt; Array.map (fun w -&gt; w.Trim())
      |&gt; Array.filter (fun w -&gt; w.Length &gt; 2)
      |&gt; Array.iter (fun w -&gt; wordCounts.AddOrUpdate(w, 1, (fun _ count -&gt; count+1 )) |&gt; ignore)

   Directory.EnumerateFiles(dirPath, wildCard)
   |&gt; Array.ofSeq
   |&gt; Array.Parallel.iter ProcessFile

   wordCounts 
   |&gt; Seq.sumBy (fun kv -&gt; kv.Value)
   // |&gt; Seq.sortBy (fun kv -&gt; -kv.Value)
If you just used your original code (TryGetValue followed by an indexer set) with ConcurrentDictionary, then that wouldn't be thread-safe.

But you're using the AddOrUpdate method instead, which is guaranteed to be thread-safe (i.e. atomic), so this use is thread-safe.



One other nitpick:

Directory.EnumerateFiles(dirPath, wildCard)
|&gt; Array.ofSeq


If you don't actually want seq, but an array, Directory has a method just for that: Directory.GetFiles.