Асинхронний підхід Майкрософт є хорошою заміною найпоширеніших цілей багатопотокового програмування: поліпшення чуйності щодо завдань вводу-виводу.
Однак важливо усвідомити, що асинхронний підхід взагалі не здатний покращити продуктивність або покращити чуйність щодо інтенсивних завдань процесора.
Багатопоточність для чуйності
Багатопотокове читання для чуйності - це традиційний спосіб підтримувати програму під час виконання важких завдань вводу-виводу або важких обчислювальних завдань. Ви зберігаєте файли на фоновому потоці, щоб користувач міг продовжувати свою роботу, не потребуючи чекати, коли жорсткий диск закінчить своє завдання. Потік вводу-виводу часто блокує очікування закінчення деякої частини запису, тому контекстні комутації є частими.
Аналогічно, виконуючи складний обчислення, ви хочете дозволити регулярну комутацію контексту, щоб користувальницький інтерфейс міг залишатися чуйним, а користувач не думає, що програма вийшла з ладу.
Ціль тут полягає не в загальному, щоб змусити кілька потоків працювати на різних процесорах. Натомість нас просто цікавить, щоб контекстні перемикання відбувалися між тривалою фоновою задачею та інтерфейсом користувача, щоб інтерфейс користувача міг оновлюватись та відповідати користувачеві під час виконання фонового завдання. Взагалі, інтерфейс не буде займати багато процесорних потужностей, і потокова рамка або ОС зазвичай вирішують запустити їх на одному процесорі.
Ми фактично втрачаємо загальну продуктивність через додаткову вартість перемикання контексту, але нас це не хвилює, оскільки продуктивність процесора не була нашою метою. Ми знаємо, що зазвичай у нас є більше потужності процесора, ніж нам потрібно, і тому наша мета щодо багатопотокової роботи - це зробити завдання, виконане для користувача, не витрачаючи часу на користувача.
"Асинхронна" альтернатива
"Асинхронний підхід" змінює цю картину, включаючи контекстні комутатори в одному потоці. Це гарантує, що всі наші завдання працюватимуть на одному процесорі та можуть забезпечити невеликі покращення продуктивності в плані меншої кількості потоків / очищення потоків та меншої кількості реальних контекстних комутацій між потоками.
Замість створення нового потоку для очікування отримання мережевого ресурсу (наприклад, завантаження зображення) використовується async
метод, який await
стає доступним для зображення, а тим часом поступається методу виклику.
Основна перевага тут полягає в тому, що вам не доведеться турбуватися про проблеми з нитками, як-от уникнення тупикової ситуації, оскільки ви взагалі не використовуєте блокування та синхронізацію, а програміст налаштовує фонову нитку та повертається трохи менше. в потоці інтерфейсу користувача, коли результат повертається, щоб безпечно оновити інтерфейс користувача.
Я не надто глибоко заглядав у технічні деталі, але моє враження полягає в тому, що управління завантаженням з періодичною легкою процесорною діяльністю стає завданням не для окремої нитки, а для чогось більш схожого на завдання в черзі подій інтерфейсу, і коли завантаження завершується, асинхронний метод поновлюється з цієї черги подій. Іншими словами, await
означає щось схоже на те, щоб "перевірити, чи потрібний мені результат, якщо ні, поверніть мене до черги завдань цього потоку".
Зауважте, що такий підхід не вирішить проблему, що займає процесорне завдання: немає жодних даних на очікування, тому ми не можемо отримати контекстні комутатори, які нам потрібні, без створення фактичної потокової робочої нитки. Звичайно, все ж може бути зручно використовувати асинхронний метод для запуску фонової нитки та повернення результату в програмі, яка всебічно використовує асинхронний підхід.
Багатопоточність для продуктивності
Оскільки ви говорите про "продуктивність", я також хотів би поговорити про те, як багатопотоковість може бути використана для підвищення продуктивності, що абсолютно неможливо при однопотоковому асинхронному підході.
Коли ви насправді знаходяться в ситуації, коли у вас недостатньо потужності процесора на одному процесорі, і ви хочете використовувати багатопотоковість для продуктивності, це насправді часто важко зробити. З іншого боку, якщо одному процесору не вистачає процесорної потужності, це також часто є єдиним рішенням, яке може дозволити вашій програмі робити те, що ви хочете досягти, у розумні часові рамки, а це робить роботу вартим.
Тривіальний паралелізм
Звичайно, іноді може бути легко отримати реальну швидкість від багатопотокової.
Якщо у вас є велика кількість незалежних обчислювальних завдань (тобто завдань, вхідних і вихідних даних яких дуже мало стосовно розрахунків, які необхідно виконати для визначення результату), то ви часто можете отримати значне прискорення шляхом створення пулу потоків (розмір належним чином залежно від кількості доступних процесорів), а також головний потік розподіляє роботу та збирає результати.
Практична багатопоточність для продуктивності
Я не хочу висувати себе як занадто великого експерта, але моє враження, що, як правило, найпрактичніша багатопотокова робота для продуктивності, що трапляється в ці дні, шукає місця в додатку, який має тривіальний паралелізм, і використовує декілька потоків щоб отримати користь.
Як і з будь-якою оптимізацією, зазвичай краще оптимізувати її після того, як ви профілювали продуктивність програми та визначили «гарячі точки»: легко уповільнити програму, вирішивши довільно, що ця частина повинна працювати в одній нитці, а ця частина в іншій, без спочатку визначте, чи займають обидві частини значну частину процесорного часу.
Додатковий потік означає більше витрат на налаштування / відмову, а також більше контекстних комутаторів або більше комунікаційних витрат між CPU. Якщо вона не проводить достатньо роботи, щоб компенсувати ці витрати, якщо вони знаходяться в окремому процесорі, і не потрібно бути окремим потоком з міркувань чутливості, це сповільнить справи без користі.
Шукайте завдання, які мають невелику взаємозалежність і які займають значну частину часу виконання вашої програми.
Якщо вони не мають взаємозалежностей, то це справа тривіального паралелізму, ви можете легко налаштувати кожного з ниток і насолоджуватися перевагами.
Якщо ви можете знайти завдання з обмеженою взаємозалежністю, щоб блокування та синхронізація для обміну інформацією не сповільнили їх істотно, то багатопотокове читання може дати деяке прискорення, за умови, що ви обережні, щоб уникнути небезпеки тупикового зв'язку через несправну логіку при синхронізації або неправильні результати через не синхронізацію, коли це необхідно.
Крім того, деякі більш поширені програми для багатопотокового читання не (в певному сенсі) не шукають прискорення заздалегідь визначеного алгоритму, а замість того, щоб збільшити бюджет для алгоритму, який вони планують писати: якщо ви пишете ігровий движок , і ваш AI повинен приймати рішення в межах частоти кадрів, ви часто можете давати AI більший бюджет циклу процесора, якщо ви можете надати йому свій власний процесор.
Однак не забудьте профілювати нитки та переконайтесь, що вони виконують достатню роботу, щоб компенсувати витрати в якийсь момент.
Паралельні алгоритми
Існує також багато проблем, які можна вирішити за допомогою декількох процесорів, але вони занадто монолітні, щоб просто розділити між процесорами.
Паралельні алгоритми повинні бути ретельно проаналізовані для їх великого періоду виконання щодо найкращого доступного непаралельного алгоритму, оскільки між комунікаційними витратами між CPU дуже легко усунути будь-які переваги від використання декількох процесорів. Загалом, вони повинні використовувати менше взаємозв'язку між процесорами (у великих значеннях), ніж вони використовують обчислення для кожного процесора.
Наразі це все ще багато простору для академічних досліджень, частково через необхідний складний аналіз, частково тому, що тривіальний паралелізм є досить поширеним явищем, частково тому, що у нас ще не так багато ядер процесора на наших комп’ютерах, що проблем, які неможливо вирішити в розумні часові рамки на одному процесорі, можна вирішити за розумні часові рамки, використовуючи всі наші процесори.