В чем разница между асинхронным программированием и многопоточностью?

235

Я думал, что это в основном одно и то же - написание программ, которые разделяют задачи между процессорами (на машинах с 2+ процессорами). Тогда я читаю это , в котором говорится:

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

Ключевые слова async и await не приводят к созданию дополнительных потоков. Асинхронные методы не требуют многопоточности, потому что асинхронный метод не выполняется в своем собственном потоке. Метод выполняется в текущем контексте синхронизации и использует время в потоке, только когда метод активен. Вы можете использовать Task.Run для перемещения работы, связанной с ЦП, в фоновый поток, но фоновый поток не помогает с процессом, который просто ждет, когда результаты станут доступны.

и мне интересно, может ли кто-нибудь перевести это на английский для меня. Кажется, что проводится различие между асинхронностью (это слово?) И многопоточностью, и подразумевается, что у вас может быть программа, которая имеет асинхронные задачи, но не поддерживает многопоточность.

Теперь я понимаю идею асинхронных задач, таких как пример на pg. 467 из C # Джона Скита в глубине, третье издание

async void DisplayWebsiteLength ( object sender, EventArgs e )
{
    label.Text = "Fetching ...";
    using ( HttpClient client = new HttpClient() )
    {
        Task<string> task = client.GetStringAsync("http://csharpindepth.com");
        string text = await task;
        label.Text = text.Length.ToString();
    }
}

В asyncключевое слово означает « Эта функция, когда она называется, не будет вызываться в контексте , в котором его завершение требуется для всего после того, как его призыв называть.»

Другими словами, писать это в середине какой-то задачи

int x = 5; 
DisplayWebsiteLength();
double y = Math.Pow((double)x,2000.0);

, поскольку не DisplayWebsiteLength()имеет ничего общего с xили y, приведет DisplayWebsiteLength()к выполнению "в фоновом режиме", как

                processor 1                |      processor 2
-------------------------------------------------------------------
int x = 5;                                 |  DisplayWebsiteLength()
double y = Math.Pow((double)x,2000.0);     |

Очевидно, это глупый пример, но я прав или я совершенно сбит с толку или как?

(Кроме того, я не понимаю, почему senderи eникогда не используется в теле вышеуказанной функции.)

user5648283
источник
13
Это хорошее объяснение: blog.stephencleary.com/2013/11/there-is-no-thread.html
Якуб Лорц
senderи eпредполагают, что это на самом деле обработчик событий - практически единственное место, где async voidэто желательно. Скорее всего, это вызывается нажатием кнопки или чем-то в этом роде, в результате чего это действие происходит совершенно асинхронно по отношению к остальной части приложения. Но все это все в одном потоке - потоке пользовательского интерфейса (с небольшим промежутком времени в потоке IOCP, который отправляет обратный вызов в поток пользовательского интерфейса).
Луаан
3
Очень важное примечание к примеру DisplayWebsiteLengthкода: вы не должны использовать HttpClientв usingутверждении. При большой нагрузке код может исчерпать количество доступных сокетов, что приводит к ошибкам SocketException. Больше информации о неправильной реализации .
Ган
1
@JakubLortz Я не знаю, для кого эта статья на самом деле. Не для новичков, так как требует хороших знаний о потоках, прерываниях, процессорах, связанных с процессорами и т. Д. Не для опытных пользователей, поскольку для них это уже все понятно. Я уверен, что это никому не поможет понять, что это такое - слишком высокий уровень абстракции.
Лорено,

Ответы:

591

Ваше недоразумение очень распространено. Многих учат, что многопоточность и асинхронность - это одно и то же, но это не так.

Обычно помогает аналогия. Вы готовите в ресторане. Приходит заказ на яйца и тосты.

  • Синхронный: вы готовите яйца, затем вы готовите тост.
  • Асинхронный, однопоточный: вы начинаете готовить яйца и устанавливаете таймер. Вы начинаете готовить тосты и устанавливаете таймер. Пока они оба готовят, ты убираешь на кухне. Когда таймеры выключаются, вы снимаете яйца с огня и тосты с тостера и подаете их.
  • Асинхронный, многопоточный: вы нанимаете еще двух поваров, один для приготовления яиц и один для приготовления тостов. Теперь у вас есть проблема координации поваров, чтобы они не конфликтовали друг с другом на кухне при совместном использовании ресурсов. И вы должны заплатить им.

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

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

Итак, давайте посмотрим на пример Джона более подробно. Что случается?

  • Кто-то вызывает DisplayWebSiteLength. ВОЗ? Нам все равно.
  • Он устанавливает метку, создает клиента и просит клиента извлечь что-то. Клиент возвращает объект, представляющий задачу извлечения чего-либо. Эта задача выполняется.
  • Это выполняется в другом потоке? Возможно нет. Прочитайте статью Стивена о том, почему нет темы.
  • Теперь мы ждем задачи. Что случается? Мы проверяем, завершилось ли задание между тем временем, когда мы его создали, и мы его ожидали. Если да, то мы получаем результат и продолжаем работать. Давайте предположим, что он еще не завершен. Мы подписываем оставшуюся часть этого метода как продолжение этой задачи и возвращаемся .
  • Теперь управление вернулось к вызывающей стороне. Что оно делает? Все, что он хочет.
  • Теперь предположим, что задача завершена. Как это сделать? Возможно, он работал в другом потоке, или, возможно, вызывающий объект, к которому мы только что вернулись, позволил ему завершиться в текущем потоке. Несмотря на это, теперь у нас есть завершенное задание.
  • Завершенная задача просит правильный поток - опять же, вероятно, единственный поток - запустить продолжение задачи.
  • Управление сразу же возвращается обратно в метод, который мы только что оставили в точке ожидания. Теперь это результат доступен , чтобы мы могли назначить textи запустить остальную часть метода.

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

