F# 异步和 TPL 任务取消

F# async and TPL task cancellation

我正在调查各种 C# 异步方法和 F# 异步函数之间的方法调用链挂起程序的问题。我认为下面的 F# 脚本通过混合 F# async 和 TPL 重现了同样的问题:

open System
open System.Threading
open System.Threading.Tasks

let testy = async {
    let tokenSource = new CancellationTokenSource();

    Task.Run(fun () ->
        printfn "Inside first task"
        Thread.Sleep(1000)
        printfn "First task now cancelling the second task"
        tokenSource.Cancel()) |> ignore

    let! result =
        Task.Factory.StartNew<int>((fun () ->
            printfn "Inside second task"
            Thread.Sleep(2000)
            printfn "Second task about to throw task cancelled exception"
            tokenSource.Token.ThrowIfCancellationRequested()
            printfn "This is never reached, as expected"
            0),
            tokenSource.Token)
        |> Async.AwaitTask

    return result }

printfn "Starting"
let result = testy |> Async.StartAsTask |> Async.AwaitTask |> Async.RunSynchronously
printfn "Program ended with result: %d" result

运行这个程序在FSI中挂了解释器。在挂起之前我得到以下输出:

Starting
Inside first task
Inside second task
First task now cancelling the second task
Second task about to throw task cancelled exception

我发现如果我换行

let result = testy |> Async.StartAsTask |> Async.AwaitTask |> Async.RunSynchronously

let result = testy |> Async.RunSynchronously

然后它不再挂起并且 "OperationCanceledException" 按预期显示在 FSI 中,但我不知道为什么。

正在将取消令牌传递给 Task.Factory.StartNew。因此,当它被取消时,Async.StartAsTask 将永远不会启动并始终报告 WaitingForActivation 的状态。如果令牌未传递给 Task.Factory.StartNew,则状态将更改为 'Faulted',这将取消阻止 Async.AwaitTask,这将允许 Async.RunSynchronously 重新抛出异常。

修复

let result = testy |> Async.StartAsTask |> Async.AwaitTask |> Async.RunSynchronously

需要将相同的取消令牌传递给 Async.StartAsTask

let result = 
    Async.StartAsTask (testy, TaskCreationOptions.None, tokenSource.Token) 
    |> Async.AwaitTask 
    |> Async.RunSynchronously