Python Lock и GIL в многопоточности - PullRequest
0 голосов
/ 28 августа 2018

Если в Python есть GIL и даже многопоточные программы не работают одновременно, если они не связаны с вводом / выводом, нужна ли нам концепция блокировки в Python?

1 Ответ

0 голосов
/ 28 августа 2018

Простой ответ заключается в том, что вам нужны блокировки для каждой операции с общими изменяемыми данными, и что «операция» означает для вашего алгоритма, может быть намного больше, чем то, что защищает GIL.


Часто легче понять вещи на конкретном примере, чем с помощью абстракций, поэтому давайте придумаем один. У вас есть итерация строк, и вы хотите посчитать слова. Для каждой строки вы вызываете эту функцию:

def updatecounts(counts, line):
    for word in line.split():
        if word in counts:
            counts[word] += 1
        else:
             counts[word] = 1

Теперь, вместо того, чтобы просто вызывать updatecounts в цикле, вы создаете исполнителя потока или пул и вызываете pool.map(partial(updatecounts, count), lines). (Хорошо, это было бы глупо, но скажем, у вас было 100 клиентских сокетов, производящих строки; тогда было бы разумно иметь потоки, которые вызывали эту функцию в середине своей другой работы.)

Поток 1 работает в строке 1, которая начинается с «Сейчас». Он проверяет, находится ли ”Now” в counts. Это не так. Итак ... но затем поток прерывается и поток 3 вступает во владение. Его строка также начинается с «Сейчас», поэтому он проверяет, находится ли ”Now” в counts. Нет, поэтому он устанавливает counts["Now"] в 1. Затем он переходит к следующему слову и… в какой-то момент поток 1 начинает выполнение снова. И что это собиралось сделать? Он устанавливает counts["Now"] в 1. И мы только что потеряли счет.


Способ предотвратить это - обойти замок:

def updatecounts (count, countslock, line): для слова в line.split (): с отсчетом: если слово в счетчиках: считает [слово] + = 1 еще: рассчитывает [слово] = 1

Теперь, если поток 1 прерывается после проверки if word in counts:, он все еще удерживает countslock. Поэтому, когда поток 3 пытается получить тот же countslock, он не может; он блокируется, пока замок не освободится. Система может некоторое время запускать некоторые другие потоки, но в конечном итоге она гарантированно вернется к потоку 1, чтобы она могла завершить свою работу и снять блокировку, прежде чем поток 3 сможет что-либо сделать.


Почему ГИЛ не защищает нас здесь? Потому что GIL не знает, что вы хотите, чтобы все четыре строки были защищены.

Что если бы мы просто использовали Counter, чтобы мы могли написать counts[word] += 1? Ну, это может быть только одна строка исходного кода, но он все равно компилируется в несколько байт-кодов, а уровень, который GIL фактически защищает, это байт-коды.

На самом деле, не совсем очевидно, что такое «байт-код» с точки зрения вашего кода. Вы можете решить это с помощью модуля dis, но даже тогда это не всегда понятно. Например, words in count компилируется в один байт-код, который выполняет сравнение, за исключением того, что байт-код фактически вызывает метод __contains__ для counts. CPython реализует dict.__contains__ на C, а не на Python, и не выпускает GIL. Но что, если counts может быть некоторым отображением, реализованным в Python (например, Counter), для реализации которого требуется несколько байт-кодов? Или, даже с диктовкой, __contains__(word) должен в конечном итоге позвонить word.__hash__. Может ли это выпустить GIL?


Иногда, когда вам действительно необходимо оптимизировать некоторый внутренний цикл, стоит проделать всю работу, чтобы убедиться, что counts определенно является dict, а word определенно является str и все операции гарантированы. в документах (или, если их нет, читая исходный код на C), чтобы хранить GIL, и, следовательно, вы можете быть уверены, что word in counts является атомарным.

Ну, вы можете быть уверены, что это в CPython 3.7 ; если ваш код должен работать на 3.5 или 2.7, вы должны также проверить это. И если он должен работать на Jython, у Jython даже нет GIL ...

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

...