Потоки в Python [закрито]


76

Які модулі використовуються для написання багатопоточних програм на Python? Мені відомі основні механізми одночасності, що надаються мовою, а також Stackless Python , але які їх сильні та слабкі сторони?


Я бачу щонайменше три причини для того, щоб це питання (сумнівне, рекомендаційне та занадто широке) було призупинено.
lpapp

Я знаю, що це питання від 09, але чи може хтось відповісти на нього, asyncioбудь ласка? Дякую заздалегідь і хотів би побачити, як виглядатиме код.
alvas

@lpapp, де запитання породило кілька впевнених відповідей, я вважаю, що його формулювання є об’єктивним (можливо, занадто широким). OP запитує, які модулі використовуються в Python для одночасності та плюси / мінуси для кожного з них. Для мене це звучить розумно.
gyarad

Відповіді:


117

З метою збільшення складності:

Використовуйте модуль різьблення

Плюси:

  • Насправді легко запустити будь-яку функцію (будь-яку викликану насправді) у власному потоці.
  • Спільний доступ до даних є непростим (блокування ніколи не буває простим :), принаймні простим.

Мінуси:

  • Як вже згадувалося , потоки Juergen Python фактично не можуть одночасно отримувати доступ до стану в інтерпретаторі (є один великий замок, сумно відомий Global Interpreter Lock .) Що це означає на практиці, це те, що корисні для завдань, пов'язаних з введенням / виведенням (мережа, запис на диск, тощо), але зовсім не корисний для одночасного обчислення.

Використовуйте модуль багатопроцесорної обробки

У простому варіанті використання це виглядає точно так само, як використання, threadingза винятком того, що кожне завдання виконується у своєму процесі, а не у власному потоці. (Майже дослівно: Якщо взяти приклад Елі , і замінити threadingз multiprocessing, Threadз Process, і Queue(модуль) з multiprocessing.Queue, він повинен працювати нормально.)

Плюси:

  • Фактична паралельність для всіх завдань (відсутність глобального блокування перекладача).
  • Масштабується до декількох процесорів, може навіть масштабуватися до декількох машин .

Мінуси:

  • Процеси повільніші, ніж нитки.
  • Обмін даними між процесами складніше, ніж з потоками.
  • Пам'ять не використовується неявно. Ви повинні явно поділитися цим, або вам потрібно замаринувати змінні та відправляти їх туди-сюди. Це безпечніше, але складніше. (Якщо це все більше важливо, розробники Python, здається, штовхають людей у ​​цьому напрямку.)

Використовуйте модель події, наприклад Twisted

Плюси:

  • Ви отримуєте надзвичайно точний контроль над пріоритетом, над тим, що виконується коли.

Мінуси:

  • Навіть при хорошій бібліотеці асинхронне програмування, як правило, складніше, ніж потокове програмування, важке як з точки зору розуміння того, що має відбутися, так і з точки зору налагодження того, що насправді відбувається.

У всіх випадках я припускаю, що ви вже розумієте багато проблем, пов'язаних із багатозадачністю, зокрема складну проблему, як розподіляти дані між завданнями. Якщо з якихось причин ви не знаєте, коли і як користуватися замками та умовами, вам доведеться починати з них. Код багатозадачності наповнений тонкощами та помилками, і насправді найкраще добре розуміти поняття перед початком.


4
Я стверджую, що ваше складне замовлення майже повністю відстале. Багатопотокове програмування насправді важко зробити правильно (майже ніхто не робить). Програмування подій відрізняється, але насправді легко зрозуміти, що відбувається, і написати тести, які доводять, що він робить те, що повинен. (Я кажу, що на цих вихідних досяг 100% охоплення масово одночасної бібліотеки мережі).
Дастін,

