F#中的“限制”异步下载

时间:2011-06-02 20:01:23

标签: asynchronous f# timeout

我正在尝试下载从我博客的xml备份引用的3000多张照片。我遇到的问题是,如果只有其中一张照片不再可用,整个异步会被阻止,因为AsyncGetResponse不会超时。

ildjarn帮助我整理了一个版本的AsyncGetResponse,它在超时时失败了,但使用它会带来很多更多超时 - 好像刚刚排队的请求超时一样。似乎所有的WebRequests都是“立即”启动的,唯一让它工作的方法是将超时设置为下载所有所有所需的时间:这不是很好,因为它意味着我根据图像数量调整了超时。

我是否达到了香草async的极限?我应该考虑反应性扩展吗?

这有点令人尴尬,因为我已经在这个特定的代码位上问了two questions,我仍然没有按照我想要的方式工作!

4 个答案:

答案 0 :(得分:9)

我认为必须有更好的方法来确定文件不可用而不是使用超时。我不完全确定,但如果找不到文件,是否有某种方法可以使它抛出异常?然后你可以将async代码包装在try .. with内,你应该避免大部分问题。

无论如何,如果你想编写自己的“并发管理器”并行运行一定数量的请求并将剩余的待处理请求排队,那么F#中最简单的选择就是使用代理(MailboxProcessor类型)。以下对象封装了行为:

type ThrottlingAgentMessage = 
  | Completed
  | Work of Async<unit>

/// Represents an agent that runs operations in concurrently. When the number
/// of concurrent operations exceeds 'limit', they are queued and processed later
type ThrottlingAgent(limit) = 
  let agent = MailboxProcessor.Start(fun agent -> 
    /// Represents a state when the agent is blocked
    let rec waiting () = 
      // Use 'Scan' to wait for completion of some work
      agent.Scan(function
        | Completed -> Some(working (limit - 1))
        | _ -> None)
    /// Represents a state when the agent is working
    and working count = async { 
      while true do
        // Receive any message 
        let! msg = agent.Receive()
        match msg with 
        | Completed -> 
            // Decrement the counter of work items
            return! working (count - 1)
        | Work work ->
            // Start the work item & continue in blocked/working state
            async { try do! work 
                    finally agent.Post(Completed) }
            |> Async.Start
            if count < limit then return! working (count + 1)
            else return! waiting () }
    working 0)      

  /// Queue the specified asynchronous workflow for processing
  member x.DoWork(work) = agent.Post(Work work)

答案 1 :(得分:6)

没有什么比这更容易了。 :)

我认为你遇到的问题是问题领域的固有问题(而不仅仅是异步编程模型的问题,尽管它们确实有些交互)。

假设您要下载3000张图片。首先,在您的.NET进程中,有一些类似于System.Net.ConnectionLimit或者我忘记名称的东西,例如限制.NET进程可以同时运行的同时HTTP连接数(我认为默认值只是'2')。因此,您可以找到该控件并将其设置为更高的数字,这将有所帮助。

但接下来,您的机器和互联网连接的带宽有限。因此,即使您可以尝试同时启动3000个HTTP连接,每个单独的连接也会因带宽管道限制而变慢。所以这也会与超时相互作用。 (这甚至不考虑服务器上有哪种类型的限制/限制。也许如果你发送3000个请求,它会认为你是DoS攻击并将你的IP列入黑名单。)

所以这确实是一个问题领域,好的解决方案需要一些智能限制和流量控制才能管理底层系统资源的使用方式。

正如在另一个答案中那样,F#代理(MailboxProcessors)是一个很好的编程模型,用于创作这种限制/流控制逻辑。

(尽管如此,如果大多数图片文件都像1MB,但那里混合了1GB文件,那么这个单一文件可能会超时。)

无论如何,这不是一个问题的答案,只是指出问题领域本身有多少内在的复杂性。 (也许它也暗示了为什么UI'下载管理器'如此受欢迎。)

答案 2 :(得分:4)

这是Tomas答案的变体,因为我需要一个可以返回结果的代理。

type ThrottleMessage<'a> = 
    | AddJob of (Async<'a>*AsyncReplyChannel<'a>) 
    | DoneJob of ('a*AsyncReplyChannel<'a>) 
    | Stop

/// This agent accumulates 'jobs' but limits the number which run concurrently.
type ThrottleAgent<'a>(limit) =    
    let agent = MailboxProcessor<ThrottleMessage<'a>>.Start(fun inbox ->
        let rec loop(jobs, count) = async {
            let! msg = inbox.Receive()  //get next message
            match msg with
            | AddJob(job) -> 
                if count < limit then   //if not at limit, we work, else loop
                    return! work(job::jobs, count)
                else
                    return! loop(job::jobs, count)
            | DoneJob(result, reply) -> 
                reply.Reply(result)           //send back result to caller
                return! work(jobs, count - 1) //no need to check limit here
            | Stop -> return () }
        and work(jobs, count) = async {
            match jobs with
            | [] -> return! loop(jobs, count) //if no jobs left, wait for more
            | (job, reply)::jobs ->          //run job, post Done when finished
                async { let! result = job 
                        inbox.Post(DoneJob(result, reply)) }
                |> Async.Start
                return! loop(jobs, count + 1) //job started, go back to waiting
        }
        loop([], 0)
    )
    member m.AddJob(job) = agent.PostAndAsyncReply(fun rep-> AddJob(job, rep))
    member m.Stop() = agent.Post(Stop)

在我的特定情况下,我只需要将它用作“一次性”'地图',所以我添加了一个静态函数:

    static member RunJobs limit jobs = 
        let agent = ThrottleAgent<'a>(limit)
        let res = jobs |> Seq.map (fun job -> agent.AddJob(job))
                       |> Async.Parallel
                       |> Async.RunSynchronously
        agent.Stop()
        res

似乎正常工作......

答案 3 :(得分:0)

这是一个开箱即用的解决方案:

FSharpx.Control offers an Async.ParallelWithThrottle function。我不确定它是否是it uses SemaphoreSlim的最佳实现方式。但是易用性非常好,因为我的应用程序不需要顶级性能,所以它对我来说效果很好。虽然它是一个图书馆,如果有人知道如何使它变得更好,那么使图书馆成为最佳表现者总是一件好事,所以我们其他人可以只使用有效的代码并完成我们的工作!