Подтвердить что ты не робот

Как Fyn Async действительно работает?

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

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

Цитата MS docs:

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

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

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

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

4b9b3361

Ответ 1

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

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

let resp = req.GetResponse()

и

let! resp = req.AsyncGetReponse()

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

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

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

Наконец, Async.Parallel выполняет fork-join parallelism. Вы можете написать аналогичную функцию, которая просто выполняет функции, а не async (например, материал в TPL), но типичное сладкое пятно в F # - это параллельные вычисления с использованием ввода-вывода, которые уже являются асинхронными объектами, поэтому это наиболее обычно полезная подпись. (Для CPU-bound parallelism вы можете использовать asyncs, но вы также можете использовать TPL.)

Ответ 2

Использование async - это сохранение количества используемых потоков.

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

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, но, скажем, 10 000! Затем для запуска их параллельно требуется 10000 потоков, что представляет собой огромную стоимость ресурсов.

В режиме async:

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/С#. В асинхронном учебнике обычно предполагается, что он знает .Net и как программировать асинхронный ввод-вывод в С# (много кода). Магия конструкции Async в F # не является параллельной. Поскольку простая параллель может быть реализована другими конструкциями, например. Параллельно в параллельном расширении .Net. Однако асинхронный IO является более сложным, так как вы видите, что поток отказывается от выполнения, когда IO заканчивается, IO должен разбудить свой родительский поток. Здесь используется асинхронная магия: в нескольких строках сжатого кода вы можете выполнять очень сложное управление.

Ответ 3

Много хороших ответов здесь, но я подумал, что я подхожу под другим углом к ​​вопросу: как действительно работает Fyn Async?

В отличие от async/await в С# 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 используется для адаптации a Task<'T> в DIY<'T>. startChild позволяет запустить несколько simulatenous 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 # devs

Ответ 4

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

Ответ 5

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

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

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

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

Модель С# часто называется "Горячие задачи", потому что задачи запускаются для вас как часть их определения, а также модели F # "Холодная задача".

Ответ 6

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

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

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

Ответ 7

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

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

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