3
Хм Я думаю, програмування подій піддає складність. Це змушує вас мати справу з цим більш безпосередньо. Ви можете стверджувати, що складність властива одночасності, незалежно від того, як ви до неї підходите, і я б з вами погодився. Але, виконавши деякі досить великі потокові програми та програми, засновані на подіях, я думаю, що я дотримуюсь того, що я сказав: програма на основі подій була набагато більше під моїм контролем, але насправді було складніше її кодувати.
кварк

1
Що означає " Процеси" повільніші за потоки. маю на увазі саме? Процеси не можуть бути повільнішими за потоки, оскільки процеси не запускають жодного коду. Потоки в цих процесах запускають код. Я можу лише здогадуватися, що це означало, що запуск процесів відбувається повільніше, що правильно. Отже, якщо вашій програмі потрібно часто запускати новий потік / процес, використання multiprocessingбуде повільнішим. У цьому випадку, ваше багатопотокове використання також можна значно покращити.
gyarad

103

Ви вже отримали досить різноманітні відповіді, починаючи від "фальшивих ниток" і закінчуючи зовнішніми фреймворками, але я бачив, що ніхто не згадував Queue.Queue- "таємний соус" потокової роботи CPython.

Щоб розширити: поки вам не потрібно перекривати обробку процесором із чистим Python (у цьому випадку вам це потрібно, multiprocessingале він також постачається із власною Queueреалізацією, тому ви можете з певними обережностями застосувати загальну пораду I Даю ;-), вбудований Python threadingбуде робити ... але він зробить це набагато краще, якщо ви використовуєте його з розумом , наприклад, наступним чином.

«Забудьте» про спільну пам’ять, нібито головний плюс потокової роботи проти багатопроцесорної обробки - вона не працює добре, вона погано масштабується, ніколи не була, ніколи не буде. Використовуйте спільну пам’ять лише для структур даних, які налаштовані один раз перед тим, як ви породите підпотоки і ніколи не змінювались згодом - для всього іншого зробіть єдиний потік, відповідальний за цей ресурс, і спілкуйтеся з цим потоком через Queue.

Присвятіть спеціалізований потік кожному ресурсу, який ви зазвичай думаєте захищати замками: змінною структурою даних або цілісною їх групою, підключенням до зовнішнього процесу (БД, серверу XMLRPC тощо), зовнішньому файлу тощо тощо. Отримайте невеликий пул потоків для загальних завдань, у яких немає або потрібен спеціальний ресурс такого роду - не породжуйте потоки, коли і коли це потрібно, інакше накладні витрати на переключення потоків вас захоплять.

Зв'язок між двома потоками завжди здійснюється через Queue.Queue- форму передачі повідомлень, єдину розумну основу для багатопроцесорної обробки (крім транзакційної пам'яті, яка є перспективною, але для якої я не знаю жодної реалізації, гідної реалізації, крім In Haskell).

Кожен виділений потік, що управляє одним ресурсом (або невеликим згуртованим набором ресурсів), прослуховує запити в конкретному екземплярі Queue.Queue. Потоки в пулі чекають на одній спільній Queue.Queue (Черга надійно захищена від потоків і не підведе вас у цьому).

