Как асинхронность F # действительно работает? - PullRequest
34 голосов
/ 22 августа 2010

Я пытаюсь узнать, как async и let! работают в F #.Все документы, которые я прочитал, кажутся запутанными.Какой смысл запускать асинхронный блок с Async.RunSynchronously?Это асинхронно или синхронно?Выглядит как противоречие.

В документации сказано, что Async.StartImmediate работает в текущем потоке.Если он работает в одном и том же потоке, он не выглядит для меня слишком асинхронным ... А может быть, асинхронные больше похожи на сопрограммы, а не на потоки.Если да, то когда они возвращают четвертое число?

Цитирование MS docs:

Строка кода, которая использует let!начинает вычисление, а затем поток приостанавливается до тех пор, пока результат не станет доступен, и в этот момент выполнение продолжается.

Если поток ожидает результата, зачем мне его использовать?Выглядит как вызов простой старой функции.

А что делает Async.Parallel?Получает последовательность Async <'T>.Почему бы не последовательность простых функций, выполняемых параллельно?

Я думаю, что мне здесь не хватает чего-то очень простого.Думаю, после того, как я это пойму, вся документация и примеры станут понятны.

Ответы [ 7 ]

32 голосов
/ 22 августа 2010

Несколько вещей.

Во-первых, разница между

let resp = req.GetResponse()

и

let! resp = req.AsyncGetReponse()

заключается в том, что для, вероятно, сотен миллисекунд (вечность дляCPU), когда веб-запрос находится «в море», первый использует один поток (заблокированный при вводе-выводе), тогда как второй использует ноль потоков.Это наиболее распространенная «победа» для асинхронных операций: вы можете написать неблокирующий ввод-вывод, который не будет тратить ни одного потока, ожидающего вращения жестких дисков или сетевых запросов для возврата.(В отличие от большинства других языков, вы не обязаны выполнять инвертирование элементов управления и разложения в обратные вызовы.)

Во-вторых, Async.StartImmediate будет запускать асинхронно в текущем потоке.Типичное использование с графическим интерфейсом, у вас есть какое-то приложение с графическим интерфейсом, которое хочет, например, обновить пользовательский интерфейс (например, где-то сказать «загрузка ...»), а затем выполнить некоторую фоновую работу (загрузить что-то с диска или что-то еще), а затемвернуться к потоку пользовательского интерфейса переднего плана, чтобы обновить пользовательский интерфейс после завершения («готово!»).StartImmediate позволяет асинхронно обновлять пользовательский интерфейс в начале операции и захватывать SynchronizationContext, чтобы в конце операции можно было вернуться в графический интерфейс для окончательного обновления пользовательского интерфейса.

Далее Async.RunSynchronously используется редко (один тезис состоит в том, что вы вызываете его не более одного раза в любом приложении).В пределе, если вы написали всю программу асинхронно, тогда в «основном» методе вы вызываете RunSynchronously, чтобы запустить программу и дождаться результата (например, распечатать результат в консольном приложении).Это блокирует поток, так что обычно это полезно только в самом верху асинхронной части вашей программы, на границе с синхронизацией.(Более продвинутый пользователь может предпочесть StartWithContinuations - RunSynchronously - это своего рода "легкий взлом", который можно получить от асинхронного возврата к синхронизации.)

Наконец, Async.Parallel выполняет параллелизм форк-соединения.Вы могли бы написать аналогичную функцию, которая просто принимает функции, а не async s (например, материал в TPL), но типичной «сладкой точкой» в F # являются параллельные вычисления ввода-вывода, которые уже являются асинхронными объектами, так что эточаще всего полезная подпись.(Для параллелизма, связанного с процессором, вы можете использовать асинхронные вычисления, но вы также можете использовать и TPL.)

12 голосов
/ 22 августа 2010

Асинхронное использование используется для сохранения количества используемых потоков.

См. Следующий пример:

let fetchUrlSync url = 
    let req = WebRequest.Create(Uri url)
    use resp = req.GetResponse()
    use stream = resp.GetResponseStream()
    use reader = new StreamReader(stream)
    let contents = reader.ReadToEnd()
    contents 

let sites = ["http://www.bing.com";
             "http://www.google.com";
             "http://www.yahoo.com";
             "http://www.search.com"]

// execute the fetchUrlSync function in parallel 
let pagesSync = sites |> PSeq.map fetchUrlSync  |> PSeq.toList

Код выше - то, что вы хотите сделать: определить функциюи выполнить параллельно.Так зачем нам здесь асинхронность?

Давайте рассмотрим что-то большое.Например, если количество сайтов не 4, а, скажем, 10000!Затем требуется 10000 потоков для их параллельного запуска, что является огромной стоимостью ресурсов.

Находясь в асинхронном режиме:

let fetchUrlAsync url =
    async { let req =  WebRequest.Create(Uri url)
            use! resp = req.AsyncGetResponse()
            use stream = resp.GetResponseStream()
            use reader = new StreamReader(stream)
            let contents = reader.ReadToEnd()
            return contents }
let pagesAsync = sites |> Seq.map fetchUrlAsync |> Async.Parallel |> Async.RunSynchronously

