5

I have a set of F# scripts that call various libraries that we have created, many of them exposing asynchronous methods originally written in C#. Recently I found out the scripts stopped working (I think it's about half a year since I used them last time and they worked back then).

I was trying to isolate the problem and came up with the following code that reproduces it:

First, let's consider a library containing the following C# class:

    public class AsyncClass
    {
        public async Task<string> GetStringAsync()
        {
            var uri = new Uri("https://www.google.com");
            var client = new HttpClient();
            var response = await client.GetAsync(uri);
            var body = await response.Content.ReadAsStringAsync();
            return body;
        }
    }

Next, let's call the library from F# (FSX script) using the following code:

let asyncClient = AsyncClass()

let strval1 = asyncClient.GetStringAsync() |> Async.AwaitTask |> Async.RunSynchronously
printfn "%s" strval1

let strval2 = 
    async {
        return! asyncClient.GetStringAsync() |> Async.AwaitTask
    } |> Async.RunSynchronously
printfn "%s" strval2

Obtaining strval1 ends up with a deadlock, whereas strval2 is retrieved just fine (I am quite sure the first scenario used to work too a couple of months ago so it looks like some sort of an update might have caused this).

This is most likely a synchronisation context issue where the thread is basically "waiting for itself to finish", but I don't understand what exactly is wrong with the first call - I can't see anything wrong with it.

Similar issues on StackOverflow:

zidour
  • 83
  • 4
  • @MarkusDeibel It's an example to show what is working in contrast to what is not. OP expected the two to be interchangable ( behave the same way). – Fildor Sep 10 '19 at 11:21
  • That's correct, @Fildor, I assume both to be working fine (although I am not saying they are completely equivalent as to their inner workings). – zidour Sep 10 '19 at 11:24
  • 3
    @zidour if you put ```Console.WriteLine($"Current context: {SynchronizationContext.Current}.");``` before GetAsync you will see that in first case current sync context is WindowsFormsSynchronizationContext, while in second it is null (ThreadPool). WindowsFormsSynchronizationContext - single UI thread - which is blocked on await. – dvitel Sep 10 '19 at 13:30
  • 1
    Thanks @dvitel, that is indeed true. I think the question can be rephrased as why is the first example not legit and not guaranteed to work? – zidour Sep 10 '19 at 14:07
  • 1
    @zidour - you can fix default sync context. In settings.json (for workspace in .ionide folder or on user level) add line: ```"FSharp.fsiExtraParameters": ["--gui-"]``` as stated [here](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/fsharp-interactive-options). Then you do not need to change your code. I assume --gui+ became default from some version of fsi.exe – dvitel Sep 10 '19 at 16:18
  • After reading the documentation and a couple of articles I would conclude that the first example in my post is not safe and should not be used (and I was just lucky before). To me it looks like it's an equivalent of calling `asyncClient.GetStringAsync().Result` on a GUI thread which always leads to a deadlock. – zidour Sep 12 '19 at 08:12
  • Documentation for `Async.RunSynchronously` states that it should not be used on the main thread in async programming environments, also [here](https://stackoverflow.com/questions/3539937/how-does-fs-async-really-work) it's suggested the method should not be used more than once in any app. I.e. the app/script should be async all the way down from the top level and Async.RunSynchronously only used once on the top level (unless fully async options are available). I was routinely using the function many times in my scripts. It would be good if someone could confirm my understanding though. – zidour Sep 12 '19 at 08:23

1 Answers1

0

So a .net Task will start immediately, while F# async {} is lazy. So when you wrap a task inside an async { } it becomes lazy, and thus will have the characteristics that Async.RunSynchronously is expecting.

Generally I use async {} when I'm doing f# asynchronous operations only, and if I'm working with .net Tasks I'll use TaskBuilder.fs (available in nuget). It's more aware of Task idiosyncrasies like ConfigureAwait(continueOnCapturedContext: false).

open FSharp.Control.Tasks.V2.ContextInsensitive

task {
    let! x = asyncClient.GetStringAsync()
    //do something with x
}
jbtule
  • 31,383
  • 12
  • 95
  • 128