Потоки, яким просто потрібно поставити в чергу запит на якусь чергу (спільну чи виділену), роблять це, не чекаючи результатів, і рухаються далі. Потоки, які врешті-решт потребують результату або підтвердження для черги запиту пару (запит, черга отримання) з екземпляром Queue.Queue, яку вони щойно зробили, і врешті-решт, коли відповідь або підтвердження є необхідними для продовження, вони отримують (очікування ) з черги отримання. Будьте впевнені, що готові отримувати відповіді на помилки, а також реальні відповіді чи підтвердження (Twisted's deferredчудово підходять для організації такого роду структурованих відповідей, BTW!).

Ви також можете використовувати Чергу, щоб "припаркувати" екземпляри ресурсів, які можуть бути використані будь-яким одним потоком, але ніколи не будуть спільно використовуватися між кількома потоками одночасно (з'єднання БД з деякими компонентами DBAPI, курсори з іншими тощо) - це дозволяє Вам розслабитися вимога виділеного потоку на користь більшого об’єднання (потік пулу, який отримує із загальної черги запит, що потребує ресурсу, що стоїть у черзі, отримає цей ресурс із відповідної черги додатків, чекаючи, якщо потрібно, тощо, тощо).

Twisted - це насправді хороший спосіб організувати цей менует (або квадратний танець, залежно від ситуації), не лише завдяки відкладеним, але завдяки його звуковій, міцній, масштабованій базовій архітектурі: ви можете організувати речі, щоб використовувати потоки або підпроцеси, лише коли справді виправдано, роблячи більшість речей, які зазвичай вважаються гідними потоків, в одній потоковій події.

Але я усвідомлюю, що Twisted не для всіх - підхід "виділити або об’єднати ресурси, використовувати Чергу до wazoo, ніколи не робити нічого, що потребує блокування, або, забороняйте Гвідо, будь-яку ще більш просунуту процедуру синхронізації, таку як семафор або умова". як і раніше використовувати, навіть якщо ви просто не можете обернути голову асинхронними керованими подіями методологіями, і все одно забезпечите більше надійності та продуктивності, ніж будь-який інший широко застосовний підхід до різьблення, який я коли-небудь стикався.


6
Якби ви могли обрати відповідь, я б зробив це для цієї. Це один з найбільш спонукальних до роздумів, який я знайшов на Stack Overflow.
кварк

1
@quark, дякую за добрі слова, і, рада, що тобі сподобалось!
Алекс Мартеллі,

4
хотілося б подвоїти голос за це
Корі Голдберг

2
Це вагома загальна відповідь щодо потоків: описаний підхід відображає обраний з інших багатопроцесорних мов, включаючи масштаб (LinkedBlockingQueue як допоміжну конструкцію, що стоїть за Actors) та smalltalk. Гідне читання.
StephenBoesch

Чудовий пост Алекс! (насправді не відповідаючи на запитання, але варто прочитати). Де більша частина змісту здається безсмертною (тобто актуальною і сьогодні, хоча ви писали близько 7 років тому), мені було цікаво, як змінився ваш погляд з тих пір. Не соромтеся посилатися на інші ресурси, звичайно ...
gyarad

22

Це залежить від того, що ви намагаєтеся зробити, але я частково просто використовую threadingмодуль у стандартній бібліотеці, тому що це дозволяє дуже легко взяти будь-яку функцію та просто запустити її в окремому потоці.

from threading import Thread

def f():
    ...

def g(arg1, arg2, arg3=None):
    ....

Thread(target=f).start()
Thread(target=g, args=[5, 6], kwargs={"arg3": 12}).start()

І так далі. Я часто налаштовую виробника / споживача за допомогою синхронізованої черги, передбаченої Queueмодулем

from Queue import Queue
from threading import Thread

q = Queue()
def consumer():
    while True:
        print sum(q.get())

def producer(data_source):
    for line in data_source:
        q.put( map(int, line.split()) )

Thread(target=producer, args=[SOME_INPUT_FILE_OR_SOMETHING]).start()
for i in range(10):
    Thread(target=consumer).start()

1
Вибачте, якщо я хочу передати виклику, що повертає значення як ціль, в Thread, як я можу отримати його результат в основному потоці? Чи можливо це, або я повинен використовувати обгортку для функції, вкладаючи її результат у модифікуваний об'єкт? Я не хотів би прив’язувати результат до самого об’єкта потоку. Яка найкраща практика? Дякую.
newtover

3
@newtover: Ви все ще описуєте ту ж саму базову ситуацію потоків виробників / споживачів, що і в моєму прикладі, тому в цьому випадку рішення Pythonic все-таки має використовувати синхронізований об'єкт Черги. Нехай кожен потік розміщує свій результат у черзі вихідних значень, а основний потік отримує їх із черги на дозвіллі. Документацію для класу Queue можна знайти за адресою docs.python.org/library/queue.html, і у них навіть є приклад виконання саме того, що ви описуєте на docs.python.org/library/queue.html#Queue.Queue.join
Елі Кортрайт,

1
дякую за посилання та відповідь. Ще одне запитання: чи є щось схоже на словник з однаковою функціональністю, або краще взяти всі предмети з черги та заповнити словник самостійно? Чи можу я використовувати для цього вбудований словник, приєднуючись до потоків сам?
newtover

2
@newtover: Специфікація Python не гарантує синхронізацію dict, але реалізація CPython робить це. Отже, якщо ви не використовуєте Jython, PyPy, IronPython чи щось інше, ви можете скористатися звичайним диктом, залежно від того, що ви робите. Отже, якщо у вас просто різні потоки, які встановлюють ключі / значення dict, це буде добре. Але якщо ви ітерацію над dict або читаєте / модифікуєте / переналаштовуєте значення dict, то вам, мабуть, доведеться виконати власну синхронізацію, наприклад: docs.python.org/library/…
Eli Courtwright

1
Моє завдання передбачало розпаралелювання обчислення значень відображення без будь-якої іншої обробки відображення в середині. Я використав вбудований дикт. Ви підтверджуєте, що реалізація dict CPython синхронізована, тому я залишатимусь із рішенням. Дякую ще раз.
newtover

13

Kamaelia - це фреймворк python для побудови додатків з безліччю комунікаційних процесів.

(джерело: kamaelia.org ) Камаелія - ​​паралельність зроблена корисною, веселою

У Kamaelia ви будуєте системи з простих компонентів, які спілкуються між собою . Це пришвидшує розробку, значно допомагає технічному обслуговуванню, а також означає, що ви створюєте одночасно одночасно програмне забезпечення . Він має бути доступним для будь-якого розробника, включаючи початківців. Це також робить це весело :)

Що за системи? Мережеві сервери, клієнти, настільні додатки, ігри на основі пігмей, системи перекодування та конвеєри, системи цифрового телебачення, викорінювачі спаму, засоби навчання та ще багато іншого :)

