Понимание асинхронного программирования 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 при завершении операции. затем продолжайте выполнение остальной части тела вычисления

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

Но в этом примере я запутался в

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

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

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

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

Ответ 1

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

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

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

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

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

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

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

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

См. также

Рекомендации по параллелизации с использованием рабочего процесса async

Как выполнять цепные обратные вызовы в F #?

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

Ответ 2

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

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

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

// 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). Это отличная альтернатива потокам для многих проблем.

Ответ 3

Это не совсем о "накопленном времени". Асинхронное программирование не заставит данные поступать быстрее. Скорее, он упрощает ментальную модель для concurrency.

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

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

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

Ответ 4

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