You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

183 lines
14 KiB
Plaintext

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

{
"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
}