Asynchronous &引用;节流;异步下载#

Asynchronous &引用;节流;异步下载#,asynchronous,f#,timeout,Asynchronous,F#,Timeout,我正在尝试下载从我博客的xml备份中引用的3000多张照片。我遇到的问题是,如果这些照片中只有一张不再可用,整个异步就会被阻止,因为AsyncGetResponse不会超时 帮助我创建了一个版本的AsyncGetResponse,它在超时时会失败,但使用它会导致更多的超时,就好像请求只是排队超时一样。看起来所有的WebRequests都是“立即”启动的,唯一的方法是将超时设置为下载所有请求所需的时间:这并不好,因为这意味着我已经根据图像的数量调整了超时 我是否达到了香草async的极限?我应该转

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

帮助我创建了一个版本的AsyncGetResponse,它在超时时会失败,但使用它会导致更多的超时,就好像请求只是排队超时一样。看起来所有的WebRequests都是“立即”启动的,唯一的方法是将超时设置为下载所有请求所需的时间:这并不好,因为这意味着我已经根据图像的数量调整了超时

我是否达到了香草
async
的极限?我应该转而考虑被动扩展吗


这有点尴尬,因为我已经在这里询问了这段特定的代码,但我仍然没有让它按照我想要的方式工作

我认为一定有比使用超时更好的方法来发现文件不可用。我不太确定,但是如果找不到文件,有没有办法让它抛出异常?然后您可以将
async
代码包装到
try。。使用
,您应该避免大多数问题

无论如何,如果您想编写自己的“并发管理器”,并行运行一定数量的请求并对剩余的未决请求进行排队,那么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)
类型ThrottlingAgentMessage=
|完成
|异步工作
///表示在中同时运行操作的代理。号码是什么时候
///超过“限制”的并发操作数,它们将排队并稍后处理
类型节流剂(限值)=
让代理=MailboxProcessor.Start(有趣的代理->
///表示代理被阻止时的状态
让rec等待()=
//使用“扫描”等待某些工作完成
代理扫描(功能)
|已完成->部分(正在工作(限制-1))
|(无)
///表示代理工作时的状态
和工作计数=异步{
尽管如此
//收到任何消息
让!msg=agent.Receive()
配味精
|已完成->
//减少工作项的计数器
返回!正在工作(计数-1)
|工作->
//启动工作项并在阻止/工作状态下继续
异步{尝试做!工作
最后,agent.Post(已完成)}
|>异步启动
如果计数<限制,则返回!工作(计数+1)
否则返回!正在等待()}
工作(0)
///将指定的异步工作流排队以进行处理
成员x.DoWork(工作)=代理岗位(工作)
没有什么是容易的。:)

我认为您遇到的问题是问题域的固有问题(而不仅仅是异步编程模型的问题,尽管它们之间确实存在一些交互)

假设你想下载3000张图片。首先,在您的.NET进程中,有一个类似System.NET.ConnectionLimit或我忘记其名称的东西,它将限制您的.NET进程可以同时运行的同时HTTP连接的数量(我认为默认值仅为“2”)。因此,您可以找到该控件并将其设置为更高的数字,这会有所帮助

但接下来,你的机器和互联网连接的带宽是有限的。因此,即使您可以尝试同时启动3000个HTTP连接,每个单独的连接也会因带宽管道限制而变慢。因此,这也会与超时相互作用。(这甚至不考虑服务器上有什么样的节流/限制。也许如果你发送3000个请求,它会认为你是DoS攻击,黑名单你的IP。) 因此,这确实是一个问题域,一个好的解决方案需要一些智能节流和流量控制,以便管理底层系统资源的使用方式

与另一个答案一样,F#Agent(MailboxProcessors)是编写此类节流/流量控制逻辑的良好编程模型

(即使如此,如果大多数图片文件都是1MB,但其中混入了1GB文件,那么单个文件可能会超时。)


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

这里是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)
类型ThrottleMessage*AsyncReplyChannel(限制)=
let agent=MailboxProcessor(限制)
让res=jobs |>Seq.map(乐趣作业->代理.AddJob(作业))
|>异步并行
|>异步运行
代理。停止()
物件

它似乎工作正常…

这里有一个现成的解决方案:


。我不确定这是否是最好的实现。但它的易用性非常好,而且由于我的应用程序不需要最高性能,所以对我来说,它工作得足够好。尽管它是一个库,但如果有人知道如何使它更好,那么让库在开箱即用的情况下表现最好总是一件好事,这样我们其他人就可以使用有效的代码,完成我们的工作

听起来好像少了一些“队列”。。。我不知道F#或它是如何工作的
async
,但如果它符合模型的话,使用CPS编写应该很容易。听起来越来越像你想要
MailboxProcessor
而不是直接使用
async
。你说有些照片不可用,那么服务器应该返回404(未找到)对吗?我的意思是,这不应该是超时之类的情况
    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