Когда код находится в use! resp = req.AsyncGetResponse(), текущий поток отказывается и его ресурс может использоваться для других целей.Если ответ возвращается через 1 секунду, то ваш поток может использовать эту 1 секунду для обработки других вещей.В противном случае поток блокируется, тратя ресурс потока на 1 секунду.

Таким образом, даже если вы загружаете 10000 веб-страниц параллельно асинхронным способом, количество потоков ограничено небольшим количеством.

Я думаю, что вы не программист .Net / C #.Учебник по асинхронности обычно предполагает, что кто-то знает .Net и как программировать асинхронный ввод-вывод в C # (много кода).Магия асинхронной конструкции в F # не параллельна.Потому что простая параллель может быть реализована с помощью других конструкций, например, ParallelFor в расширении параллели .Net.Однако асинхронный ввод-вывод является более сложным, так как вы видите, что поток прекращает свое выполнение. Когда ввод-вывод завершается, ввод-вывод должен активизировать свой родительский поток.Вот где используется асинхронная магия: в нескольких строках лаконичного кода вы можете выполнять очень сложное управление.

9 голосов
/ 28 марта 2015

Здесь много хороших ответов, но я подумал, что под другим углом зрения задаю вопрос: как на самом деле работает асинхронность F #?

В отличие от async/await в C # F # разработчики могут реализовать собственную версию Async. Это может быть отличным способом узнать, как работает Async.

(Для интересующихся исходный код Async можно найти здесь: https://github.com/Microsoft/visualfsharp/blob/fsharp4/src/fsharp/FSharp.Core/control.fs)

В качестве основного строительного блока для наших рабочих процессов DIY мы определяем:

type DIY<'T> = ('T->unit)->unit

Это функция, которая принимает другую функцию (называемую продолжением), которая вызывается, когда готов результат типа 'T. Это позволяет DIY<'T> запускать фоновую задачу, не блокируя вызывающий поток. Когда результат готов, вызывается продолжение, позволяющее продолжить вычисление.

Строительный блок F # Async немного сложнее, поскольку он также включает в себя отмены и продолжения исключений, но по сути это так.

Для поддержки синтаксиса рабочего процесса F # нам нужно определить выражение для вычисления (https://msdn.microsoft.com/en-us/library/dd233182.aspx).. Хотя это довольно продвинутая функция F #, она также является одной из самых удивительных функций F #. Две самые важные операции для определить return & bind, которые используются F # для объединения наших DIY<_> строительных блоков в агрегированные DIY<_> строительные блоки.

adaptTask используется для адаптации Task<'T> в DIY<'T>. startChild позволяет запускать несколько одновременных DIY<'T>, обратите внимание, что он не запускает новые потоки для этого, но повторно использует вызывающий поток.

Без лишних слов, вот пример программы:

open System
open System.Diagnostics
open System.Threading
open System.Threading.Tasks

// Our Do It Yourself Async workflow is a function accepting a continuation ('T->unit).
// The continuation is called when the result of the workflow is ready. 
// This may happen immediately or after awhile, the important thing is that 
//  we don't block the calling thread which may then continue executing useful code.
type DIY<'T> = ('T->unit)->unit

