Понимание асинхронного программирования F#

Я отчасти знаю синтаксис асинхронного программирования в F#. Например.

let downloadUrl(url:string) = async { 
  let req = HttpWebRequest.Create(url)
  // Run operation asynchronously
  let! resp = req.AsyncGetResponse()
  let stream = resp.GetResponseStream()
  // Dispose 'StreamReader' when completed
  use reader = new StreamReader(stream)
  // Run asynchronously and then return the result
  return! reader.AsyncReadToEnd() }

В опытной книге F# (и много других источников), они говорят как

позвольте! var = expr просто означает, "выполняют асинхронную операцию expr и связывают результат с var, когда операция завершается. Затем продолжите путем выполнения остальной части органа по вычислению"

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

Но в этом примере, я смущен в

  let! resp = req.AsyncGetResponse()
  let stream = resp.GetResponseStream()

Что происходит если resp еще не запустился, и поток в асинхронном теле хочет GetResponseStream? Действительно ли это - возможная ошибка?

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

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

22
задан Yin Zhu 15 March 2010 в 02:05
поделиться

4 ответа

В этом примере "async" не имеет отношения к параллелизму или экономии времени, скорее это обеспечение хорошей модели программирования без блокирования (читай: траты) потоков.

При использовании других языков программирования, как правило, у вас есть два варианта:

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

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

Таким образом, вы оказываетесь между молотом и наковальней: либо вы отказываетесь от простой модели программирования, либо тратите потоки.

Модель F# дает лучшее из двух миров; вы не блокируете потоки, но сохраняете простую модель программирования. Такие конструкции, как let! позволяют вам "переходить от потока к потоку" в середине асинхронного блока, поэтому в коде типа

Blah1()
let! x = AsyncOp()
Blah2()

Blah1 может выполняться, скажем, поток ThreadPool #13, но затем AsyncOp отпустит этот поток обратно в ThreadPool. Позже, когда AsyncOp завершится, остальной код снова запустится на доступном потоке (скажем, ThreadPool thread #20), который свяжет x с результатом и затем запустит Blah2. В тривиальных клиентских приложениях это редко имеет значение (за исключением случаев, когда вы не блокируете поток пользовательского интерфейса), но в серверных приложениях, выполняющих ввод-вывод (где потоки часто являются ценным ресурсом - потоки дороги, и вы не можете тратить их на блокировку) неблокирующий ввод-вывод часто является единственным способом обеспечить масштабирование приложения. F# позволяет вам писать неблокирующий ввод-вывод без того, чтобы программа превратилась в массу обратных вызовов спагетти-кода.

См. также

Лучшие практики распараллеливания с помощью async workflow

Как сделать цепочку обратных вызовов в F#?

http://cs.hubfs.net/forums/thread/8262.aspx

23
ответ дан 29 November 2019 в 04:57
поделиться

Дело не в «выигранном времени». Асинхронное программирование не ускорит доставку данных. Скорее, речь идет об упрощении ментальной модели параллелизма.

В C #, например, если вы хотите выполнить асинхронную операцию, вам нужно начать возиться с обратными вызовами, передавать локальное состояние этим обратным вызовам и так далее. Для простой операции, подобной той, что описана в Expert F # с двумя асинхронными операциями, вы рассматриваете три, казалось бы, отдельных метода (инициатор и два обратных вызова). Это маскирует последовательный, концептуально линейный характер рабочего процесса: выполнение запроса, чтение потока, печать результатов.

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

Тем не менее, в F # есть механизмы, которые могут помочь сэкономить время, если выполняется несколько независимых асинхронных операций. Например, вы можете запустить несколько асинхронных рабочих процессов одновременно, и они будут выполняться параллельно. Но в рамках одного экземпляра асинхронного рабочего процесса речь идет прежде всего о простоте, безопасности и понятности: о том, чтобы позволить вам рассуждать об асинхронных последовательностях операторов так же легко, как вы рассуждаете о синхронных последовательностях операторов в стиле C #.

3
ответ дан 29 November 2019 в 04:57
поделиться

Я думаю, что наиболее Важно понимать, что асинхронные рабочие процессы являются последовательными так же, как обычный код, написанный на F # (или C #, если на то пошло), является последовательным. У вас есть привязки let , которые вычисляются в обычном порядке, и некоторые выражения (которые могут иметь побочные эффекты). Фактически, асинхронные рабочие процессы часто больше похожи на императивный код.

Второй важный аспект асинхронных рабочих процессов заключается в том, что они неблокирующие . Это означает, что у вас могут быть операции, которые выполняются некоторым нестандартным способом и не блокируют поток во время выполнения. (В общем, let! в вычислительных выражениях F # всегда сигнализирует о некотором нестандартном поведении - это может быть возможность неудачи без получения результата в монаде Maybe , или это может быть неблокирующее выполнение для асинхронных рабочих процессов).

С технической точки зрения, неблокирующее выполнение реализуется путем регистрации некоторого обратного вызова, который будет запущен после завершения операции.Относительно простой пример - асинхронный рабочий процесс, который ждет определенное время - это можно реализовать с помощью Таймера без блокировки каких-либо потоков (Пример из главы 13 моей книги, исходный код доступен здесь ):

// Primitive that delays the workflow
let Sleep(time) = 
  // 'FromContinuations' is the basic primitive for creating workflows
  Async.FromContinuations(fun (cont, econt, ccont) ->
    // This code is called when workflow (this operation) is executed
    let tmr = new System.Timers.Timer(time, AutoReset=false)
    tmr.Elapsed.Add(fun _ -> 
      // Run the rest of the computation
      cont())
    tmr.Start() )

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

  • Вы можете использовать StartChild для запуска рабочего процесса в фоновом режиме - метод дает вам работающий рабочий процесс, который вы можете использовать (используя let! ) позже в рабочем процессе, чтобы дождаться завершения. , а вы можете продолжать заниматься другими делами. Это похоже на Задачи в .NET 4.0, но выполняется асинхронно, поэтому больше подходит для операций ввода-вывода.

  • Вы можете использовать Async.Parallel , чтобы создать несколько рабочих процессов и дождаться их завершения (что отлично подходит для операций с параллельными данными). Это похоже на PLINQ, но опять же, async лучше, если вы выполняете некоторые операции ввода-вывода.

  • Наконец, вы можете использовать MailboxProcessor , который позволяет вам писать параллельные приложения, используя стиль передачи сообщений (стиль Erlang). Это отличная альтернатива потокам для решения многих проблем.

9
ответ дан 29 November 2019 в 04:57
поделиться

Это хороший вопрос. Важно отметить, что несколько операторов в async блоках не выполняются параллельно. Блоки async по сути уступают процессорное время другим процессам, пока выполняются асинхронные запросы. Поэтому асинхронный блок в целом не будет выполняться быстрее, чем аналогичная последовательность синхронных операций, но в целом он позволит выполнить больше работы. Если вы хотите выполнять несколько операторов параллельно, вам лучше обратиться к библиотеке Task Parallel Library.

2
ответ дан 29 November 2019 в 04:57
поделиться
Другие вопросы по тегам:

Похожие вопросы: