Те, що говорить Джуліо Франко , стосується багатопотокової та багатопроцесорної обробки в цілому .
Однак у Python * є додаткова проблема: Існує глобальний замок інтерпретатора, який не дозволяє двом потокам в одному і тому ж процесі запускати код Python одночасно. Це означає, що якщо у вас є 8 ядер, і ви зміните свій код на 8 потоків, він не зможе використовувати 800% CPU і запустити 8x швидше; він буде використовувати той самий 100% процесор і працювати з однаковою швидкістю. (Насправді вона працюватиме трохи повільніше, оскільки є додаткові накладні витрати від нанизування, навіть якщо у вас немає спільних даних, але поки що це ігноруйте.)
З цього є винятки. Якщо важкі обчислення вашого коду насправді не відбуваються в Python, але в деякій бібліотеці зі спеціальним кодом C, який робить належну обробку GIL, як нумерований додаток, ви отримаєте очікувану вигоду від продуктивності від нарізки. Те ж саме, якщо важкі обчислення виконуються деяким підпроцесом, який ви запускаєте і чекаєте.
Що ще важливіше, є випадки, коли це не має значення. Наприклад, мережевий сервер витрачає більшу частину свого часу на читання пакетів з мережі, а додаток GUI витрачає більшу частину свого часу на очікування подій користувача. Однією з причин використання потоків у мережевому сервері чи додатку GUI є те, що ви можете виконувати тривалі «фонові завдання», не зупиняючи головний потік продовжувати обслуговувати мережеві пакети чи події GUI. І це добре працює з потоками Python. (У технічному плані це означає, що потоки Python надають вам паралельність, хоча вони не дають вам паралелізму ядра.)
Але якщо ви пишете програму, пов'язану з процесором, в чистому Python, використання більшої кількості потоків, як правило, не корисно.
Використання окремих процесів не має таких проблем з GIL, оскільки кожен процес має свій окремий GIL. Звичайно, ви все ще маєте ті самі компроміси між потоками та процесами, як і будь-якими іншими мовами - обмінюватися даними між процесами складніше і дорожче, ніж між потоками, запускати величезну кількість процесів або створювати та знищувати це може бути дорого. їх часто і т. д. Але GIL важить велику рівновагу до процесів, таким чином, що не відповідає дійсності, скажімо, C або Java. Отже, у Python ви опинитесь багатопроцесорними, але набагато частіше, ніж у C чи Java.
Тим часом філософія Python "включені батареї" приносить добру новину: писати код дуже легко, який можна перемикати назад і вперед між потоками та процесами, змінюючи однолінійку.
Якщо ви розробляєте свій код з точки зору автономних "завдань", які не поділяють нічого з іншими завданнями (або основною програмою), окрім введення та виводу, ви можете використовувати concurrent.futures
бібліотеку для запису коду навколо пулу потоків таким чином:
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
executor.submit(job, argument)
executor.map(some_function, collection_of_independent_things)
# ...
Ви навіть можете отримати результати цих завдань і передати їх на подальші робочі місця, чекати речей у порядку виконання або в порядку завершення тощо; Future
детально прочитайте розділ на об'єктах.
Тепер, якщо виявиться, що у вашій програмі постійно використовується 100% процесор, а додавання більшої кількості потоків просто робить це повільніше, ви стикаєтеся з проблемою GIL, тому вам потрібно перейти до процесів. Все, що вам потрібно зробити - це змінити перший рядок:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
Єдине справжнє застереження полягає в тому, що аргументи ваших завдань і повернені значення повинні бути підбірними (і не потребувати занадто багато часу або пам’яті, щоб підбирати), щоб бути корисним перехресним процесом. Зазвичай це не проблема, але іноді це є.
Але що робити, якщо ваші роботи не можуть бути самостійними? Якщо ви можете розробити свій код з точки зору завдань, які передають повідомлення одне до іншого, це все ще досить просто. Можливо, вам доведеться використовувати threading.Thread
або multiprocessing.Process
замість цього покладатися на басейни. І вам доведеться явно створювати queue.Queue
або multiprocessing.Queue
об’єкти. (Є багато інших варіантів - труби, розетки, файли зі зграями… Але справа в тому, що вам доведеться щось робити вручну, якщо автоматична магія Виконавця недостатня.)
Але що робити, якщо ви навіть не можете покластися на передачу повідомлення? Що робити, якщо вам потрібно два завдання, щоб обидві мутувати одну і ту ж структуру і бачити зміни один одного? У цьому випадку вам потрібно буде виконати ручну синхронізацію (блокування, семафори, умови тощо) і, якщо ви хочете використовувати процеси, явні об'єкти спільної пам'яті для завантаження. Це коли багатопотокове чи багатопроцесорне ускладнення. Якщо ви можете уникнути цього, чудово; якщо ви не можете, вам потрібно буде прочитати більше, ніж хтось зможе відповісти на відповідь.
З коментаря ви хотіли дізнатися, що відрізняється між потоками та процесами в Python. Дійсно, якщо ви прочитаєте відповідь Джуліо Франко і мої та всі наші посилання, це повинно охоплювати все… але резюме, безумовно, буде корисним, так ось:
- Нитки діляться даними за замовчуванням; процесів немає.
- Як наслідок (1), передача даних між процесами, як правило, вимагає підбирання та видалення їх. **
- Як інший наслідок (1), безпосередньо обмін даними між процесами, як правило, вимагає розміщення їх у форматах низького рівня, таких як Value, Array та
ctypes
типи.
- Процеси не підлягають GIL.
- На деяких платформах (в основному Windows) процеси набагато дорожчі для створення та знищення.
- Існують деякі додаткові обмеження щодо процесів, деякі з яких різні на різних платформах. Докладніше див. Вказівки щодо програмування .
threading
Модуль не має деякі особливості multiprocessing
модуля. (Ви можете використовувати multiprocessing.dummy
для отримання більшості відсутніх API поверх потоків, або ви можете використовувати модулі вищого рівня, як, concurrent.futures
і не турбуватися про це.)
* Ця проблема не є насправді Python, мовою, але CPython, "стандартною" реалізацією цієї мови. Деякі інші реалізації не мають GIL, як Jython.
** Якщо ви використовуєте метод запуску fork для багатопроцесорної обробки, який ви можете використовувати на більшості платформ, що не належать до Windows, кожен дочірній процес отримує будь-які ресурси, у яких був батько, коли дитина була запущена, що може бути ще одним способом передачі даних дітям.
Thread
модуль (викликається_thread
в python 3.x). Якщо чесно, я ніколи не розумів відмінностей сам ...