Можем посмотреть при помощи команды top,
что этот пример потребляет
определенное количество центрального процессора.
Да, видим, действительно, наш интерпретатор потребляет
почти 100% CPU на одном ядре.
Давайте вернемся к программе и рассмотрим, что она делает.
Прежде всего, у нас есть функция count,
которая в цикле уменьшает значение счетчика до нуля.
Нам необходимо выполнить два вызова этой функции
со значением 100_000_000 и засечь, сколько времени займет
выполнение двух этих функций с этим счетчиком.
То есть функция потребляет только центральный процессор.
Она не делает никаких операций ввода-вывода, не ходит в сеть.
Для сравнения мы можем выполнить эту функцию в потоке.
Создадим два потока при помощи уже известных нам ранее
методов модуля threading.
Передадим туда эту функцию, те же самые аргументы,
запустим наши потоки, подождем, пока они завершатся
при помощи метода join, и выведем количество секунд,
которое было потрачено на выполнение работы этих двух потоков.
Давайте вернемся в консоль и посмотрим на
результаты работы наших функций.
Мы видим, что параллельное выполнение
при помощи потоков заняло больше времени.
Как же так?
Тогда зачем нужны потоки вообще, и почему так происходит?
Всё дело как раз здесь в глобальной блокировке интерпретатора.
Дело в том, что потоки при выполнении своего кода
каждый раз получают блокировку интерпретатора.
Если у нас задача CPU bound, так называют задачи,
которые потребляют только процессор, то код, написанный с
использованием тредов в Python, будет неэффективным.
Он будет работать медленнее, чем код, который
запущен последовательно.
Тем не менее, если мы код нашей функции
заменим, например, на задачу, которая требует
операции ввода-вывода, то мы заметим
большой прирост в итоговом времени выполнения,
если сравнивать параллельное выполнение и выполнение в тредах.
Если изобразить схематично, как выполняется поток,
то выглядит это следующим образом.
У нас есть поток, в котором выполняется наш Python код,
и каждый раз Python интерпретатор пробует получить
глобальную блокировку интерпретатора.
Если Python выполняет операцию ввода-вывода
или системный вызов, то он эту блокировку снимает,
и далее выполнение происходит без блокировки.
Поэтому если у нас таких будет потоков много,
все задачи с вводом-выводом, с ожиданием завершения для
операции ввода-вывода будут очень хорошо параллелиться.
Это нужно учитывать в своих задачах,
если вы будете применять потоки или процессы.
GIL внутри реализован как обычная нерекурсивная блокировка,
или объект класса threading lock.
Все потоки спят пять миллисекунд в ожидании получения блокировки,
и в Python 3, если работает один главный поток,
то он не требует освобождения этой глобальной блокировки интерпретатора.
Итак, в этом видео мы обсудили, что такое GIL
и какое он отношение имеет к потокам в Python.
Так, Python потоки — это обычные потоки,
или POSIX threads, но с ограничениями в виде
глобальной блокировки интерпретатора.
Все потоки выполняются с захватом GIL, но системные вызовы
и операции ввода-вывода, для них GIL не нужен.
Итак, мы рассмотрели вопросы
про то, как работают потоки и процессы,
и в следующих видео мы рассмотрим, как устроены сокеты
и как работать с сетью с применением
полученных знаний о потоках и процессах.