5

There's various posts/answers that say that the .NET/.NET Core's ConcurrentDictionary GetOrAdd method is not thread-safe when the Func delegate is used to calculate the value to insert into the dictionary, if the key didn't already exist.

I'm under the belief that when using the factory method of a ConcurrentDictionary's GetOrAdd method, it could be called multiple times "at the same time/in really quick succession" if a number of requests occur at the "same time". This could be wasteful, especially if the call is "expensive". (@panagiotis-kanavos explains this better than I). With this assumption, I'm struggling to understand how some sample code I made, seems to work.

I've created a working sample on .NET Fiddle but I'm stuck trying to understand how it works.

A common recommendation suggestion/idea I've read is to have a Lazy<Task<T>> value in the ConcurrentDictionary. The idea is that the Lazy prevents other calls from executing the underlying method.

The main part of the code which does the heavy lifting is this:

    public static async Task<DateTime> GetDateFromCache()
    {
        var result = await _cache.GetOrAdd("someDateTime", new Lazy<Task<DateTime>>(async () => 
        {
            // NOTE: i've made this method take 2 seconds to run, each time it's called.
            var someData = await GetDataFromSomeExternalDependency();
            
            return DateTime.UtcNow;
            
        })).Value;
        
        return result;
    }

This is how I read this:

  • Check if someDateTime key exists in the dictionary.
  • If yes, return that. <-- That's a thread-safe atomic action. Yay!
  • If no, then here we go ....
  • Create an instance of a Lazy<Task<DateTime>> (which is basically instant)
  • Return that Lazy instance. (so far, the actual 'expensive' operation hasn't been called, yet.)
  • Now get the Value, which is a Task<DateTime>.
  • Now await this task .. which finally does the 'expensive' call. It waits 2 seconds .. and then returns the result (some point in Time).

Now this is where I'm all wrong. Because I'm assuming (above) that the value in the key/value is a Lazy<Task<DateTime>> ... which the await would call each time. If the await is called, one at a time (because the Lazy protects other callers from all calling at the same time) then I would have though that the result would a different DateTime with each independent call.

So can someone please explain where I'm wrong in my thinking, please?

(please refer to the full running code in .NET Fiddler).

Pure.Krome
  • 84,693
  • 113
  • 396
  • 647
  • 2
    `ConcurrentDictionary GetOrAdd method is not thread-safe when` that's not what those answers say at all – Panagiotis Kanavos Jan 12 '21 at 07:27
  • 1
    What they say is that the *factory method*, while thread-safe, isn't guaranteed to produce just a single result if called multiple times concurrently. If that happens, only one of the values will be used – Panagiotis Kanavos Jan 12 '21 at 07:30
  • 1
    Does asking for the value of a `Task` run it again, in your view? Why / why not? `which the await would call each time.` How did you come to that conclusion? – mjwills Jan 12 '21 at 07:30
  • @00110001 `Your lazy code would need to be an instance method, using it like is jsut making the existing problem worse` I don't think it is making the problem worse if it ensures it runs only once. – mjwills Jan 12 '21 at 07:31
  • 3
    `which the await would call each time` that's not the case. A Task is an already-active *promise*, not a function pointer. Calling `await` doesn't run the promise, it awaits for it to produce a result. Calling `await` multiple times won't run the task again, it will return the first result – Panagiotis Kanavos Jan 12 '21 at 07:32
  • @mjwills are yes i didnt see the bracket, even still, this is suspect – TheGeneral Jan 12 '21 at 07:32
  • I don't think it is suspect @00110001. I mean, `AsyncLazy` may be better (since `Result` may deadlock). And I'd use `LazyWithNoExceptionCaching` over `Lazy` as a general rule. But other than that the code does what I expect. – mjwills Jan 12 '21 at 07:33
  • 2
    As for `Lazy`, it's not really needed for lazy evaluation - when you call `GetDateFromCache` you want the answer *now*. `ConcurrentDictionary` doesn't have async factory methods though so `Lazy` is used to turn the asynchronous factory method into a synchronous object. Essentially, it synchronizes possible concurrent factory calls – Panagiotis Kanavos Jan 12 '21 at 07:40
  • 2
    `(because the Lazy protects other callers from all calling at the same time) ... with each independent call.` Lazy ensures it gets called successfully only once (and by default ensures only one attempt occurs at a given point in time). The first run succeeded, so each subsequent call doesn't even occur. Asking for the `Result` of a completed task doesn't _do_ anything. It just serves up the same value again. And if it isn't completed (inflight) it just blocks until it is ready. Which is consistent with https://dotnetfiddle.net/SS3NJH . – mjwills Jan 12 '21 at 07:47
  • 1
    This technique can be improved if you use one of the many AsyncLazy implementations like [this one](https://github.com/StephenCleary/AsyncEx/wiki/AsyncLazy). A Lazy doesn't know about tasks, which is why you need to await its `Value` to get a `Task<>` instead of a `Task>` – Panagiotis Kanavos Jan 12 '21 at 07:52
  • Thank you @PanagiotisKanavos and @mjwills for spending time on my question - really appreciate it both. So in summary, is it fair to say the code provided is Okay? It can be made better (read: simpler to read) with @stephencleary 's `AsyncLazy` .. but it still _works_ okay? – Pure.Krome Jan 12 '21 at 08:59
  • @Pure.Krome Basically yes. It will work (if it doesn't deadlock). – mjwills Jan 12 '21 at 10:17
  • Who ever downvoted, please explain why? I'm asking an honest question to why my understanding of some code, is not what is happening. Shees! – Pure.Krome Jan 12 '21 at 11:28
  • Fixed up the OP to hopefully reflect my understanding, learning from all of this. – Pure.Krome Jan 13 '21 at 01:32

1 Answers1

3

Because I'm assuming (above) that the value in the key/value is a Lazy<Task<DateTime>>

Yes, that is true.

which the await would call each time. If the await is called, one at a time (because the Lazy protects other callers from all calling at the same time) then I would have though that the result would a different DateTime with each independent call.

await is not a call, it is more like "continue execution when the result is available". Accessing Lazy.Value will create the task, and this will initiate the call to the GetDataFromSomeExternalDependency that eventually returns the DateTime. You can await the task however many times you want and get the same result.

Pure.Krome
  • 84,693
  • 113
  • 396
  • 647
JonasH
  • 28,608
  • 2
  • 10
  • 23