// In order to support let!, do! and so on we implement a computation expression.
// The two most important operations are returnValue/bind but delay is also generally 
//  good to implement.
module DIY =

    // returnValue is called when devs uses return x in a workflow.
    // returnValue passed v immediately to the continuation.
    let returnValue (v : 'T) : DIY<'T> =
        fun a ->
            a v

    // bind is called when devs uses let!/do! x in a workflow
    // bind binds two DIY workflows together
    let bind (t : DIY<'T>) (fu : 'T->DIY<'U>) : DIY<'U> =
        fun a ->
            let aa tv =
                let u = fu tv
                u a
            t aa

    let delay (ft : unit->DIY<'T>) : DIY<'T> =
        fun a ->
            let t = ft ()
            t a

    // starts a DIY workflow as a subflow
    // The way it works is that the workflow is executed 
    //  which may be a delayed operation. But startChild
    //  should always complete immediately so in order to
    //  have something to return it returns a DIY workflow
    // postProcess checks if the child has computed a value 
    //  ie rv has some value and if we have computation ready
    //  to receive the value (rca has some value).
    //  If this is true invoke ca with v
    let startChild (t : DIY<'T>) : DIY<DIY<'T>> =
        fun a ->
            let l   = obj()
            let rv  = ref None
            let rca = ref None

            let postProcess () =
                match !rv, !rca with
                | Some v, Some ca ->
                    ca v
                    rv  := None
                    rca := None
                | _ , _ -> ()

            let receiver v =
                lock l <| fun () ->
                    rv := Some v
                    postProcess ()

            t receiver

            let child : DIY<'T> =
                fun ca ->
                    lock l <| fun () ->
                        rca := Some ca
                        postProcess ()

            a child

    let runWithContinuation (t : DIY<'T>) (f : 'T -> unit) : unit =
        t f

    // Adapts a task as a DIY workflow
    let adaptTask (t : Task<'T>) : DIY<'T> =
        fun a ->
            let action = Action<Task<'T>> (fun t -> a t.Result)
            ignore <| t.ContinueWith action

    // Because C# generics doesn't allow Task<void> we need to have
    //  a special overload of for the unit Task.
    let adaptUnitTask (t : Task) : DIY<unit> =
        fun a ->
            let action = Action<Task> (fun t -> a ())
            ignore <| t.ContinueWith action

    type DIYBuilder() =
        member x.Return(v)  = returnValue v
        member x.Bind(t,fu) = bind t fu
        member x.Delay(ft)  = delay ft

let diy = DIY.DIYBuilder()

open DIY

[<EntryPoint>]
let main argv = 

    let delay (ms : int) = adaptUnitTask <| Task.Delay ms

    let delayedValue ms v =
        diy {
            do! delay ms
            return v
        }

    let complete = 
        diy {
            let sw = Stopwatch ()
            sw.Start ()

            // Since we are executing these tasks concurrently 
            //  the time this takes should be roughly 700ms
            let! cd1 = startChild <| delayedValue 100 1
            let! cd2 = startChild <| delayedValue 300 2
            let! cd3 = startChild <| delayedValue 700 3

            let! d1 = cd1
            let! d2 = cd2
            let! d3 = cd3

            sw.Stop ()

            return sw.ElapsedMilliseconds,d1,d2,d3
        }

    printfn "Starting workflow"

    runWithContinuation complete (printfn "Result is: %A")

    printfn "Waiting for key"

    ignore <| Console.ReadKey ()

    0

Вывод программы должен выглядеть примерно так:

Starting workflow
Waiting for key
Result is: (706L, 1, 2, 3)

При запуске программы обратите внимание, что Waiting for key печатается сразу, поскольку поток консоли не блокируется от запуска рабочего процесса. Примерно через 700 мс результат печатается.

Надеюсь, это было интересно некоторым разработчикам F #

6 голосов
/ 22 августа 2010

Недавно я сделал краткий обзор функций в модуле Async: здесь .Может быть, это поможет.

2 голосов
/ 04 октября 2016

Очень много деталей в других ответах, но, будучи новичком, меня смутили различия между C # и F #.

F # асинхронные блоки - это рецепт для того, как должен выполняться код, а не инструкция для его запуска.

Вы создаете свой рецепт, возможно, комбинируете его с другими рецептами (например, Async.Parallel). Только после этого вы просите систему запустить ее, и вы можете сделать это в текущем потоке (например, Async.StartImmediate) или в новой задаче, или другими способами.

Таким образом, это разделение того, что вы хотите сделать, от того, кто должен это делать.

Модель C # часто называют «горячими задачами», потому что задачи запускаются для вас как часть их определения, в отличие от моделей F # «холодных задач».

1 голос
/ 22 августа 2010

Идея, стоящая за let! и Async.RunSynchronously, заключается в том, что иногда вы выполняете асинхронное действие, результаты которого вам нужны, прежде чем вы сможете продолжить.Например, функция «загрузить веб-страницу» может не иметь синхронного эквивалента, поэтому вам нужно каким-то образом запустить ее синхронно.Или, если у вас Async.Parallel, у вас могут быть сотни задач, которые все выполняются одновременно, но вы хотите, чтобы все они выполнялись до продолжения.

Насколько я могу судить, причина, по которой вы будете использовать Async.StartImmediateв том, что у вас есть вычисления, которые вам нужно запустить в текущем потоке (возможно, в потоке пользовательского интерфейса), не блокируя его.Использует ли он сопрограммы?Я думаю, вы могли бы назвать это так, хотя в .Net нет общего механизма сопрограмм.

Так почему для Async.Parallel требуется последовательность Async<'T>?Возможно, потому что это способ составления Async<'T> объектов.Вы можете легко создать свою собственную абстракцию, которая работает только с простыми функциями (или комбинацией простых функций и Async s, но это будет просто вспомогательная функция.

0 голосов
/ 22 августа 2010

В асинхронном блоке у вас могут быть некоторые синхронные и некоторые асинхронные операции, поэтому, например, у вас может быть веб-сайт, который будет показывать статус пользователя несколькими способами, поэтому вы можете показать, есть ли у них счета, которые являются скоро, дни рождения и домашнее задание. Ни один из них не находится в одной базе данных, поэтому ваше приложение выполнит три отдельных вызова. Возможно, вы захотите выполнять параллельные вызовы, чтобы, когда выполняется самый медленный вызов, вы могли объединить результаты и отобразить их, поэтому в результате будет отображаться самый медленный дисплей. Вам не важен порядок их возвращения, вы просто хотите знать, когда все три будут получены.

Чтобы завершить мой пример, вы можете синхронно выполнить работу по созданию пользовательского интерфейса для отображения этой информации. Итак, в конце вы хотели, чтобы эти данные были извлечены и отображен пользовательский интерфейс, части, где порядок не имеет значения, выполняются параллельно, и где порядок может быть выполнен синхронно.

Вы можете сделать это как три потока, но затем вы должны отслеживать и отключать исходный поток, когда третий закончен, но это больше работы, легче сделать так, чтобы .NET Framework позаботился об этом.

...