Понимание F # Асинхронного программирования - PullRequest
22 голосов
/ 15 марта 2010

Я вроде знаю синтаксис асинхронного программирования на 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 ]

23 голосов
/ 15 марта 2010

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

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

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

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

9 голосов
/ 15 марта 2010

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

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

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

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

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

3 голосов
/ 15 марта 2010

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

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

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

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

2 голосов
/ 15 марта 2010

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

...