Русский
Русский
English
Статистика
Реклама

Визуализация использования GIL в CPython

Интересно, как ведут себя потоки, когда борются за 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 voidtake_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 voiddrop_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, до тика, когда либо установил эту переменную повторно, либо взял управление.

Пример сборки
git clone https://github.com/python/cpython.gitmkdir debug_pythoncd debug_python../cpython/configuremakecd ..



Примеры


  1. Атомарные операции могут выполняться долго.

    Запустим:
    debug_python/python main.py --type="cpu-bound" 1>logspython drawing.py
    

    На рисунке ниже в узких полосках время выполнения больше 5 миллисекунд, из-за чего успевает выполнится только один тик (сортировка массива).
    image
  2. Не обязательно тот поток, что установил gil_drop_request, получит управление.

    Запускаем аналогично.

    В примере ниже главный поток ждёт 4 раза по около 5 миллисекунд, прежде чем получит управление.
    image
  3. Попробуем установить значение interval в 30 миллисекунд.

    Запускаем аналогично.

    image

Код из main.py
import sysimport threadingimport randomimport argparsetext = """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 -= 1def 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 argsdef main():    args = parse_args()    if args.without_threads:        run_without_threads(args.type)    else:        run_threads(args.type)if __name__ == "__main__":    main()


Код из drawing.py
from collections import defaultdictimport matplotlib.pyplot as pltimport matplotlib.patches as patcheswith 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
К списку статей
Опубликовано: 19.10.2020 18:20:51
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Python

Cpython

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru