Понимание 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# фактически означают, что "создавая новый поток, повесьте текущий поток, когда новый поток завершится, разбудите основной поток и продолжите", но в этом случае я не вижу, что мы могли бы сохранить в любой момент.

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

4 ответа

Решение

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

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

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

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

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

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

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

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

Смотрите также

Лучшие практики для распараллеливания с использованием асинхронного рабочего процесса

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

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

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

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

Технически говоря, неблокирующее выполнение реализуется путем регистрации некоторого обратного вызова, который будет запущен после завершения операции. Относительно простым примером является асинхронный рабочий процесс, который ожидает определенное время - это можно реализовать с помощью Timer без блокировки потоков (пример из главы 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). Это отличная альтернатива темам для многих задач.

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

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

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

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

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

Другие вопросы по тегам