Многопоточный пример Python GIL

Я довольно много читал о том, насколько «плох» этот бизнес с GIL на питоне при написании многопоточного кода, но я никогда не видел примера. Может кто-нибудь, пожалуйста, дайте мне простой пример, когда GIL вызывает проблемы при использовании потоков.

Спасибо!


person vgoklani    schedule 18.07.2017    source источник


Ответы (1)


Одна из основных причин многопоточности заключается в том, что программа может использовать преимущества нескольких ЦП (и/или нескольких ядер ЦП) для выполнения большего количества операций в секунду. Но в Python GIL означает, что даже если у вас есть несколько потоков, работающих одновременно над вычислением, только один из этих потоков будет фактически работать в любой момент, потому что все остальные будут заблокированы, ожидая получения глобального интерпретатора. замок. Это означает, что многопоточная версия программы на Python на самом деле будет медленнее, чем однопоточная версия, а не быстрее, поскольку одновременно выполняется только один поток. заставляя каждый поток ждать, получать и затем отказываться от GIL (стиль циклического перебора) каждые несколько миллисекунд.

Чтобы продемонстрировать это, вот игрушечный Python-скрипт, который порождает заданное количество потоков, а затем в ходе своих вычислений каждый поток просто постоянно увеличивает счетчик, пока не пройдет 5 секунд. В конце основной поток подсчитывает общее количество произошедших встречных приращений и печатает общее количество, чтобы дать нам измерение того, сколько работы было выполнено за 5-секундный период.

import threading
import sys
import time

numSecondsToRun = 5

class CounterThread(threading.Thread):
   def __init__(self):
      threading.Thread.__init__(self)
      self._counter = 0
      self._endTime = time.time() + numSecondsToRun

   def run(self):
      # Simulate a computation on the CPU
      while(time.time() < self._endTime):
         self._counter += 1

if __name__ == "__main__":
   if len(sys.argv) < 2:
      print "Usage:  python counter 5"
      sys.exit(5)

   numThreads = int(sys.argv[1])
   print "Spawning %i counting threads for %i seconds..." % (numThreads, numSecondsToRun)

   threads = []
   for i in range(0,numThreads):
      t = CounterThread()
      t.start()
      threads.append(t)

   totalCounted = 0
   for t in threads:
      t.join()
      totalCounted += t._counter
   print "Total amount counted was %i" % totalCounted

.... и вот результаты, которые я получаю на своем компьютере (это двухъядерный Mac Mini с включенной гиперпоточностью, FWIW):

$ python counter.py 1
Spawning 1 counting threads for 5 seconds...
Total amount counted was 14210740

$ python counter.py 2
Spawning 2 counting threads for 5 seconds...
Total amount counted was 10398956

$ python counter.py 3
Spawning 3 counting threads for 5 seconds...
Total amount counted was 10588091

$ python counter.py 4
Spawning 4 counting threads for 5 seconds...
Total amount counted was 11091197

$ python counter.py 5
Spawning 5 counting threads for 5 seconds...
Total amount counted was 11130036

$ python counter.py 6
Spawning 6 counting threads for 5 seconds...
Total amount counted was 10771654

$ python counter.py 7
Spawning 7 counting threads for 5 seconds...
Total amount counted was 10464226

Обратите внимание, что наилучшая производительность была достигнута на первой итерации (где был создан только один рабочий поток); производительность подсчета значительно снижалась, когда одновременно выполнялось более одного потока. Это показывает, как производительность многопоточности в Python снижается из-за GIL — та же программа, написанная на C (или любом другом языке без GIL), будет показывать гораздо лучшую производительность с большим количеством запущенных потоков, а не хуже (до тех пор, пока количество рабочих потоков не сравняется с числом рабочих потоков). количество ядер на железе, разумеется).

Это не означает, что многопоточность совершенно бесполезна в Python — она все же полезна в случаях, когда большинство или все ваши потоки заблокированы в ожидании ввода-вывода, а не привязаны к процессору. Это связано с тем, что поток Python, который заблокирован в ожидании ввода-вывода, не удерживает заблокированным GIL, пока он ожидает, поэтому в это время другие потоки все еще могут выполняться. Однако, если вам нужно распараллелить задачу, требующую больших вычислительных ресурсов (например, трассировку лучей или вычисление всех цифр числа Пи, взлом кода и т. п.), вам следует либо использовать несколько процессов, а не несколько потоков, либо использовать другой язык, который не имеет ГИЛ.

person Jeremy Friesner    schedule 18.07.2017