4

Assume the following code

public class ValuesController : ApiController
{
    // GET api/values
    public IEnumerable<string> Get()
    {
        Lazy<TimeSpan> lm = new Lazy<TimeSpan>(GetDataAsync1, System.Threading.LazyThreadSafetyMode.PublicationOnly);

        return new string[] { "value1", "value2", lm.Value.ToString() };
    }

    private TimeSpan GetDataAsync1()
    {

        return GetTS().ConfigureAwait(false).GetAwaiter().GetResult();

    }

    // I Cant change this method, and what is inside it...
    private async Task<TimeSpan> GetTS()
    {
        var sw = Stopwatch.StartNew();

        using (var client = new HttpClient())
        {
            var result = await client.GetAsync("https://www.google.com/");
        }

        sw.Stop();
        return sw.Elapsed;
    }
}

The point is that I am getting some data from remote server, and want to cache that for later use. As remote server may fail in a given point, I dont want to cache exception, but only success result... So keeping than awaiting the value will not work for me

// Cant use this, because this caches failed exception as well
Lazy<Task...> lz = ...
await lz.Value

But above snipped, as expected produce a deadlock, given that I cant change GetTS, is it possible to force Lazy work with my logic?

Arsen Mkrtchyan
  • 49,896
  • 32
  • 148
  • 184
  • What do you want to do if you don't have a cached value when one is fetched, and you try to fetch one from the server but that fails? What should you return? – canton7 Feb 11 '21 at 14:05
  • 2
    This GetTS().ConfigureAwait(false).GetAwaiter().GetResult() cries deadlock! – Nick Feb 11 '21 at 14:14
  • when fails, I want to return exception, and on the next request try again... LazyThreadSafetyMode.PublicationOnly - without async gives this functionality out of the box... – Arsen Mkrtchyan Feb 11 '21 at 14:14
  • @Nick, I understand, If I move ConfigureAwait into GetTS, it will solve the problem... but I cant... – Arsen Mkrtchyan Feb 11 '21 at 14:16
  • So, have some logic which checks the cached `Task`. If it is complete and contains an exception, then retry the request and return the new in-progress `Task` – canton7 Feb 11 '21 at 14:31
  • Don't ever call `GetResult()` on a `Task`. Instead, use `await` on it. – Alejandro Feb 11 '21 at 14:59
  • @Alejandro, I know that, but try to do await on Lazy ;) – Arsen Mkrtchyan Feb 11 '21 at 15:19
  • @canton7, in theory yes, than I dont need lazy right? – Arsen Mkrtchyan Feb 11 '21 at 15:21
  • Correct, but you'll need your own locking of course – canton7 Feb 11 '21 at 15:29
  • @ArsenMkrtchyan You likely need to change the `Lazy` to `Lazy>` instead, where the implementing method can be async there. Or as an alternative, change `GetTS` to be sync instead. – Alejandro Feb 11 '21 at 15:30
  • Ok Guys, decided to move away from Lazy... async/await is nice, but it force some logic in whole application... thanks all for help – Arsen Mkrtchyan Feb 11 '21 at 15:36
  • 1
    You may find this interesting: [Enforce an async method to be called once](https://stackoverflow.com/questions/28340177/enforce-an-async-method-to-be-called-once). It includes an implementation that doesn't use the `Lazy` class. – Theodor Zoulias Feb 11 '21 at 16:49
  • I found AsyncLazy useful, but it still does not solve issue non caching exceptions... however I have decided to go with it ... I found this discussion, at the latest post @StephenCleary suggested a way to solve that issue, however dont want to go so deep at this point. https://github.com/dotnet/runtime/issues/27510 – Arsen Mkrtchyan Feb 11 '21 at 17:29

2 Answers2

3

The problem actually has nothing to do with Lazy<T>. The deadlock is because it's blocking on asynchronous code.

In this code:

private TimeSpan GetDataAsync1()
{
  return GetTS().ConfigureAwait(false).GetAwaiter().GetResult();
}

the ConfigureAwait(false) does nothing. ConfigureAwait configures awaits, not tasks, and there is no await there.

The best option is to go async all the way. If exceptions are a concern, you can use AsyncLazy<T> and pass AsyncLazyFlags.RetryOnFailure.

If you can't go async all the way, the next best option would be to go synchronous all the way. If you can't do either of these, then you'll have to choose a sync-over-async hack; be aware that there is no hack that works in all situations.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks @StephenCleary, this really make sense. Didnt know about AsyncLazyFlags.RetryOnFailure, that is exactly what I am asking. I understand that the problem is not in Lazy side, but that not all FCL classes are easy suitable to async staff – Arsen Mkrtchyan Feb 12 '21 at 17:26
1

Actually I found no easy way of doing this, but the feature request discussion in .net github gives a complete picture of current situation

GitHub: Add async support to System.Lazy

Actually 5th point from last @StephenCleary answers my question for exception caching

  1. Resetting on exceptions is also an important use case. I've addressed this by adding another async lazy flag that will reset the AsyncLazy to an uninitialized state if an exception is thrown from the delegate. All existing accessors see the exception, but the next accessor will retry the delegate.
Arsen Mkrtchyan
  • 49,896
  • 32
  • 148
  • 184