【发布时间】: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 被检索得很好(我很确定第一个场景在几个月前也可以正常工作,所以看起来某种更新可能会导致这个)。
这很可能是一个同步上下文问题,其中线程基本上是“等待自己完成”,但我不明白第一次调用到底出了什么问题——我看不出它有什么问题。
StackOverflow 上的类似问题:
- 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 成为默认值
标签: c# asynchronous .net-core f#