Асинхронный подход Microsoft является хорошей заменой для наиболее распространенных целей многопоточного программирования: повышение скорости реагирования на задачи ввода-вывода.
Тем не менее, важно понимать, что асинхронный подход вообще не способен ни повысить производительность, ни улучшить скорость реагирования на задачи с интенсивным использованием ЦП.
Многопоточность для отзывчивости
Многопоточность для отзывчивости - это традиционный способ обеспечить отзывчивость программы во время тяжелых задач ввода-вывода или сложных вычислительных задач. Вы сохраняете файлы в фоновом потоке, чтобы пользователь мог продолжить свою работу, не дожидаясь завершения работы жесткого диска. Поток ввода-вывода часто блокирует ожидание завершения части записи, поэтому переключение контекста происходит часто.
Аналогично, при выполнении сложных вычислений вы хотите разрешить регулярное переключение контекста, чтобы пользовательский интерфейс оставался отзывчивым, и пользователь не думал, что программа потерпела крах.
В целом цель здесь не в том, чтобы несколько потоков работали на разных процессорах. Вместо этого мы просто заинтересованы в том, чтобы переключение контекста происходило между длительной фоновой задачей и пользовательским интерфейсом, чтобы пользовательский интерфейс мог обновлять и отвечать пользователю во время выполнения фоновой задачи. В общем случае пользовательский интерфейс не потребляет много ресурсов процессора, а многопоточная среда или ОС обычно решают запустить их на одном и том же процессоре.
Мы фактически теряем общую производительность из-за дополнительных затрат на переключение контекста, но нам все равно, потому что производительность процессора не была нашей целью. Мы знаем, что обычно у нас больше ресурсов процессора, чем нам нужно, и поэтому наша цель в отношении многопоточности состоит в том, чтобы выполнить задачу для пользователя, не тратя время пользователя.
«Асинхронная» альтернатива
«Асинхронный подход» меняет эту картину, позволяя переключать контексты в одном потоке. Это гарантирует, что все наши задачи будут выполняться на одном процессоре, и может обеспечить некоторые незначительные улучшения производительности с точки зрения меньшего количества создания / очистки потоков и меньшего количества реальных переключений контекста между потоками.
Вместо создания нового потока, ожидающего получения сетевого ресурса (например, загрузки изображения), используется async
метод, при котором await
изображение становится доступным и, тем временем, уступает вызывающему методу.
Основным преимуществом здесь является то, что вам не нужно беспокоиться о проблемах с многопоточностью, таких как избежание взаимоблокировки, так как вы вообще не используете блокировки и синхронизацию, и программисту нужно немного настроить фоновый поток и вернуться назад. в потоке пользовательского интерфейса, когда возвращается результат для безопасного обновления пользовательского интерфейса.
Я не слишком углублялся в технические детали, но у меня сложилось впечатление, что управление загрузкой с периодической легкой загрузкой ЦП становится задачей не для отдельного потока, а скорее чем-то более похожим на задачу в очереди событий пользовательского интерфейса, и когда загрузка завершена, асинхронный метод возобновляется из этой очереди событий. Другими словами, await
означает что-то вроде «проверить, доступен ли мне нужный результат, если нет, вернуть меня в очередь задач этого потока».
Обратите внимание, что этот подход не решит проблему задачи, интенсивно использующей процессор: данных ждать не приходится, поэтому мы не можем получить переключения контекста, которые нам нужны, без создания фактического фонового рабочего потока. Конечно, все еще может быть удобно использовать асинхронный метод для запуска фонового потока и возврата результата в программе, которая широко использует асинхронный подход.
Многопоточность для производительности
Поскольку вы говорите о «производительности», я также хотел бы обсудить, как многопоточность может использоваться для повышения производительности, что совершенно невозможно при однопоточном асинхронном подходе.
Когда вы на самом деле находитесь в ситуации, когда вам не хватает мощности ЦП на одном ЦП, и вы хотите использовать многопоточность для повышения производительности, на самом деле это часто бывает трудно сделать. С другой стороны, если одному процессору не хватает вычислительной мощности, это также часто единственное решение, которое может позволить вашей программе делать то, что вы хотели бы выполнить в разумные сроки, что и делает работу стоящей.
Тривиальный параллелизм
Конечно, иногда может быть легко получить реальное ускорение от многопоточности.
Если у вас есть большое количество независимых задач, требующих большого объема вычислений (то есть задач, чьи входные и выходные данные очень малы по сравнению с вычислениями, которые необходимо выполнить для определения результата), то вы часто можете получить значительное ускорение за счет создание пула потоков (с соответствующим размером в зависимости от количества доступных процессоров) и наличие главного потока для распределения работы и сбора результатов.
Практическая многопоточность для производительности
Я не хочу выдвигать себя в качестве эксперта, но у меня сложилось впечатление, что, как правило, наиболее практичная многопоточность для производительности, которая происходит в наши дни, - это поиск мест в приложении с тривиальным параллелизмом и использование нескольких потоков. пожинать плоды.
Как и в случае любой оптимизации, обычно лучше оптимизировать после того, как вы профилировали производительность вашей программы и определили «горячие точки»: программу легко замедлить, произвольно решив, что эта часть должна выполняться в одном потоке, а другая - в другом, без сначала определить, занимают ли обе части значительную часть процессорного времени.
Дополнительный поток означает больше затрат на установку / разборку и либо больше переключений контекста, либо больше затрат на связь между процессорами. Если он не выполняет достаточно работы, чтобы компенсировать эти затраты, если он находится на отдельном процессоре, и не нуждается в отдельном потоке по соображениям отзывчивости, он замедлит работу без какой-либо выгоды.
Ищите задачи, которые имеют мало взаимозависимостей и занимают значительную часть времени выполнения вашей программы.
Если у них нет взаимозависимостей, то это случай тривиального параллелизма, вы можете легко настроить каждый из них с потоком и наслаждаться преимуществами.
Если вы можете найти задачи с ограниченной взаимозависимостью, так что блокировка и синхронизация для обмена информацией не сильно их замедляют, тогда многопоточность может дать некоторое ускорение, при условии, что вы будете осторожны, чтобы избежать опасностей тупика из-за неисправной логики при синхронизации или неверные результаты из-за отсутствия синхронизации, когда это необходимо.
В качестве альтернативы, некоторые из наиболее распространенных приложений для многопоточности не (в некотором смысле) не ищут ускорения заранее определенного алгоритма, но вместо этого для большего бюджета для алгоритма, который они планируют написать: если вы пишете игровой движок и ваш ИИ должен принимать решение в пределах вашей частоты кадров, вы часто можете дать своему ИИ больший бюджет цикла ЦП, если вы можете назначить ему свой собственный ЦП.
Однако обязательно профилируйте потоки и убедитесь, что они выполняют достаточно работы, чтобы в какой-то момент компенсировать затраты.
Параллельные алгоритмы
Есть также много проблем, которые могут быть ускорены при использовании нескольких процессоров, но они слишком монолитны, чтобы просто делиться между процессорами.
Параллельные алгоритмы должны быть тщательно проанализированы на предмет их времени выполнения big-O с точки зрения лучшего из доступных непараллельных алгоритмов, поскольку затраты на межпроцессорное взаимодействие очень легко исключают любые выгоды от использования нескольких процессоров. В общем, они должны использовать меньше межпроцессорного взаимодействия (в терминах big-O), чем они используют вычисления для каждого CPU.
На данный момент это все еще в значительной степени пространство для академических исследований, отчасти из-за необходимого сложного анализа, отчасти потому, что тривиальный параллелизм довольно распространен, отчасти потому, что на наших компьютерах еще не так много процессорных ядер, что вызывает проблемы, которые не может быть решена в разумные сроки на одном процессоре может быть решена в разумные сроки с использованием всех наших процессоров.