如果其中之一存在异常,Async.Parallel是否会取消所有作业?

时间:2019-09-05 06:47:45

标签: .net asynchronous f#

我很想找到F Task.WhenAny(相当于Async<'T>而不是Task<'T>的F#),但是我发现最接近的是Async.Choice,此实现要求作业返回Option<'T>而不是返回'T,所以我写了我自己的Async.WhenAny(通过我在互联网上找到的一些摘要对其进行了修改):

type internal ResultWrapper<'T>(value : 'T) =
    inherit Exception()

    member self.Value = value

module AsyncExtensions =

    let private RaiseResult (e: ResultWrapper<'T>) =
        Async.FromContinuations(fun (_, econt, _) -> econt e)

    // like Async.Choice, but with no need for Option<T> types
    let WhenAny<'T>(jobs: seq<Async<'T>>): Async<'T> =
        let wrap job =
            async {
                let! res = job
                return! RaiseResult <| ResultWrapper res
            }

        async {
            try
                do!
                    jobs
                    |> Seq.map wrap
                    |> Async.Parallel
                    |> Async.Ignore

                // unreachable
                return failwith "No successful result?"
            with
            | :? ResultWrapper<'T> as ex ->
                return ex.Value
        }

这是一个非常简单的实现,完全看不到取消(至少与Async.Choice相比)。

但是,我发现它仍然可以以某种方式取消较慢的工作,而且我不明白为什么。是因为Async.Parallel是如何在幕后工作的?

注意:要找出较慢的作业是否被AsyncChoice或AsyncWhenAny取消,我编写了此单元测试:

[<Test>]
member __.``AsyncExtensions-WhenAny job cancellation``() =
    let shortJobRes = 1
    let shortTime = TimeSpan.FromSeconds 2.
    let shortJob = async {
        do! Async.Sleep (int shortTime.TotalMilliseconds)
        return shortJobRes
    }

    let longJobRes = 2
    let mutable longJobFinished = false
    let longTime = TimeSpan.FromSeconds 3.
    let longJob = async {
        do! Async.Sleep (int longTime.TotalMilliseconds)
        longJobFinished <- true
        return longJobRes
    }

    let result =
        AsyncExtensions.WhenAny [longJob; shortJob]
        |> Async.RunSynchronously

    Assert.That(result, Is.EqualTo shortJobRes)
    Assert.That(longJobFinished, Is.EqualTo false, "#before")
    Threading.Thread.Sleep(TimeSpan.FromSeconds 7.0)
    Assert.That(longJobFinished, Is.EqualTo false, "#after")

还有Async.Parallel:

[<Test>]
member __.``AsyncParallel cancels all jobs if there's an exception in one?``() =
    let shortJobRes = 1
    let shortTime = TimeSpan.FromSeconds 2.
    let shortJob = async {
        do! Async.Sleep (int shortTime.TotalMilliseconds)
        return failwith "foo"
    }

    let longJobRes = 2
    let mutable longJobFinished = false
    let longTime = TimeSpan.FromSeconds 3.
    let longJob = async {
        do! Async.Sleep (int longTime.TotalMilliseconds)
        longJobFinished <- true
        return longJobRes
    }

    let result =
        try
            Async.Parallel [longJob; shortJob]
            |> Async.RunSynchronously |> Some
        with
        | _ -> None

    Assert.That(result, Is.EqualTo None)
    Assert.That(longJobFinished, Is.EqualTo false, "#before")
    Threading.Thread.Sleep(TimeSpan.FromSeconds 7.0)
    Assert.That(longJobFinished, Is.EqualTo false, "#after")

1 个答案:

答案 0 :(得分:5)

look at the source代表Async.Parallel。实际阅读并理解它可能需要熟悉F#Async的工作原理,但值得庆幸的是,第1220行有一个方便的注释:

// Attempt to cancel the individual operations if an exception happens on any of the other threads

以下代码也不难理解:然后创建一个取消令牌,并将该令牌传递给它并行启动的所有异步操作。如果它们中的任何一个抛出异常,则该令牌被取消,这意味着所有并行操作都被取消。因此,注释准确地反映了代码的其余部分,并且该答案基本上是对您在问题标题中提出的问题说“是”的漫长方式。 :-)