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

Posted

技术标签:

【中文标题】从 F# 调用 C# 异步方法会导致死锁【英文标题】:Calling C# async method from F# results in a deadlock 【发布时间】:2020-01-12 02:54:30 【问题描述】:

我有一组 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;
        
    

接下来,让我们使用以下代码从 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

获得strval1 最终会出现死锁,而strval2 被检索得很好(我很确定第一个场景在几个月前也可以正常工作,所以看起来某种更新可能会导致这个)。

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

*** 上的类似问题:

Why do I have to wrap an Async<T> into another async workflow and let! it? - 这似乎是同一个问题,但没有提供足够的信息,并且缺少一个简单的复制示例 Why is Async.RunSynchronously hanging? - 这很相似,但作者犯了一个明显的错误

【问题讨论】:

@MarkusDeibel 这是一个例子来展示什么是有效的,什么是无效的。 OP 期望两者可以互换(行为方式相同)。 没错,@Fildor,我认为两者都可以正常工作(尽管我并不是说它们的内部工作方式完全等效)。 @zidour 如果您将Console.WriteLine($"Current context: SynchronizationContext.Current."); 放在 GetAsync 之前,您将看到在第一种情况下当前同步上下文是 WindowsFormsSynchronizationContext,而在第二种情况下它为空(线程池)。 WindowsFormsSynchronizationContext - 单个 UI 线程 - 在等待时被阻塞。 感谢@dvitel,确实如此。我认为这个问题可以改写为为什么第一个示例不合法且不能保证有效? @zidour - 您可以修复默认同步上下文。在 settings.json 中(对于 .ionide 文件夹或用户级别的工作区)添加行:"FSharp.fsiExtraParameters": ["--gui-"],如 here 所述。然后你不需要改变你的代码。我假设 --gui+ 从某些版本的 fsi.exe 成为默认值 【参考方案1】:

所以 .net Task 将立即启动,而 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

【讨论】:

以上是关于从 F# 调用 C# 异步方法会导致死锁的主要内容,如果未能解决你的问题,请参考以下文章

C# :异步编程的注意点

如果异步调用不一定在不同的线程上执行,阻塞异步调用如何导致死锁?

使用嵌套异步调用锁定

C# async await 死锁问题总结

C#:在控制台应用程序的 Main 中调用异步方法导致编译失败 [关闭]

“从您的主线程调用它可能会导致死锁和/或 ANR,同时从 GoogleAuthUtil(Android 中的 Google Plus 集成)获取 accessToken”