{ "cells": [ { "cell_type": "markdown", "id": "437382d2", "metadata": {}, "source": [ "# Глобальная блокировка интерпретатора #" ] }, { "cell_type": "markdown", "id": "edf47592", "metadata": {}, "source": [ "На этой лекции мы поговорим о том, что такое глобальная блокировка интерпретатора, или, как ее сокращенно иногда называют, `GIL`.\n", "\n", "`GIL` очень тесно связан с выполнением потоков, и для более глубокого понимания того, как работают потоки в Python, нужны общие сведения о том, как устроен и как работает `GIL` в Python. Многие разработчики знают о `GIL` лишь то, что это какая-то штука, которая не позволяет одновременно двум потокам выполняться на одном ядре процессора, даже если этих ядер у процессора несколько. Тем не менее, `GIL` в первую очередь предназначен для защиты памяти интерпретатора от разрушений и делает все операции с памятью атомарными.\n", "\n", "Давайте рассмотрим следующую программу, которую я представил на слайде, и сразу запустим ее выполнение в консоли, потому что выполнение займет некоторое время." ] }, { "cell_type": "code", "execution_count": 2, "id": "540fbcd9", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "25.041810989379883\n", "71.44423460960388\n" ] } ], "source": [ "# cpu bound programm\n", "\n", "from threading import Thread\n", "import time\n", "\n", "def count(n):\n", " while n > 0:\n", " n -= 1\n", " \n", "# series run\n", "t0 = time.time()\n", "count(100_000_000)\n", "count(100_000_000)\n", "print(time.time() - t0)\n", "\n", "# parallel run\n", "t0 = time.time()\n", "th1 = Thread(target=count, args=(100_000_000,))\n", "th2 = Thread(target=count, args=(100_000_000,))\n", "\n", "th1.start(); th2.start()\n", "th1.join(); th2.join()\n", "print(time.time() - t0)" ] }, { "cell_type": "markdown", "id": "ddfd23f8", "metadata": {}, "source": [ "Итак, вот наш пример. Запускаем его. Можем посмотреть при помощи команды top, что этот пример потребляет определенное количество центрального процессора. Да, видим, действительно, наш интерпретатор потребляет почти 100% CPU на одном ядре." ] }, { "cell_type": "code", "execution_count": 3, "id": "5444e55f", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[?1h\u001b=\u001b[H\u001b[2J\u001b[mtop - 15:30:28 up 415 days, 7:20, 1 user, load average: 0.73, 0.33, 0.19\u001b[m\u001b[m\u001b[m\u001b[m\u001b[K\n", "Tasks:\u001b[m\u001b[m\u001b[1m 314 \u001b[m\u001b[mtotal,\u001b[m\u001b[m\u001b[1m 2 \u001b[m\u001b[mrunning,\u001b[m\u001b[m\u001b[1m 312 \u001b[m\u001b[msleeping,\u001b[m\u001b[m\u001b[1m 0 \u001b[m\u001b[mstopped,\u001b[m\u001b[m\u001b[1m 0 \u001b[m\u001b[mzombie\u001b[m\u001b[m\u001b[m\u001b[m\u001b[K\n", "%Cpu(s):\u001b[m\u001b[m\u001b[1m 4.8 \u001b[m\u001b[mus,\u001b[m\u001b[m\u001b[1m 0.7 \u001b[m\u001b[msy,\u001b[m\u001b[m\u001b[1m 0.0 \u001b[m\u001b[mni,\u001b[m\u001b[m\u001b[1m 94.5 \u001b[m\u001b[mid,\u001b[m\u001b[m\u001b[1m 0.0 \u001b[m\u001b[mwa,\u001b[m\u001b[m\u001b[1m 0.0 \u001b[m\u001b[mhi,\u001b[m\u001b[m\u001b[1m 0.0 \u001b[m\u001b[msi,\u001b[m\u001b[m\u001b[1m 0.0 \u001b[m\u001b[mst\u001b[m\u001b[m\u001b[m\u001b[m\u001b[K\n", "KiB Mem :\u001b[m\u001b[m\u001b[1m 65805548 \u001b[m\u001b[mtotal,\u001b[m\u001b[m\u001b[1m 23703044 \u001b[m\u001b[mfree,\u001b[m\u001b[m\u001b[1m 1669436 \u001b[m\u001b[mused,\u001b[m\u001b[m\u001b[1m 40433068 \u001b[m\u001b[mbuff/cache\u001b[m\u001b[m\u001b[m\u001b[m\u001b[K\n", "KiB Swap:\u001b[m\u001b[m\u001b[1m 33030140 \u001b[m\u001b[mtotal,\u001b[m\u001b[m\u001b[1m 33030140 \u001b[m\u001b[mfree,\u001b[m\u001b[m\u001b[1m 0 \u001b[m\u001b[mused.\u001b[m\u001b[m\u001b[1m 63027376 \u001b[m\u001b[mavail Mem \u001b[m\u001b[m\u001b[m\u001b[m\u001b[K\n", "\u001b[K\n", "\u001b[7m PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND \u001b[m\u001b[m\u001b[K\n", "\u001b[m\u001b[1m19526 mikhayl+ 20 0 128124 6996 2748 R 100.0 0.0 0:05.20 python \u001b[m\u001b[m\u001b[K\n", "\u001b[m19506 mikhayl+ 20 0 921208 45756 7872 S 11.8 0.1 3:14.42 python3 \u001b[m\u001b[m\u001b[K\n", "\u001b[m15053 snaga 20 0 288136 37196 6792 S 5.9 0.1 16:41.20 uwsgi \u001b[m\u001b[m\u001b[K\n", "\u001b[m17818 mikhayl+ 20 0 498940 69812 8296 S 5.9 0.1 4:34.57 jupyter-no+ \u001b[m\u001b[m\u001b[K\n", "\u001b[m\u001b[1m19527 mikhayl+ 20 0 157848 2212 1436 R 5.9 0.0 0:00.05 top \u001b[m\u001b[m\u001b[K\n", "\u001b[m 1 root 20 0 191788 4784 2420 S 0.0 0.0 20:00.79 systemd \u001b[m\u001b[m\u001b[K\n", "\u001b[m 2 root 20 0 0 0 0 S 0.0 0.0 0:04.01 kthreadd \u001b[m\u001b[m\u001b[K\n", "\u001b[m 3 root 20 0 0 0 0 S 0.0 0.0 0:00.62 ksoftirqd/0 \u001b[m\u001b[m\u001b[K\n", "\u001b[m 5 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kworker/0:+ \u001b[m\u001b[m\u001b[K\n", "\u001b[m 8 root rt 0 0 0 0 S 0.0 0.0 0:00.05 migration/0 \u001b[m\u001b[m\u001b[K\n", "\u001b[m 9 root 20 0 0 0 0 S 0.0 0.0 0:00.00 rcu_bh \u001b[m\u001b[m\u001b[K\n", "\u001b[m 10 root 20 0 0 0 0 S 0.0 0.0 20:58.21 rcu_sched \u001b[m\u001b[m\u001b[K\n", "\u001b[m 11 root rt 0 0 0 0 S 0.0 0.0 2:53.71 watchdog/0 \u001b[m\u001b[m\u001b[K\n", "\u001b[m 12 root rt 0 0 0 0 S 0.0 0.0 2:42.51 watchdog/1 \u001b[m\u001b[m\u001b[K\n", "\u001b[m 13 root rt 0 0 0 0 S 0.0 0.0 0:01.13 migration/1 \u001b[m\u001b[m\u001b[K\n", "\u001b[m 14 root 20 0 0 0 0 S 0.0 0.0 0:00.66 ksoftirqd/1 \u001b[m\u001b[m\u001b[K\n", "\u001b[m 16 root 0 -20 0 0 0 S 0.0 0.0 0:00.00 kworker/1:+ \u001b[m\u001b[m\u001b[K\u001b[?1l\u001b>\u001b[25;1H\n", "\u001b[K" ] } ], "source": [ "! top" ] }, { "cell_type": "markdown", "id": "0a282bd2", "metadata": {}, "source": [ "Давайте вернемся к программе и рассмотрим, что она делает. Прежде всего, у нас есть функция `count`, которая в цикле уменьшает значение счетчика до нуля. Нам необходимо выполнить два вызова этой функции со значением `100_000_000` и засечь, сколько времени займет выполнение двух этих функций с этим счетчиком. То есть функция потребляет только центральный процессор. Она не делает никаких операций ввода-вывода, не ходит в сеть. Для сравнения мы можем выполнить эту функцию в потоке. Создадим два потока при помощи уже известных нам ранее методов модуля `threading`. Передадим туда эту функцию, те же самые аргументы, запустим наши потоки, подождем, пока они завершатся при помощи метода `join`, и выведем количество секунд, которое было потрачено на выполнение работы этих двух потоков.\n", "\n", "Давайте вернемся в консоль и посмотрим на результаты работы наших функций. Мы видим, что параллельное выполнение при помощи потоков заняло больше времени. Как же так? Тогда зачем нужны потоки вообще, и почему так происходит?\n", "\n", "Всё дело как раз здесь в глобальной блокировке интерпретатора. Дело в том, что потоки при выполнении своего кода каждый раз получают блокировку интерпретатора. Если у нас задача `CPU bound`, так называют задачи, которые потребляют только процессор, то код, написанный с использованием тредов в Python, будет неэффективным. Он будет работать медленнее, чем код, который запущен последовательно. Тем не менее, если мы код нашей функции заменим, например, на задачу, которая требует операции ввода-вывода, то мы заметим большой прирост в итоговом времени выполнения, если сравнивать параллельное выполнение и выполнение в тредах." ] }, { "cell_type": "markdown", "id": "be510455", "metadata": {}, "source": [ "```\n", "# как выполняется поток?\n", "\n", "a r a r a r a\n", " run |------| run |--------------| run |----| run\n", "------>| IO |-------->| IO |-------->| IO |----->\n", " |------| |--------------| |----|\n", "a r a r a r a\n", "\n", "a - acquire GIL\n", "r - release GIL\n", "\n", "```" ] }, { "cell_type": "markdown", "id": "0cc602c7", "metadata": {}, "source": [ "Если изобразить схематично, как выполняется поток, то выглядит это следующим образом. У нас есть поток, в котором выполняется наш Python код, и каждый раз Python интерпретатор пробует получить глобальную блокировку интерпретатора. Если Python выполняет операцию ввода-вывода или системный вызов, то он эту блокировку снимает, и далее выполнение происходит без блокировки. Поэтому если у нас таких будет потоков много, все задачи с вводом-выводом, с ожиданием завершения для операции ввода-вывода будут очень хорошо параллелиться. Это нужно учитывать в своих задачах, если вы будете применять потоки или процессы.\n", "\n", "`GIL` внутри реализован как обычная нерекурсивная блокировка, или объект класса `threading lock`. Все потоки спят пять миллисекунд в ожидании получения блокировки, и в Python 3, если работает один главный поток, то он не требует освобождения этой глобальной блокировки интерпретатора.\n", "\n", "Итак, на этой лекции мы обсудили, что такое `GIL` и какое он отношение имеет к потокам в Python. Так, Python потоки — это обычные потоки, или `POSIX threads`, но с ограничениями в виде глобальной блокировки интерпретатора. Все потоки выполняются с захватом `GIL`, но системные вызовы и операции ввода-вывода, для них `GIL` не нужен.\n", "\n", "Итак, мы рассмотрели вопросы про то, как работают потоки и процессы, и в следующих лекциях мы рассмотрим, как устроены сокеты и как работать с сетью с применением полученных знаний о потоках и процессах." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.8" } }, "nbformat": 4, "nbformat_minor": 5 }