[Python] Визуализация использования GIL в CPython
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Интересно, как ведут себя потоки, когда борются за GIL, или немного информации отсюда только для Python3.
Сразу оговорюсь, что использую Ubuntu 16.04 c ядром 4.15.0-115-generic, на машине стоит 4-х ядерный процессор Intel(R) Core(TM) i5-4200U CPU @ 1.60GHz с 4 GB RAM.
Теория
Ни для кого не секрет, что в Linux библиотека потоков реализует стандарт POSIX threads. Реализация потоков в CPython использует данные потоки, из-за чего управление ими полностью осуществляется операционной системой.
GIL в Python3 это булевская переменная locked, доступ к которой защищен мьютексом mutex, и при изменении которой в false, ОС «сигнализирует» какому-то потоку, который ожидает условную переменную cond.
Как это работает
В главном цикле (см. файл ceval.c) в зависимости от некоторых условий вызывается функция eval_frame_handle_pending, в которой, если установлена пременная gil_drop_request, текущий поток освобождает GIL, давая шанс другим потокам его захватить.
/* GIL drop request */
if (_Py_atomic_load_relaxed(&ceval2->gil_drop_request)) {
/* Give another thread a chance */
if (_PyThreadState_Swap(&runtime->gilstate, NULL) != tstate) {
Py_FatalError("tstate mix-up");
}
drop_gil(ceval, ceval2, tstate);
/* Other threads may run now */
take_gil(tstate);
if (_PyThreadState_Swap(&runtime->gilstate, tstate) != NULL) {
Py_FatalError("orphan tstate");
}
}
Переменная gil_drop_request устанавливается в функции take_gil (см. файл ceval_gil.h). Она устанавливается после ожидания потоком interval миллисекунд условной переменной cond. Этот приём не гарантирует, что через данный промежуток времени другой поток получит управление, так как некоторые атомарные операции могут выполняться гораздо дольше. С другой стороны, гарантируется, что после установки переменной gil_drop_request, другой поток (кроме текущего) получит управление.
while (_Py_atomic_load_relaxed(&gil->locked)) {
unsigned long saved_switchnum = gil->switch_number;
unsigned long interval = (gil->interval >= 1 ? gil->interval : 1);
int timed_out = 0;
COND_TIMED_WAIT(gil->cond, gil->mutex, interval, timed_out);
/* If we timed out and no switch occurred in the meantime, it is time
to ask the GIL-holding thread to drop it. */
if (timed_out &&
_Py_atomic_load_relaxed(&gil->locked) &&
gil->switch_number == saved_switchnum)
{
if (tstate_must_exit(tstate)) {
MUTEX_UNLOCK(gil->mutex);
PyThread_exit_thread();
}
assert(is_tstate_valid(tstate));
SET_GIL_DROP_REQUEST(interp);
}
}
В функции drop_gil, после установки переменной locked в false, «сигнализируется» условная переменная cond.
MUTEX_LOCK(gil->mutex);
_Py_ANNOTATE_RWLOCK_RELEASED(&gil->locked, /*is_write=*/1);
_Py_atomic_store_relaxed(&gil->locked, 0);
COND_SIGNAL(gil->cond);
MUTEX_UNLOCK(gil->mutex);
Для изменения значения interval, можно воспользоваться функцией sys.setswitchinterval. По умолчанию это значение равно 5 миллисекундам (можно получить через sys.getswitchinterval).
Если поток пишет в файл или работает с сетью (или выполняет ещё какие-то I/O операции), то в таких случаях GIL отпускается. Так же он не используется в реализации некоторых библиотек, таких как Numpy.
Визуализация
Реализация
Добавим логирование в функции take_gil и drop_gil.
static void
take_gil(PyThreadState *tstate)
{
...
while (_Py_atomic_load_relaxed(&gil->locked)) {
unsigned long saved_switchnum = gil->switch_number;
unsigned long interval = (gil->interval >= 1 ? gil->interval : 1);
int timed_out = 0;
COND_TIMED_WAIT(gil->cond, gil->mutex, interval, timed_out);
fprintf(stdout, "busy gil: %d %d %d\n", pthread_self(), ATOMIC_COUNT, interval);
...
}
...
fprintf(stdout, "take gil: %d %d\n", pthread_self(), ATOMIC_COUNT);
}
static void
drop_gil(struct _ceval_runtime_state *ceval, struct _ceval_state *ceval2,
PyThreadState *tstate)
{
...
fprintf(stdout, "drop gil: %d %d\n", pthread_self(), ATOMIC_COUNT);
}
Можно было бы сохранять данные в массиве и выводить в файл в конце работы скрипта, но в данной реализации логи выводяться на стандартный поток вывода сразу во время выполнения.
По оси абсцисс обозначены просто тики, а не время. Стоит заметить, что нет возможности показать, в какое время потоки вообще работают, так как их запуском и остановкой управляет ОС.
Зелёные полоски — расстояние (в тиках) от тика, когда поток взял управления, до тика, когда — отдал. Красные полоски — расстояние от тика, когда поток установил gil_drop_request, до тика, когда либо установил эту переменную повторно, либо взял управление.
Пример сборки
SPL
git clone https://github.com/python/cpython.git
mkdir debug_python
cd debug_python
../cpython/configure
make
cd ..
Примеры
- Атомарные операции могут выполняться долго.
Запустим:
debug_python/python main.py --type="cpu-bound" 1>logs
python drawing.py
На рисунке ниже в узких полосках время выполнения больше 5 миллисекунд, из-за чего успевает выполнится только один тик (сортировка массива).
- Не обязательно тот поток, что установил gil_drop_request, получит управление.
Запускаем аналогично.
В примере ниже главный поток ждёт 4 раза по около 5 миллисекунд, прежде чем получит управление.
- Попробуем установить значение interval в 30 миллисекунд.
Запускаем аналогично.
Код из main.py
SPL
import sys
import threading
import random
import argparse
text = """
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque id mi tortor. Pellentesque habitant morbi
tristique senectus et netus et malesuada fames ac turpis egestas. Mauris arcu neque, tempor interdum magna non,
fringilla maximus ex. Proin a mollis elit. Nunc lacinia mollis sem, eget sodales ligula vulputate at. In euismod
elit vel mi suscipit, in pellentesque velit tempor. Nullam eleifend ornare risus ac ultricies. Nam interdum velit
sit amet eros dapibus euismod. Proin non orci imperdiet, interdum velit in, cursus justo. Nullam fringilla, tortor
quis sollicitudin pretium, erat felis porta odio, dictum sodales massa nisi id magna. Integer vitae ipsum ac lectus
imperdiet tristique ac a nibh. Interdum et malesuada fames ac ante ipsum primis in faucibus. Maecenas suscipit id mi
ac eleifend. Interdum et malesuada fames ac ante ipsum primis in faucibus.
"""
def func_cpu_bound(n):
for _ in range(n):
[random.randint(1, 1000000) for _ in range(10000)].sort()
def func_io_bound(n):
while n > 0:
with open(f"{threading.get_ident()}", "w") as f:
f.write(text)
n -= 1
def run_threads(type):
if type == "cpu-bound":
t1 = threading.Thread(target=func_cpu_bound, args=(10000000,))
t2 = threading.Thread(target=func_cpu_bound, args=(10000000,))
elif type == "io-bound":
t1 = threading.Thread(target=func_io_bound, args=(50,))
t2 = threading.Thread(target=func_io_bound, args=(50,))
t1.start()
t2.start()
t1.join()
t2.join()
def run_without_threads(type):
if type == "cpu-bound":
func_cpu_bound(10000000)
func_cpu_bound(10000000)
elif type == "io-bound":
func_io_bound(50)
func_io_bound(50)
def parse_args():
parser = argparse.ArgumentParser()
parser.add_argument("-wt", "--without-threads", dest="without_threads", action="store_true")
parser.add_argument("-t", "--type", dest="type", choices=["cpu-bound", "io-bound"])
args = parser.parse_args()
return args
def main():
args = parse_args()
if args.without_threads:
run_without_threads(args.type)
else:
run_threads(args.type)
if __name__ == "__main__":
main()
Код из drawing.py
SPL
from collections import defaultdict
import matplotlib.pyplot as plt
import matplotlib.patches as patches
with open("logs", "r") as f:
fig, ax = plt.subplots(1, figsize=(20, 10))
lines = defaultdict(list)
for line in f:
if ":" not in line:
continue
name, tokens = line.split(":")
name = name.strip()
if "gil" in name:
ident, num_op, *other = tokens.strip().split()
ident = int(ident)
num_op = int(num_op)
lines[ident].append((num_op, name))
def get_color(name):
if name == "take gil":
return "g"
elif name == "busy gil":
return "r"
elif name == "drop gil":
return "y"
for idx, (key, items) in enumerate(lines.items()):
for i in range(len(items)):
if items[i][1] in ["take gil", "busy gil"]:
if i + 1 < len(items):
rect = patches.Rectangle((items[i][0], idx), items[i + 1][0] - items[i][0], 1, color=get_color(items[i][1]), fill=True)
ax.add_patch(rect)
else:
rect = patches.Rectangle((items[i][0], idx), num_op - items[i][0], 1, color=get_color(items[i][1]), fill=True)
ax.add_patch(rect)
plt.xlim(9950, num_op)
plt.ylim(0, 3)
plt.show()
Заключение
В примерах видно, что все отрезки примерно равны, что позволяет предположить, что все эти отрезки примерно по 5 миллисекунд. Из чего следует, что в Python3 не возможна ситуация, когда один поток надолго захватит управление, как это было в Python2. И не считая ситуации с «длительными» атомарными инструкциями, в общем, каждый поток через небольшие промежутки времени «с большой вероятностью» снова будет получать квант времени. Выходит, что выполнение хоть и не параллельное, но всё же.
===========
Источник:
habr.com
===========
Похожие новости:
- [Python, Кодобред] Сказка про декораторы в Python
- [Системное администрирование, IT-инфраструктура, SAN] Мониторинг СХД IBM Storwize при помощи Zabbix
- [Python, Алгоритмы, Визуализация данных, Графический дизайн, Дизайн] Песочный алфавит при помощи генеративных алгоритмов (перевод)
- [Python, Карьера в IT-индустрии, Статистика в IT] Зарплаты Python-разработчиков: самые большие зарплаты не в Москве, а в Воронеже нет сеньоров
- [DevOps, Kubernetes, Python] Конфигурация проекта внутри и вне Kubernetes
- [PostgreSQL, Django, Apache] Поднимаем Django стек на MS Windows
- [Python, Программирование] Парсинг и аудит
- [Занимательные задачки, Python, Учебный процесс в IT, Логические игры] DataArt запустил бесплатную платформу Kiddo — онлайн-задачник для школьников, изучающих Питон
- [Разработка веб-сайтов, Python, Открытые данные] Разработка онлайн-сервиса для инвесторов на pythonanywhere.com с использованием данных Yahoo Finance
- [GitHub, Open source, Python, Машинное обучение, Программирование] Делаем нейронную сеть, которая сможет отличить борщ от пельмешек
Теги для поиска: #_python, #_python, #_cpython, #_python
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:52
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Интересно, как ведут себя потоки, когда борются за GIL, или немного информации отсюда только для Python3. Сразу оговорюсь, что использую Ubuntu 16.04 c ядром 4.15.0-115-generic, на машине стоит 4-х ядерный процессор Intel(R) Core(TM) i5-4200U CPU @ 1.60GHz с 4 GB RAM. Теория Ни для кого не секрет, что в Linux библиотека потоков реализует стандарт POSIX threads. Реализация потоков в CPython использует данные потоки, из-за чего управление ими полностью осуществляется операционной системой. GIL в Python3 это булевская переменная locked, доступ к которой защищен мьютексом mutex, и при изменении которой в false, ОС «сигнализирует» какому-то потоку, который ожидает условную переменную cond. Как это работает В главном цикле (см. файл ceval.c) в зависимости от некоторых условий вызывается функция eval_frame_handle_pending, в которой, если установлена пременная gil_drop_request, текущий поток освобождает GIL, давая шанс другим потокам его захватить. /* GIL drop request */
if (_Py_atomic_load_relaxed(&ceval2->gil_drop_request)) { /* Give another thread a chance */ if (_PyThreadState_Swap(&runtime->gilstate, NULL) != tstate) { Py_FatalError("tstate mix-up"); } drop_gil(ceval, ceval2, tstate); /* Other threads may run now */ take_gil(tstate); if (_PyThreadState_Swap(&runtime->gilstate, tstate) != NULL) { Py_FatalError("orphan tstate"); } } Переменная gil_drop_request устанавливается в функции take_gil (см. файл ceval_gil.h). Она устанавливается после ожидания потоком interval миллисекунд условной переменной cond. Этот приём не гарантирует, что через данный промежуток времени другой поток получит управление, так как некоторые атомарные операции могут выполняться гораздо дольше. С другой стороны, гарантируется, что после установки переменной gil_drop_request, другой поток (кроме текущего) получит управление. while (_Py_atomic_load_relaxed(&gil->locked)) {
unsigned long saved_switchnum = gil->switch_number; unsigned long interval = (gil->interval >= 1 ? gil->interval : 1); int timed_out = 0; COND_TIMED_WAIT(gil->cond, gil->mutex, interval, timed_out); /* If we timed out and no switch occurred in the meantime, it is time to ask the GIL-holding thread to drop it. */ if (timed_out && _Py_atomic_load_relaxed(&gil->locked) && gil->switch_number == saved_switchnum) { if (tstate_must_exit(tstate)) { MUTEX_UNLOCK(gil->mutex); PyThread_exit_thread(); } assert(is_tstate_valid(tstate)); SET_GIL_DROP_REQUEST(interp); } } В функции drop_gil, после установки переменной locked в false, «сигнализируется» условная переменная cond. MUTEX_LOCK(gil->mutex);
_Py_ANNOTATE_RWLOCK_RELEASED(&gil->locked, /*is_write=*/1); _Py_atomic_store_relaxed(&gil->locked, 0); COND_SIGNAL(gil->cond); MUTEX_UNLOCK(gil->mutex); Для изменения значения interval, можно воспользоваться функцией sys.setswitchinterval. По умолчанию это значение равно 5 миллисекундам (можно получить через sys.getswitchinterval). Если поток пишет в файл или работает с сетью (или выполняет ещё какие-то I/O операции), то в таких случаях GIL отпускается. Так же он не используется в реализации некоторых библиотек, таких как Numpy. Визуализация Реализация Добавим логирование в функции take_gil и drop_gil. static void
take_gil(PyThreadState *tstate) { ... while (_Py_atomic_load_relaxed(&gil->locked)) { unsigned long saved_switchnum = gil->switch_number; unsigned long interval = (gil->interval >= 1 ? gil->interval : 1); int timed_out = 0; COND_TIMED_WAIT(gil->cond, gil->mutex, interval, timed_out); fprintf(stdout, "busy gil: %d %d %d\n", pthread_self(), ATOMIC_COUNT, interval); ... } ... fprintf(stdout, "take gil: %d %d\n", pthread_self(), ATOMIC_COUNT); } static void drop_gil(struct _ceval_runtime_state *ceval, struct _ceval_state *ceval2, PyThreadState *tstate) { ... fprintf(stdout, "drop gil: %d %d\n", pthread_self(), ATOMIC_COUNT); } Можно было бы сохранять данные в массиве и выводить в файл в конце работы скрипта, но в данной реализации логи выводяться на стандартный поток вывода сразу во время выполнения. По оси абсцисс обозначены просто тики, а не время. Стоит заметить, что нет возможности показать, в какое время потоки вообще работают, так как их запуском и остановкой управляет ОС. Зелёные полоски — расстояние (в тиках) от тика, когда поток взял управления, до тика, когда — отдал. Красные полоски — расстояние от тика, когда поток установил gil_drop_request, до тика, когда либо установил эту переменную повторно, либо взял управление. Пример сборкиSPLgit clone https://github.com/python/cpython.git
mkdir debug_python cd debug_python ../cpython/configure make cd .. Примеры
Код из main.pySPLimport sys
import threading import random import argparse text = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque id mi tortor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Mauris arcu neque, tempor interdum magna non, fringilla maximus ex. Proin a mollis elit. Nunc lacinia mollis sem, eget sodales ligula vulputate at. In euismod elit vel mi suscipit, in pellentesque velit tempor. Nullam eleifend ornare risus ac ultricies. Nam interdum velit sit amet eros dapibus euismod. Proin non orci imperdiet, interdum velit in, cursus justo. Nullam fringilla, tortor quis sollicitudin pretium, erat felis porta odio, dictum sodales massa nisi id magna. Integer vitae ipsum ac lectus imperdiet tristique ac a nibh. Interdum et malesuada fames ac ante ipsum primis in faucibus. Maecenas suscipit id mi ac eleifend. Interdum et malesuada fames ac ante ipsum primis in faucibus. """ def func_cpu_bound(n): for _ in range(n): [random.randint(1, 1000000) for _ in range(10000)].sort() def func_io_bound(n): while n > 0: with open(f"{threading.get_ident()}", "w") as f: f.write(text) n -= 1 def run_threads(type): if type == "cpu-bound": t1 = threading.Thread(target=func_cpu_bound, args=(10000000,)) t2 = threading.Thread(target=func_cpu_bound, args=(10000000,)) elif type == "io-bound": t1 = threading.Thread(target=func_io_bound, args=(50,)) t2 = threading.Thread(target=func_io_bound, args=(50,)) t1.start() t2.start() t1.join() t2.join() def run_without_threads(type): if type == "cpu-bound": func_cpu_bound(10000000) func_cpu_bound(10000000) elif type == "io-bound": func_io_bound(50) func_io_bound(50) def parse_args(): parser = argparse.ArgumentParser() parser.add_argument("-wt", "--without-threads", dest="without_threads", action="store_true") parser.add_argument("-t", "--type", dest="type", choices=["cpu-bound", "io-bound"]) args = parser.parse_args() return args def main(): args = parse_args() if args.without_threads: run_without_threads(args.type) else: run_threads(args.type) if __name__ == "__main__": main() Код из drawing.pySPLfrom collections import defaultdict
import matplotlib.pyplot as plt import matplotlib.patches as patches with open("logs", "r") as f: fig, ax = plt.subplots(1, figsize=(20, 10)) lines = defaultdict(list) for line in f: if ":" not in line: continue name, tokens = line.split(":") name = name.strip() if "gil" in name: ident, num_op, *other = tokens.strip().split() ident = int(ident) num_op = int(num_op) lines[ident].append((num_op, name)) def get_color(name): if name == "take gil": return "g" elif name == "busy gil": return "r" elif name == "drop gil": return "y" for idx, (key, items) in enumerate(lines.items()): for i in range(len(items)): if items[i][1] in ["take gil", "busy gil"]: if i + 1 < len(items): rect = patches.Rectangle((items[i][0], idx), items[i + 1][0] - items[i][0], 1, color=get_color(items[i][1]), fill=True) ax.add_patch(rect) else: rect = patches.Rectangle((items[i][0], idx), num_op - items[i][0], 1, color=get_color(items[i][1]), fill=True) ax.add_patch(rect) plt.xlim(9950, num_op) plt.ylim(0, 3) plt.show() Заключение В примерах видно, что все отрезки примерно равны, что позволяет предположить, что все эти отрезки примерно по 5 миллисекунд. Из чего следует, что в Python3 не возможна ситуация, когда один поток надолго захватит управление, как это было в Python2. И не считая ситуации с «длительными» атомарными инструкциями, в общем, каждый поток через небольшие промежутки времени «с большой вероятностью» снова будет получать квант времени. Выходит, что выполнение хоть и не параллельное, но всё же. =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 23-Ноя 00:52
Часовой пояс: UTC + 5