Чи непотрібні замки в багатопотоковому коді Python через GIL?


76

Якщо ви покладаєтесь на реалізацію Python, яка має Global Interpreter Lock (тобто CPython) і пише багатопотоковий код, вам справді потрібні блокування?

Якщо GIL не дозволяє паралельно виконувати декілька вказівок, чи не буде спільні дані непотрібними для захисту?

вибачте, якщо це глупе запитання, але це те, що я завжди дивувався про Python на багатопроцесорних / ядерних машинах.

те саме стосується будь-якої іншої мовної реалізації, що має GIL.


1
Також зверніть увагу на те, що GIL - це та деталі реалізації. Наприклад, IronPython та Jython не мають GIL.
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

Відповіді:


72

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

Наприклад:

#!/usr/bin/env python
import threading

shared_balance = 0

class Deposit(threading.Thread):
    def run(self):
        for _ in xrange(1000000):
            global shared_balance
            balance = shared_balance
            balance += 100
            shared_balance = balance

class Withdraw(threading.Thread):
    def run(self):
        for _ in xrange(1000000):
            global shared_balance
            balance = shared_balance
            balance -= 100
            shared_balance = balance

threads = [Deposit(), Withdraw()]

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

print shared_balance

Тут ваш код може бути перерваний між читанням спільного стану ( balance = shared_balance) та написанням зміненого результату назад ( shared_balance = balance), спричиняючи втрату оновлення. Результатом є випадкове значення для загального стану.

Щоб зробити оновлення послідовними, методам запуску потрібно буде зафіксувати загальний стан навколо розділів читання-модифікація-запис (усередині циклів) або мати спосіб виявити, коли спільний стан змінився з моменту його читання .


Приклад коду дає чітке та наочне розуміння! Гарний пост Гаррісе! Я би хотів, щоб я міг двічі проголосувати!
RayLuo

Чи буде безпечно, якщо буде лише один рядок shared_balance += 100і shared_balance -= 100?
mrgloom

24

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

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


11

Додавання до обговорення:

Оскільки GIL існує, деякі операції є атомними в Python і не потребують блокування.

http://www.python.org/doc/faq/library/#what-kinds-of-global-value-mutation-are-thread-safe

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


9

Цей пост описує GIL на досить високому рівні:

Особливий інтерес представляють ці цитати:

Кожні десять інструкцій (це значення за замовчуванням можна змінити) ядро ​​випускає GIL для поточного потоку. У цей момент ОС вибирає потік з усіх потоків, які конкурують за блокування (можливо, вибираючи той самий потік, який щойно випустив GIL - ви не маєте контролю над тим, який потік буде обраний); цей потік отримує GIL, а потім працює ще на десять байт-кодів.

і

Уважно зауважте, що GIL обмежує лише чистий код Python. Розширення (зовнішні бібліотеки Python, які зазвичай пишуться на мові C) можна записати, що звільняють блокування, що дозволяє інтерпретатору Python працювати окремо від розширення, доки розширення не придбає блокування.

Здається, GIL просто надає менше можливих примірників для перемикання контексту і змушує багатоядерні / процесорні системи поводитися як одне ядро ​​щодо кожного екземпляра інтерпретатора python, так що так, вам все одно потрібно використовувати механізми синхронізації.


2
Зверніть увагу, sys.getcheckinterval()повідомляє, скільки команд байт-коду виконується між "випусками GIL" (і минуло 100 (а не 10) принаймні 2,5). У 3.2 це може бути перехід на інтервал, заснований на часі (5 мс або близько того), а не на підрахунок інструкцій. Зміна може бути застосована і до 2.7, хоча це ще триває.
Пітер Хансен,

8

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

Відповідь, з якою я стикався раз за разом, полягає в тому, що багатопотоковість у Python рідко коштує накладних витрат через це. Я чув хороші речі про проект PyProcessing , який робить запуск декількох процесів таким же «простим», як багатопотоковість, зі спільними структурами даних, чергами тощо (PyProcessing буде введено в стандартну бібліотеку майбутнього Python 2.6 як багатопроцесорний модуль .) Це допоможе вам обійти GIL, оскільки кожен процес має власного інтерпретатора.


4

Подумайте про це так:

На одному процесорному комп'ютері багатопотоковість відбувається шляхом призупинення одного потоку і запуску іншого досить швидко, щоб здавалося, що він працює одночасно. Це як Python з GIL: насправді коли-небудь працює лише один потік.

Проблема полягає в тому, що потік можна призупинити де завгодно, наприклад, якщо я хочу обчислити b = (a + b) * 3, це може створити інструкції приблизно так:

1    a += b
2    a *= 3
3    b = a

Тепер давайте скажемо, що працює в потоці, і цей потік призупинено після будь-якого рядка 1 або 2, а потім інший потік запускається і запускається:

b = 5

Потім, коли інший потік відновлюється, b замінюється старими обчисленими значеннями, що, мабуть, не те, що очікувалося.

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


1

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


1

Замки все ще потрібні. Я спробую пояснити, навіщо вони потрібні.

Будь-яка операція / інструкція виконується в інтерпретаторі. GIL гарантує, що інтерпретатор утримується одним потоком у певний момент часу . А ваша програма з декількома потоками працює в одному інтерпретаторі. У будь-який конкретний момент часу цей інтерпретатор утримується однією ниткою. Це означає, що в будь-який момент часу працює лише потік, який утримує інтерпретатор .

Припустимо, що є два потоки, скажімо t1 і t2, і обидва хочуть виконати дві інструкції, які читають значення глобальної змінної та збільшують її.

#increment value
global var
read_var = var
var = read_var + 1

Як зазначено вище, GIL гарантує лише те, що два потоки не можуть виконувати команду одночасно, а це означає, що обидва потоки не можуть виконуватися read_var = varв будь-який конкретний момент часу. Але вони можуть виконувати інструкції один за одним, і ви все одно можете мати проблеми. Розглянемо цю ситуацію:

  • Припустимо, read_var дорівнює 0.
  • GIL утримується ниткою t1.
  • t1 виконується read_var = var. Отже, read_var у t1 дорівнює 0. GIL гарантує лише те, що ця операція читання не буде виконана для будь-якого іншого потоку в цей момент.
  • GIL надається потоку t2.
  • t2 виконується read_var = var. Але read_var все одно 0. Отже, read_var у t2 дорівнює 0.
  • GIL присвоюється t1.
  • t1 виконується, var = read_var+1а var стає 1.
  • GIL присвоюється t2.
  • t2 вважає read_var = 0, тому що саме це він прочитав.
  • t2 виконується, var = read_var+1і var стає 1.
  • Ми очікували, що varмає стати 2.
  • Отже, замок повинен використовуватися для збереження як збільшення, так і збільшення як атомної операції.
  • Відповідь Уіла Гарріса пояснює це на прикладі коду.

0

Трохи оновлення з прикладу Вілла Гарріса:

class Withdraw(threading.Thread):  
def run(self):            
    for _ in xrange(1000000):  
        global shared_balance  
        if shared_balance >= 100:
          balance = shared_balance
          balance -= 100  
          shared_balance = balance

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

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

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


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