从 F# 调用 C# 异步方法会导致死锁

zid*_*our 5 c# f# asynchronous .net-core

我有一组 F# 脚本,它们调用我们创建的各种库,其中许多公开了最初用 C# 编写的异步方法。最近我发现脚本停止工作了(我想距离我上次使用它们大约有半年时间了,然后它们工作了)。

我试图隔离问题并想出了以下重现它的代码:

首先,让我们考虑一个包含以下 C# 类的库:

    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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

接下来,让我们使用以下代码从 F#(FSX 脚本)调用库:

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
Run Code Online (Sandbox Code Playgroud)

获取strval1以死锁告终,而strval2检索得很好(我很确定第一个场景在几个月前也曾经工作过,所以看起来可能是某种更新导致了这种情况)。

这很可能是一个同步上下文问题,其中线程基本上是“等待自己完成”,但我不明白第一次调用到底有什么问题 - 我看不出有什么问题。

StackOverflow 上的类似问题:

jbt*_*ule 0

所以 .netTask会立即启动,而 F#async {}是惰性的。因此,当您将任务包装在其中时async { },它会变得懒惰,因此将具有Async.RunSynchronously预期的特征。

一般来说,我仅在执行 f# 异步操作时使用 async {},如果我使用 .net 任务,我将使用TaskBuilder.fs(在 nuget 中提供)。它更了解Task诸如 之类的特质ConfigureAwait(continueOnCapturedContext: false)

open FSharp.Control.Tasks.V2.ContextInsensitive

task {
    let! x = asyncClient.GetStringAsync()
    //do something with x
}
Run Code Online (Sandbox Code Playgroud)