我正在尝试下载从我博客的xml备份引用的3000多张照片。我遇到的问题是,如果只有其中一张照片不再可用,整个异步会被阻止,因为AsyncGetResponse不会超时。
ildjarn帮助我整理了一个版本的AsyncGetResponse,它在超时时失败了,但使用它会带来很多更多超时 - 好像刚刚排队的请求超时一样。似乎所有的WebRequests都是“立即”启动的,唯一让它工作的方法是将超时设置为下载所有所有所需的时间:这不是很好,因为它意味着我根据图像数量调整了超时。
我是否达到了香草async
的极限?我应该考虑反应性扩展吗?
答案 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
的最佳实现方式。但是易用性非常好,因为我的应用程序不需要顶级性能,所以它对我来说效果很好。虽然它是一个图书馆,如果有人知道如何使它变得更好,那么使图书馆成为最佳表现者总是一件好事,所以我们其他人可以只使用有效的代码并完成我们的工作!