Эрик Липперт
источник
8
@ user5648283: Аппаратное обеспечение - это неправильный уровень, чтобы думать о задачах. Задача - это просто объект, который (1) представляет, что значение станет доступным в будущем, и (2) может запустить код (в правильном потоке), когда это значение станет доступным . Как любая отдельная задача получает результат в будущем, зависит от него. Некоторые будут использовать для этого специальное оборудование, такое как «диски» и «сетевые карты»; некоторые будут использовать аппаратные средства, такие как процессоры.
Эрик Липперт
13
@ user5648283: Опять подумайте о моей аналогии. Когда кто-то просит вас приготовить яйца и тосты, вы используете специальное оборудование - плиту и тостер - и вы можете чистить кухню, пока оборудование выполняет свою работу. Если кто-то попросит у вас яйца, тост и оригинальную критику последнего фильма о Хоббите, вы можете написать свой отзыв, пока яйца и тост готовятся, но для этого вам не нужно использовать оборудование.
Эрик Липперт
9
@ user5648283: Теперь рассмотрим вопрос о «перестановке кода». Предположим, у вас есть метод P, который имеет возвращаемую доходность, и метод Q, который делает foreach по результату P. Шаг через код. Вы увидите, что мы запускаем немного Q, затем немного P, затем немного Q ... Вы понимаете смысл этого? Ждем, по сути, доходности в костюмах . Теперь это более понятно?
Эрик Липперт
10
Тостер аппаратный. Аппаратному обеспечению не нужен поток для его обслуживания; диски и сетевые карты и все такое работает на уровне, намного ниже уровня потоков ОС.
Эрик Липперт
5
@ShivprasadKoirala: Это абсолютно не соответствует действительности . Если вы в это верите, то у вас есть очень ложные представления об асинхронности . Весь смысл асинхронности в C # состоит в том, что он не создает поток.
Эрик Липперт
27

Встроенный в браузер Javascript - отличный пример асинхронной программы без потоков.

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

Однако при выполнении чего-то вроде запроса AJAX код вообще не выполняется, поэтому другой javascript может реагировать на такие вещи, как события click, пока этот запрос не вернется и не вызовет обратный вызов, связанный с ним. Если один из этих других обработчиков событий все еще работает, когда возвращается запрос AJAX, его обработчик не будет вызываться, пока они не будут выполнены. Работает только одна «нить» JavaScript, хотя вы можете эффективно приостановить то, что вы делали, пока не получите необходимую информацию.

В приложениях C # то же самое происходит каждый раз, когда вы имеете дело с элементами пользовательского интерфейса - вам разрешено взаимодействовать с элементами пользовательского интерфейса только тогда, когда вы находитесь в потоке пользовательского интерфейса. Если пользователь нажал кнопку, и вы хотите ответить, прочитав большой файл с диска, неопытный программист может ошибиться, прочитав файл в самом обработчике события щелчка, что приведет к «зависанию» приложения до тех пор, пока файл завершил загрузку, потому что ему больше не разрешается отвечать на щелчки, зависания или любые другие события, связанные с пользовательским интерфейсом, пока этот поток не будет освобожден.

Один из вариантов, который могут использовать программисты, чтобы избежать этой проблемы, - это создать новый поток для загрузки файла, а затем сообщить коду этого потока, что при загрузке файла ему необходимо снова запустить оставшийся код в потоке пользовательского интерфейса, чтобы он мог обновлять элементы пользовательского интерфейса. на основании того, что он нашел в файле. До недавнего времени этот подход был очень популярен, потому что это было то, что облегчали библиотеки и язык C #, но он существенно сложнее, чем должен быть.

Если вы думаете о том, что делает процессор, когда он читает файл на уровне аппаратного обеспечения и операционной системы, он в основном дает инструкцию для чтения фрагментов данных с диска в память и для удара операционной системы «прерыванием». «когда чтение завершено. Другими словами, чтение с диска (или любого другого ввода-вывода) является асинхронной операцией. Концепция потока, ожидающего завершения ввода-вывода, - это абстракция, которую разработчики библиотеки создали для облегчения программирования. Это необязательно.

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

Пока async/awaitключевые слова не были добавлены, в C # было гораздо более очевидно, как вызывается код обратного вызова, потому что эти обратные вызовы были в форме делегатов, которые вы связали с задачей. Чтобы все еще дать вам преимущество использования ...Async()операции, избегая при этом сложности в коде, async/awaitабстрагируемся от создания этих делегатов. Но они все еще есть в скомпилированном коде.

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

StriplingWarrior
источник
Работает только одна «нить» JavaScript - больше не относится к Web Workers .
Алексей
6
@oleksii: Это технически верно, но я не собирался вдаваться в это, потому что сам API Web Workers является асинхронным, и Web Workers не могут напрямую влиять на значения javascript или DOM на вызываемой ими веб-странице от, что означает, что важный второй абзац этого ответа все еще остается в силе. С точки зрения программиста, есть небольшая разница между вызовом Web Worker и вызовом AJAX-запроса.
StriplingWarrior