取消的任务不 return 控制异步块
Canceled Task does not return control to async block
我试图将其减少到尽可能小的重现,但它仍然有点长,我很抱歉。
我有一个 F# 项目引用了一个 C# 项目,代码如下。
public static class CSharpClass {
public static async Task AsyncMethod(CancellationToken cancellationToken) {
await Task.Delay(3000);
cancellationToken.ThrowIfCancellationRequested();
}
}
这是 F# 代码。
type Message =
| Work of CancellationToken
| Quit of AsyncReplyChannel<unit>
let mkAgent() = MailboxProcessor.Start <| fun inbox ->
let rec loop() = async {
let! msg = inbox.TryReceive(250)
match msg with
| Some (Work cancellationToken) ->
let! result =
CSharpClass.AsyncMethod(cancellationToken)
|> Async.AwaitTask
|> Async.Catch
// THIS POINT IS NEVER REACHED AFTER CANCELLATION
match result with
| Choice1Of2 _ -> printfn "Success"
| Choice2Of2 exn -> printfn "Error: %A" exn
return! loop()
| Some (Quit replyChannel) -> replyChannel.Reply()
| None -> return! loop()
}
loop()
[<EntryPoint>]
let main argv =
let agent = mkAgent()
use cts = new CancellationTokenSource()
agent.Post(Work cts.Token)
printfn "Press any to cancel."
System.Console.Read() |> ignore
cts.Cancel()
printfn "Cancelled."
agent.PostAndReply Quit
printfn "Done."
System.Console.Read()
问题是,取消后,控制永远不会 return 到异步块。我不确定它是挂在 AwaitTask
还是 Catch
中。直觉告诉我它在尝试 return 到以前的同步上下文时阻塞,但我不确定如何确认这一点。我正在寻找有关如何解决此问题的想法,或者在这里有更深入了解的人可能会发现问题。
可能的解决方案
let! result =
Async.FromContinuations(fun (cont, econt, _) ->
let ccont e = econt e
let work = CSharpClass.AsyncMethod(cancellationToken) |> Async.AwaitTask
Async.StartWithContinuations(work, cont, econt, ccont))
|> Async.Catch
最终导致此行为的原因是取消在 F# Async 中很特殊。取消有效地转化为 stop and teardown. As you can see in the source,Task
中的取消使其完全脱离计算。
如果您想要旧的 OperationCanceledException
可以作为计算的一部分进行处理,我们可以自己制作。
type Async =
static member AwaitTaskWithCancellations (task: Task<_>) =
Async.FromContinuations(fun (setResult, setException, setCancelation) ->
task.ContinueWith(fun (t:Task<_>) ->
match t.Status with
| TaskStatus.RanToCompletion -> setResult t.Result
| TaskStatus.Faulted -> setException t.Exception
| TaskStatus.Canceled -> setException <| OperationCanceledException()
| _ -> ()
) |> ignore
)
取消现在只是另一种异常 - 我们可以处理异常。
这是重现:
let tcs = TaskCompletionSource<unit>()
tcs.SetCanceled()
async {
try
let! result = tcs.Task |> Async.AwaitTaskWithCancellations
return result
with
| :? OperationCanceledException ->
printfn "cancelled"
| ex -> printfn "faulted %A" ex
()
} |> Async.RunSynchronously
我试图将其减少到尽可能小的重现,但它仍然有点长,我很抱歉。
我有一个 F# 项目引用了一个 C# 项目,代码如下。
public static class CSharpClass {
public static async Task AsyncMethod(CancellationToken cancellationToken) {
await Task.Delay(3000);
cancellationToken.ThrowIfCancellationRequested();
}
}
这是 F# 代码。
type Message =
| Work of CancellationToken
| Quit of AsyncReplyChannel<unit>
let mkAgent() = MailboxProcessor.Start <| fun inbox ->
let rec loop() = async {
let! msg = inbox.TryReceive(250)
match msg with
| Some (Work cancellationToken) ->
let! result =
CSharpClass.AsyncMethod(cancellationToken)
|> Async.AwaitTask
|> Async.Catch
// THIS POINT IS NEVER REACHED AFTER CANCELLATION
match result with
| Choice1Of2 _ -> printfn "Success"
| Choice2Of2 exn -> printfn "Error: %A" exn
return! loop()
| Some (Quit replyChannel) -> replyChannel.Reply()
| None -> return! loop()
}
loop()
[<EntryPoint>]
let main argv =
let agent = mkAgent()
use cts = new CancellationTokenSource()
agent.Post(Work cts.Token)
printfn "Press any to cancel."
System.Console.Read() |> ignore
cts.Cancel()
printfn "Cancelled."
agent.PostAndReply Quit
printfn "Done."
System.Console.Read()
问题是,取消后,控制永远不会 return 到异步块。我不确定它是挂在 AwaitTask
还是 Catch
中。直觉告诉我它在尝试 return 到以前的同步上下文时阻塞,但我不确定如何确认这一点。我正在寻找有关如何解决此问题的想法,或者在这里有更深入了解的人可能会发现问题。
可能的解决方案
let! result =
Async.FromContinuations(fun (cont, econt, _) ->
let ccont e = econt e
let work = CSharpClass.AsyncMethod(cancellationToken) |> Async.AwaitTask
Async.StartWithContinuations(work, cont, econt, ccont))
|> Async.Catch
最终导致此行为的原因是取消在 F# Async 中很特殊。取消有效地转化为 stop and teardown. As you can see in the source,Task
中的取消使其完全脱离计算。
如果您想要旧的 OperationCanceledException
可以作为计算的一部分进行处理,我们可以自己制作。
type Async =
static member AwaitTaskWithCancellations (task: Task<_>) =
Async.FromContinuations(fun (setResult, setException, setCancelation) ->
task.ContinueWith(fun (t:Task<_>) ->
match t.Status with
| TaskStatus.RanToCompletion -> setResult t.Result
| TaskStatus.Faulted -> setException t.Exception
| TaskStatus.Canceled -> setException <| OperationCanceledException()
| _ -> ()
) |> ignore
)
取消现在只是另一种异常 - 我们可以处理异常。 这是重现:
let tcs = TaskCompletionSource<unit>()
tcs.SetCanceled()
async {
try
let! result = tcs.Task |> Async.AwaitTaskWithCancellations
return result
with
| :? OperationCanceledException ->
printfn "cancelled"
| ex -> printfn "faulted %A" ex
()
} |> Async.RunSynchronously