Ось відео з PyCon 2009. Вона починається шляхом порівняння Kamaelia до Twisted і Parallel Python , а потім дає руки на демонстрації Kamaelia.

Легкий паралелізм з Камаелією - Частина 1 (59:08)
Легкий паралелізм з Камаелією - Частина 2 (18:15)


1
Я не знаю точно, чому б хтось відзначав цю відповідь вниз ... зворотне голосування, будь ласка ... якщо ви не можете навести вагому причину, щоб позначити його ...
Джон

15
Думаю, деякі люди ненавидять котів
Сем Хаслер

6

Щодо Камаелії, відповідь вище насправді не стосується переваг тут. Підхід Камаелії забезпечує уніфікований інтерфейс, який є прагматичним і не ідеальним для роботи з потоками, генераторами та процесами в єдиній системі для паралельності.

По суті, це забезпечує метафору запущеної речі, яка має вхідні та вихідні скриньки. Ви надсилаєте повідомлення в папки "Вихідні", а коли вони з'єднані між собою, повідомлення надходять з папок "Вхідні". Ця метафора / API залишається незмінною, незалежно від того, використовуєте ви генератори, потоки чи процеси, чи спілкуєтесь з іншими системами.

Частина "не ідеальна" пов'язана з тим, що синтаксичний цукор ще не доданий для вхідних та вихідних скриньок (хоча це обговорюється) - у системі робиться акцент на безпеці / зручності використання.

Беручи приклад споживача виробника з використанням голих потоків вище, це стає таким у Камаелії:

Pipeline(Producer(), Consumer() )

У цьому прикладі не має значення, це різьбові компоненти чи інше, єдина відмінність полягає в тому, що між ними з точки зору використання є базовий клас для компонента. Компоненти генератора взаємодіють за допомогою списків, потокові компоненти за допомогою Queue.Queues та процеси, засновані на os.pipes.

