为什么我的 F# 异步代码的第二个片段有效,而第一个片段却无效?
Why does my second snippet of F# async code work, but the first does not?
注意:我不是专业的软件开发人员,但我确实编写了很多不使用异步的代码,所以如果这个问题真的很直白,我深表歉意。
我正在与用 C# 编写的库进行交互。有一个特定的函数(让我们称之为 'func') returns a 'Threading.Tasks.Task>'
我正在用 F# 构建一个使用 func 的库。我在控制台应用程序中测试了以下代码,它运行良好。
let result =
func()
|> Async.AwaitTask
|> Async.RunSynchronously
|> Array.ofSeq
然而,当我从 WinForms 应用程序 运行 它时(这最终是我想要做的),执行块在表单代码中,从不 returns。
所以我弄乱了代码并尝试了以下方法,它起作用了。
let result =
async{
let! temp =
func()
|> Async.AwaitTask
return temp
} |> Async.RunSynchronously |> Array.ofSeq
为什么第一个代码段不起作用?为什么第二个片段有效? this page 上是否有任何内容可以回答这两个问题?如果是这样,这似乎并不明显。如果没有,你能告诉我哪里有吗?
您的第一个和第二个片段之间的区别在于 AwaitTask
调用发生在 不同的线程上 。
试试这个来验证:
let printThread() = printfn "%d" System.Threading.Thread.CurrentThread.ManagedThreadId
let result =
printThread()
func()
|> Async.AwaitTask
|> Async.RunSynchronously
|> Array.ofSeq
let res2 =
printThread()
async {
printThread()
let! temp = func() |> Async.AwaitTask
return temp
} |> Async.RunSynchronously |> Array.ofSeq
当您 运行 res2
时,您将得到两行输出,上面有两个不同的数字。 async
运行 内部所在的线程与 res2
本身 运行 所在的线程不同。深入 async
会使您处于不同的线程。
现在,这与 .NET TPL 任务的实际工作方式相互作用。当你去等待一个任务时,你不只是在一些随机线程上得到回调,哦不!相反,您的回调是通过 "current" SynchronizationContext
安排的。这是一种特殊的野兽,总有一个 "current" 一个(可通过静态访问 属性 - 谈论全局状态!),你可以要求它安排事情 "in the same context" ,其中 "same context" 的概念由实现定义。
WinForms 当然有自己的实现,恰如其分地命名为 WindowsFormsSynchronizationContext
. When you're running within a WinForms event handler, and you ask the current context to schedule something, it will be scheduled using the WinForms' own event loop - a la Control.Invoke
。
但是当然,由于您用 Async.RunSynchronously
阻塞了事件循环的线程,等待任务永远不会发生。你在等它发生,它也在等你释放线程。又名 "deadlock".
要解决此问题,您需要在不同的线程上启动等待,以便不使用 WinForms 的同步上下文 - 您不小心偶然发现的解决方案。
另一种推荐的解决方案是通过 Task.ConfigureAwait
:
明确告诉 TPL 不要使用 "current" 上下文
let result =
func().ConfigureAwait( continueOnCapturedContext = false )
|> Async.AwaitTask
|> Async.RunSynchronously
|> Array.ofSeq
不幸的是,这不会编译,因为 Async.AwaitTask
需要 Task
,而 ConfigureAwait
returns 需要 ConfiguredTaskAwaitable
.
注意:我不是专业的软件开发人员,但我确实编写了很多不使用异步的代码,所以如果这个问题真的很直白,我深表歉意。
我正在与用 C# 编写的库进行交互。有一个特定的函数(让我们称之为 'func') returns a 'Threading.Tasks.Task>'
我正在用 F# 构建一个使用 func 的库。我在控制台应用程序中测试了以下代码,它运行良好。
let result =
func()
|> Async.AwaitTask
|> Async.RunSynchronously
|> Array.ofSeq
然而,当我从 WinForms 应用程序 运行 它时(这最终是我想要做的),执行块在表单代码中,从不 returns。
所以我弄乱了代码并尝试了以下方法,它起作用了。
let result =
async{
let! temp =
func()
|> Async.AwaitTask
return temp
} |> Async.RunSynchronously |> Array.ofSeq
为什么第一个代码段不起作用?为什么第二个片段有效? this page 上是否有任何内容可以回答这两个问题?如果是这样,这似乎并不明显。如果没有,你能告诉我哪里有吗?
您的第一个和第二个片段之间的区别在于 AwaitTask
调用发生在 不同的线程上 。
试试这个来验证:
let printThread() = printfn "%d" System.Threading.Thread.CurrentThread.ManagedThreadId
let result =
printThread()
func()
|> Async.AwaitTask
|> Async.RunSynchronously
|> Array.ofSeq
let res2 =
printThread()
async {
printThread()
let! temp = func() |> Async.AwaitTask
return temp
} |> Async.RunSynchronously |> Array.ofSeq
当您 运行 res2
时,您将得到两行输出,上面有两个不同的数字。 async
运行 内部所在的线程与 res2
本身 运行 所在的线程不同。深入 async
会使您处于不同的线程。
现在,这与 .NET TPL 任务的实际工作方式相互作用。当你去等待一个任务时,你不只是在一些随机线程上得到回调,哦不!相反,您的回调是通过 "current" SynchronizationContext
安排的。这是一种特殊的野兽,总有一个 "current" 一个(可通过静态访问 属性 - 谈论全局状态!),你可以要求它安排事情 "in the same context" ,其中 "same context" 的概念由实现定义。
WinForms 当然有自己的实现,恰如其分地命名为 WindowsFormsSynchronizationContext
. When you're running within a WinForms event handler, and you ask the current context to schedule something, it will be scheduled using the WinForms' own event loop - a la Control.Invoke
。
但是当然,由于您用 Async.RunSynchronously
阻塞了事件循环的线程,等待任务永远不会发生。你在等它发生,它也在等你释放线程。又名 "deadlock".
要解决此问题,您需要在不同的线程上启动等待,以便不使用 WinForms 的同步上下文 - 您不小心偶然发现的解决方案。
另一种推荐的解决方案是通过 Task.ConfigureAwait
:
let result =
func().ConfigureAwait( continueOnCapturedContext = false )
|> Async.AwaitTask
|> Async.RunSynchronously
|> Array.ofSeq
不幸的是,这不会编译,因为 Async.AwaitTask
需要 Task
,而 ConfigureAwait
returns 需要 ConfiguredTaskAwaitable
.