(坚持使用异步获取许多网页的常见示例)
如何异步分离多个(数百个)网页请求,然后在进入下一步之前等待所有请求完成? Async.AsParallel一次处理几个请求,由CPU上的核心数控制。抓取网页不是CPU绑定操作。不满意Async.AsParallel的加速,我正在寻找替代方案。
我试图连接Async.StartAsTask和Task []之间的点.WaitAll。本能地,我编写了以下代码,但它没有编译。
let processItemsConcurrently (items : int seq) =
let tasks = items |> Seq.map (fun item -> Async.StartAsTask(fetchAsync item))
Tasks.Task.WaitAll(tasks)
你会怎么做?
答案 0 :(得分:8)
Async.Parallel
几乎肯定就在这里。不确定你不满意的是什么; F#asyncs的优势在于异步计算比在任务并行CPU绑定的东西(更适合于Task
和.NET 4.0 TPL)。这是一个完整的例子:
open System.Diagnostics
open System.IO
open System.Net
open Microsoft.FSharp.Control.WebExtensions
let sites = [|
"http://bing.com"
"http://google.com"
"http://cnn.com"
"http://stackoverflow.com"
"http://yahoo.com"
"http://msdn.com"
"http://microsoft.com"
"http://apple.com"
"http://nfl.com"
"http://amazon.com"
"http://ebay.com"
"http://expedia.com"
"http://twitter.com"
"http://reddit.com"
"http://hulu.com"
"http://youtube.com"
"http://wikipedia.org"
"http://live.com"
"http://msn.com"
"http://wordpress.com"
|]
let print s =
// careful, don't create a synchronization bottleneck by printing
//printf "%s" s
()
let printSummary info fullTimeMs =
Array.sortInPlaceBy (fun (i,_,_) -> i) info
// for i, size, time in info do
// printfn "%2d %7d %5d" i size time
let longest = info |> Array.map (fun (_,_,time) -> time) |> Array.max
printfn "longest request took %dms" longest
let bytes = info |> Array.sumBy (fun (_,size,_) -> float size)
let seconds = float fullTimeMs / 1000.
printfn "sucked down %7.2f KB/s" (bytes / 1024.0 / seconds)
let FetchAllSync() =
let allsw = Stopwatch.StartNew()
let info = sites |> Array.mapi (fun i url ->
let sw = Stopwatch.StartNew()
print "S"
let req = WebRequest.Create(url)
use resp = req.GetResponse()
use stream = resp.GetResponseStream()
use reader = new StreamReader(stream,
System.Text.Encoding.UTF8, true, 4096)
print "-"
let contents = reader.ReadToEnd()
print "r"
i, contents.Length, sw.ElapsedMilliseconds)
let time = allsw.ElapsedMilliseconds
printSummary info time
time, info |> Array.sumBy (fun (_,size,_) -> size)
let FetchAllAsync() =
let allsw = Stopwatch.StartNew()
let info = sites |> Array.mapi (fun i url -> async {
let sw = Stopwatch.StartNew()
print "S"
let req = WebRequest.Create(url)
use! resp = req.AsyncGetResponse()
use stream = resp.GetResponseStream()
use reader = new AsyncStreamReader(stream, // F# PowerPack
System.Text.Encoding.UTF8, true, 4096)
print "-"
let! contents = reader.ReadToEnd() // in F# PowerPack
print "r"
return i, contents.Length, sw.ElapsedMilliseconds })
|> Async.Parallel
|> Async.RunSynchronously
let time = allsw.ElapsedMilliseconds
printSummary info time
time, info |> Array.sumBy (fun (_,size,_) -> size)
// By default, I think .NET limits you to 2 open connections at once
ServicePointManager.DefaultConnectionLimit <- sites.Length
for i in 1..3 do // to warmup and show variance
let time1,r1 = FetchAllSync()
printfn "Sync took %dms, result was %d" time1 r1
let time2,r2 = FetchAllAsync()
printfn "Async took %dms, result was %d (speedup=%2.2f)"
time2 r2 (float time1/ float time2)
printfn ""
在我的4核盒子上,这始终提供近4倍的加速。
修改
在回复您的评论时,我已更新了代码。你是对的,因为我添加了更多的网站而且没有看到预期的加速(仍然稳定在4倍左右)。我已经开始在上面添加一些调试输出,将继续调查,看看是否有其他东西限制连接......
修改
再次编辑代码。好吧,我发现可能是瓶颈。以下是PowerPack中AsyncReadToEnd的实现:
type System.IO.StreamReader with
member s.AsyncReadToEnd () =
FileExtensions.UnblockViaNewThread (fun () -> s.ReadToEnd())
换句话说,它只是阻塞线程池线程并同步读取。哎呀!让我看看我是否可以解决这个问题。
修改
好的,PowerPack中的AsyncStreamReader做对了,我现在正在使用它。
但是,关键问题似乎是 variance 。
当你点击cnn.com时,很多时候结果会像500毫秒一样重新出现。但是每隔一段时间你就得到一个需要4s的请求,这当然可能会杀死明显的异步性,因为整个时间是不幸请求的时间。
运行上面的程序,我在家里的2核盒子上看到的速度从大约2.5倍到9倍。但是,它变化很大。我仍然错过了该计划中的一些瓶颈,但我认为网络的差异可能会解释我此时所看到的所有内容。
答案 1 :(得分:2)
使用.NET的Reactive Extensions结合F#,您可以编写一个非常优雅的解决方案 - 查看http://blog.paulbetts.org/index.php/2010/11/16/making-async-io-work-for-you-reactive-style/处的示例(这使用C#,但使用F#也很容易;关键是使用Begin / end方法而不是sync方法,即使你可以进行编译,它也会不必要地阻塞n
ThreadPool线程,而不是在它们进入时接收完成例程的Threadpool)
答案 2 :(得分:2)
我敢打赌,您遇到的加速速度并不足以满足您的需求,因为您要么使用WebRequest的子类型,要么使用依赖它的类(例如WebClient)。
如果是这种情况,您需要在ConnectionManagementElement上设置MaxConnection(我建议您只在需要时设置它,否则它将成为一项非常耗时的操作)到一个很高的值,具体取决于同时发生的数量你想从你的申请中发起的联系。
答案 3 :(得分:1)
我不是F#的人,但从纯粹的.NET角度来看,你正在寻找的是TaskFactory :: FromAsync,你在Task中包装的异步调用就像是HttpRequest :: BeginGetResponse。您还可以使用TaskCompletionSource包装WebClient公开的EAP模型。有关MSDN上这两个topics here的更多信息。
希望有了这些知识,你可以找到最接近的原生F#方法来完成你想要做的事情。
答案 4 :(得分:1)
这是一些避免未知数的代码,例如Web访问延迟。我的CPU利用率低于5%,同步和异步代码路径的效率约为60-80%。
open System.Diagnostics
let numWorkers = 200
let asyncDelay = 50
let main =
let codeBlocks = [for i in 1..numWorkers ->
async { do! Async.Sleep asyncDelay } ]
while true do
printfn "Concurrent started..."
let sw = new Stopwatch()
sw.Start()
codeBlocks |> Async.Parallel |> Async.RunSynchronously |> ignore
sw.Stop()
printfn "Concurrent in %d millisec" sw.ElapsedMilliseconds
printfn "efficiency: %d%%" (int64 (asyncDelay * 100) / sw.ElapsedMilliseconds)
printfn "Synchronous started..."
let sw = new Stopwatch()
sw.Start()
for codeBlock in codeBlocks do codeBlock |> Async.RunSynchronously |> ignore
sw.Stop()
printfn "Synchronous in %d millisec" sw.ElapsedMilliseconds
printfn "efficiency: %d%%" (int64 (asyncDelay * numWorkers * 100) / sw.ElapsedMilliseconds)
main