Почему Python предоставляет механизмы блокировки, если он подчиняется GIL?

Я знаю, что потоки Python могут выполнять только байт-код по одному за раз, так почему библиотека потоков обеспечивает блокировки? Я предполагаю, что условия гонки не могут возникнуть, если одновременно выполняется только один поток.

Библиотека предоставляет блокировки, условия и семафоры. Является ли единственной целью этого синхронизировать выполнение?

Обновить:

Я выполнил небольшой эксперимент:

from threading import Thread
from multiprocessing import Process

num = 0

def f():
    global num
    num += 1

def thread(func):
    # return Process(target=func)
    return Thread(target=func)


if __name__ == '__main__':
    t_list = []
    for i in xrange(1, 100000):
        t = thread(f)
        t.start()
        t_list.append(t)

    for t in t_list:
        t.join()

    print num

По сути, я должен был запустить 100k потоков и увеличить их на 1. Возвращенный результат был 99993.

а) Как результат не может быть 99999, если есть GIL-синхронизация и избегание условий гонки? б) Можно ли даже запустить 100k потоков ОС?

Обновление 2, после просмотра ответов:

Если GIL на самом деле не обеспечивает способ выполнения простой операции, такой как атомарный инкремент, какова цель его наличия? Это не помогает с неприятными проблемами параллелизма, так почему это было введено в действие? Я слышал сценарии использования C-расширений, кто-нибудь может привести пример?

2 ответа

Решение

GIL синхронизирует операции с байт-кодом. Только один байт-код может выполняться одновременно. Но если у вас есть операция, которая требует более одного байт-кода, вы можете переключать потоки между байт-кодами. Если вам нужно, чтобы операция была атомарной, то вам нужна синхронизация выше и за пределами GIL.

Например, увеличение целого числа - это не один байт-код:

>>> def f():
...   global num
...   num += 1
...
>>> dis.dis(f)
  3           0 LOAD_GLOBAL              0 (num)
              3 LOAD_CONST               1 (1)
              6 INPLACE_ADD
              7 STORE_GLOBAL             0 (num)
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE

Здесь потребовалось четыре байтовых кода для реализации num += 1, GIL не гарантирует, что x увеличивается атомарно. Ваш эксперимент демонстрирует проблему: вы потеряли обновления, потому что потоки переключались между LOAD_GLOBAL и STORE_GLOBAL.

Цель GIL состоит в том, чтобы счетчики ссылок на объекты Python увеличивались и уменьшались атомарно. Это не предназначено, чтобы помочь вам с вашими собственными структурами данных.

Собственные потоки Python работают на уровне байт-кода. То есть после каждого байт-кода (ну, на самом деле, я считаю, что число байт-кодов настраивается), поток может передавать управление другому потоку.

Любая операция с общим ресурсом, которая не является ни одним байт-кодом, требует блокировки. И даже если данная операция в определенной версии CPython представляет собой один байт-код, это может быть не так в каждой версии каждого интерпретатора, поэтому в любом случае лучше использовать блокировку.

По той же причине, для начала вам нужны блокировки, кроме как на уровне виртуальной машины, а не на аппаратном уровне.

Другие вопросы по тегам