Дорогое асинхронное чтение потока ответа - PullRequest
1 голос
/ 28 октября 2011

Последние пару дней я пытался выучить F # и продолжаю сталкиваться с тем, что меня озадачивает. Мой "учебный проект" - это скребок экрана для некоторых данных, которыми я немного интересуюсь для манипулирования.

В F # PowerPack происходит вызов Stream.AsyncReadToEnd. Я не хотел использовать PowerPack только для одного звонка, поэтому я посмотрел, как они это сделали.

module Downloader =
    open System
    open System.IO
    open System.Net
    open System.Collections

    type public BulkDownload(uriList : IEnumerable) =
        member this.UriList with get() = uriList

        member this.ParalellDownload() =
            let Download (uri : Uri) = async {
                let UnblockViaNewThread f = async {
                    do! Async.SwitchToNewThread()
                    let res = f()
                    do! Async.SwitchToThreadPool()
                    return res }

                let request = HttpWebRequest.Create(uri)
                let! response = request.AsyncGetResponse()
                use responseStream = response.GetResponseStream()
                use reader = new StreamReader(responseStream)
                let! contents = UnblockViaNewThread (fun() -> reader.ReadToEnd())
                return uri, contents.ToString().Length }

            this.UriList
            |> Seq.cast
            |> Seq.map Download
            |> Async.Parallel
            |> Async.RunSynchronously

У них есть эта функция UnblockViaNewThread. Это действительно единственный способ асинхронного чтения потока ответа? Не создает ли новый поток действительно дорого (я видел "~ 1 Мб памяти", разбросанные повсюду). Есть лучший способ сделать это? Это то, что действительно происходит в каждом Async* звонке (тот, который я могу let!)?

РЕДАКТИРОВАТЬ: Я следую советам Томаса и на самом деле придумал что-то независимое от F # PowerTools. Вот. Это действительно требует обработки ошибок, но оно асинхронно запрашивает и загружает URL в байтовый массив.

namespace Downloader
open System
open System.IO
open System.Net
open System.Collections

type public BulkDownload(uriList : IEnumerable) =
    member this.UriList with get() = uriList

    member this.ParalellDownload() =                
        let Download (uri : Uri) = async {
            let processStreamAsync (stream : Stream) = async { 
                let outputStream = new MemoryStream()
                let buffer = Array.zeroCreate<byte> 0x1000
                let completed = ref false
                while not (!completed) do
                    let! bytesRead = stream.AsyncRead(buffer, 0, 0x1000)
                    if bytesRead = 0 then
                        completed := true
                    else
                        outputStream.Write(buffer, 0, bytesRead)
                stream.Close()
                return outputStream.ToArray() }

            let request = HttpWebRequest.Create(uri)
            let! response = request.AsyncGetResponse()
            use responseStream = response.GetResponseStream()
            let! contents = processStreamAsync responseStream
            return uri, contents.Length }

        this.UriList
        |> Seq.cast
        |> Seq.map Download
        |> Async.Parallel
        |> Async.RunSynchronously

    override this.ToString() = String.Join(", ", this.UriList)

1 Ответ

9 голосов
/ 28 октября 2011

Я думаю, что AsyncReadToEnd, который просто синхронно вызывает ReadToEnd в отдельном потоке, неправильный.

F # PowerPack также содержит тип AsyncStreamReader, который содержит правильную асинхронную реализацию потокового чтения.У него есть метод ReadLine, который (асинхронно) возвращает следующую строку и загружает только несколько фрагментов из исходного потока (используя асинхронный ReadAsync вместо запуска в фоновом потоке).

let processStreamAsync stream = async { 
  use asyncReader = new AsyncStreamReader(stream)
  let completed = ref false
  while not (!completed) do 
    // Asynchrnously get the next line
    let! nextLine = asyncReader.ReadLine()
    if nextLine = null then completed := true
    else
       (* process the next line *)  }

Если вы хотите загрузить весь контент в виде строки (вместо построчной обработки), вы можете использовать ReadToEnd метод AsyncStreamReader.Это правильная асинхронная реализация, которая начинает загрузку блока данных (асинхронно) и повторяет это без блокировки.

async { 
  use asyncReader = new AsyncStreamReader(stream)
  return! asyncReader.ReadToEnd() }

Кроме того, F # PowerPack является open-souorce и имеет разрешительную лицензию, поэтому лучший способ его использоватьчасто просто копировать несколько нужных вам файлов в ваш проект.

...