Причиною такого підходу є ускладнення ускладнення помилок. Під час створення потоків - або будь-якої спільної пам'яті, яка у вас є, проблема номер один, з якою ви стикаєтесь, випадково порушує оновлення спільних даних. За допомогою передачі повідомлень ви усуваєте один клас помилок.

Якщо ви використовуєте оголену різьбу та блокування скрізь, ви, як правило, працюєте з припущенням, що коли ви пишете код, ви не помилитеся. Хоча ми всі до цього прагнемо, це трапляється дуже рідко. Завершуючи замкову поведінку в одному місці, ви спрощуєте, де щось може піти не так. (Контекстні обробники допомагають, але не допомагають при випадкових оновленнях поза обробником контексту)

Очевидно, що не кожен фрагмент коду може бути записаний як передача повідомлень та спільний стиль, тому Kamaelia також має просту програмну транзакційну пам'ять (STM), що є дійсно акуратною ідеєю з неприємною назвою - це більше схоже на контроль версій змінних - тобто перевіряйте деякі змінні, оновлюйте їх і виконайте повернення. Якщо ви отримаєте зіткнення, ви промиєте і повторите.

Відповідні посилання:

У будь-якому випадку, я сподіваюся, що це корисна відповідь. FWIW, основною причиною налаштування Kamaelia є зробити паралельність більш безпечною та простішою у використанні в системах пітонів, без хвоста, який махає собакою. (тобто велике відро компонентів

Я можу зрозуміти, чому інша відповідь Камаелії була змінена, оскільки навіть для мене це більше схоже на рекламу, ніж на відповідь. Як автору "Камаелії" приємно бачити ентузіазм, хоча, сподіваюся, він містить трохи більше відповідного вмісту :-)

І це мій спосіб сказати, будь ласка, зауважте, що ця відповідь за визначенням упереджена, але для мене метою Камаелії є спробувати обговорити найкращу практику ІМО. Я б запропонував випробувати кілька систем і подивитися, яка саме для вас працює. (також якщо це недоречно для переповнення стека, вибачте - я новачок на цьому форумі :-)


3

Я б використовував мікропотоки (Tasklets) Stackless Python, якби мені взагалі довелося використовувати потоки.

Ціла онлайн-гра (масово багатокористувацька) будується навколо Stackless та його принципу багатопоточності - оскільки оригінал просто уповільнюється для масової багатокористувацької властивості гри.

Потоки в CPython широко не рекомендуються. Однією з причин є GIL - глобальний блокувальник інтерпретатора, який серіалізує потокову передачу для багатьох частин виконання. Я відчуваю, що насправді важко створювати швидкі програми таким чином. Мій приклад кодування, де все повільніше з потоковими потоками - з одним ядром (але багато очікувань на введення мало б зробити можливим підвищення продуктивності).

З CPython, скоріше, використовуйте окремі процеси, якщо це можливо.


3

Якщо ви дійсно хочете забруднити руки, можете спробувати використовувати генератори для підробки підпрограм . Це, мабуть, не найефективніше з точки зору задіяної роботи, але програми дійсно пропонують вам дуже точний контроль кооперативної багатозадачності, а не попереджувальну багатозадачність, яку ви знайдете деінде.

Однією з переваг, яку ви знайдете, є те, що вам, за великим рахунком, не знадобляться замки або мьютекси при використанні багатозадачної кооперації, але більш важливою перевагою для мене була майже нульова швидкість перемикання між "потоками". Звичайно, Stackless Python також дуже хороший для цього; а ще є Erlang, якщо це не обов’язково повинен бути Python.

Ймовірно, найбільшим недоліком спільної багатозадачності є загальна відсутність обхідних шляхів блокування вводу-виводу. А у підроблених корутинах ви також зіткнетеся з проблемою, що ви не можете переключати "потоки" ні з чого, крім верхнього рівня стека всередині потоку.

Після того, як ви зробили навіть дещо складну програму з підробленими програмами, ви справді почнете оцінювати роботу, яка переходить до планування процесів на рівні ОС.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.