Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Как работает GIL в Ruby. Часть 2

Anna | 20.06.2014 | нет комментариев
В прошлый раз я предложил заглянуть в код MRI, Дабы разобраться с реализацией GIL и ответить на оставшиеся вопросы. Что мы сегодня и сделаем.

Черновая версия этой статьи изобиловала кусками кода на C, впрочем, из-за этого суть терялась в деталях. В финальной версии примерно нет кода, а для любителей поковыряться в исходниках я оставил ссылки на функции, которые упоминал.

В предыдущей серии

Позже первой части остались два вопроса:

  1. Делает ли GIL array << nil атомарной операцией?
  2. Делает ли GIL код на Ruby потокобезопасным?

На 1-й вопрос дозволено ответив, взглянув на реализацию, следственно начнем с него.

В прошлый раз мы разбирались со дальнейшим кодом:

array = []

5.times.map do
  Thread.new do
    1000.times do
      array << nil
    end
  end
end.each(&:join)

puts array.size

Считая массив потокобезопасным, разумно ждать, что в итоге мы получим массив с пятью тысячами элементов. Так как в реальности массив не потокобезопасен, при запуске кода на JRuby либо Rubinius получается итог, чудесный от ожидаемого (массив с менее чем пятью тысячами элементов).

MRI дает ожидаемый итог, но это случайность либо обоснованность? Начнем изыскание с небольшого куска кода на Ruby.

Thread.new do
  array << nil
end

Начнем-с

Дабы разобраться в том, что происходит в этом куске кода, необходимо взглянуть на то, как MRI создает новейший поток, основным образом на код в файлах thread*.c.

Первым делом внутри реализации Thread.new создается новейший нативный поток, тот, что будет применяться Ruby-потоком. Позже этого выполняется функция thread_start_func_2. Взглянем на нее, не исключительно вдаваясь в детали.

Для нас теперь значим совсем не каждый код, следственно я выделил те части, которые нам увлекательны. В начале функции новейший поток захватывает GIL, перед этим дождавшись его освобождения. Где-то в середине функции выполняется блок, с которым был вызван способ Thread.new. В конце концов блокировка освобождается и завершает свою работу нативный поток.

В нашем случае новейший поток создается в основном потоке, значит, мы может предположить, что в нынешний момент GIL удерживается именно им. Раньше чем продолжить выполнение, новейший поток должен дождаться, пока основной поток освободит блокировку.

Посмотрим, что происходит, когда новейший поток пытается захватить GIL.

static void
gvl_acquire_common(rb_vm_t *vm)
{
  if (vm->gvl.acquired) {
    vm->gvl.waiting  ;
    if (vm->gvl.waiting == 1) {
      rb_thread_wakeup_timer_thread_low();
    }

    while (vm->gvl.acquired) {
      native_cond_wait(&vm->gvl.cond, &vm->gvl.lock);
    }

Это часть функции gvl_acquire_common, которая вызывается, когда новейший поток пытается захватить GIL.

Первым делом она проверяет, удерживается ли теснее блокировка. Если удерживается, то признак waitingвозрастает. В случае с нашим кодом, он становится равным 1. В дальнейшей строке следует проверка, не равен ли признак waiting 1. Он равен, следственно дальнейшая строка будит таймерный поток.

Таймерный поток обеспечивает работу потоков MRI, не допуская обстановку, в которой один из них непрерывно удерживает GIL. Но раньше чем перейти к изложению таймерного потока, разберемся с GIL.

Я теснее несколько раз упоминал, что за всяким потоком в MRI стоит нативный поток. Так и есть, но данная схема полагает, что потоки MRI работают параллельно, так же как и нативные. GIL этому препятствует. Дополним схему и сделаем ее больше приближенной к реальности.

Дабы задействовать нативный поток, Ruby-поток вначале должен захватить GIL. GIL служит посредником между Ruby-потоками и соответствующими нативными потоками, гораздо ограничивая параллелизм. На прошлой схеме Ruby-потоки могли применять нативные потоки параллельно. Вторая схема ближе к действительности в случае с MRI — только один поток может держать GIL в определенный момент времени, следственно параллельное выполнение кода всецело исключено.

Для команды разработчиков MRI GIL охраняет внутреннее состояние системы. Вследствие GIL, внутренние конструкции данных не требуют блокировок. Если два потока не могут изменять всеобщие данные единовременно, состояние гонки немыслимо.

Для вас как разработчика написанное выше значит, что параллелизм в MRI крепко лимитирован.

Таймерный поток

Как я теснее говорил, таймерный поток препятствует непрерывному удержанию GIL одним потоком. Таймерный поток — это нативный поток для внутренних нужд MRI, у него нет соответствующего Ruby-потока. Он стартует при запуске интерпретатора в функции rb_thread_create_timer_thread.

Когда MRI только запустился и работает только основной поток, таймерный поток спит. Но как только какой-нибудь поток начинает ждать освобождения GIL, таймерный поток пробуждается.

Эта схема еще вернее иллюстрирует, как реализован GIL в MRI. Поток справа только что запустился и, так как только он один ожидает освобождения GIL, будит таймерный поток.

Всякие 100 ms таймерный поток выставляет флаг прерывания потока, тот, что в данный момент удерживает GIL, с поддержкой макроса RUBY_VM_SET_TIMER_INTERRUPT. Эти подробности главны для понимания того, атомарно ли выражение array << nil.

Это схоже на доктрину квантования времени в ОС, если она вам знакома.

Установка флага не приводит к незамедлительному прерыванию потока (если бы приводила, дозволено было бы уверенно сказать, что выражение array << nil не атомарно).

Обработка флага прерывания

В глубинах файла vm_eval.c находится код обработки вызова способа в Ruby. Он устанавливает окружение для вызова способа и вызывает требуемую функцию. В конце функции vm_call0_body, прямо перед возвратом значения способа, проверяется флаг прерывания.

Если флаг прерывания потока установлен, выполнение кода приостанавливается перед возвратом значения. Перед тем, как исполнить еще какой-либо Ruby-код, нынешний поток освобождает GIL и вызывает функциюsched_yieldsched_yield — это системная функция, которая запрашивает возобновление дальнейшего в очереди потока планировщиком ОС. Позже этого прекращенный поток пытается вновь захватить GIL, перед этим дождавшись, пока иной поток освободит его.

Вот и результат на 1-й вопрос: array << nil является атомарной операцией. Вследствие GIL все Ruby-способы, реализованные экстраординарно на C, атомарны.

То есть данный код:

array = []

5.times.map do
  Thread.new do
    1000.times do
      array << nil
    end
  end
end.each(&:join)

puts array.size

гарантированно дает ожидаемый итог, будучи запущен на MRI (речь идет только о предсказуемости длины массива, насчет порядка элементов никаких гарантий нет — прим. пер.)

Но имейте в виду, что это никак не следует из Ruby-кода. Если вы запустите данный код на иной реализации, в которой нет GIL, он выдаст непредвиденный итог. Благотворно знать, что дает GIL, но писать код, тот, что полагается на GIL — не самая лучшая идея. Поступая так, вы попадаете в обстановку, сходственную вендор-локу.

GIL не предоставляет публичный API. На GIL нет ни документации, ни спефицикации. Некогда команда разработчиков MRI может изменить поведение GIL либо совсем избавиться от нее. Вот отчего написание кода, тот, что зависит от GIL в его нынешней реализации — не слишком отличная идея.

Что насчет способов, реализованных на Ruby?

Выходит, мы знаем, что array << nil — атомарная операция. В этом выражении вызывается один способArray#<<, которому передается константа как параметр и тот, что реализован на C. Переключение контекста, случись оно, не приведет к нарушению целостности данных — данный способ в любом случае освободит GIL только перед заключением.

А что насчет чего-нибудь такого?

array << User.find(1)

Перед тем, как вызвать способ Array#<<, необходимо вычислить значение параметра, то есть вызватьUser.find(1). Как вы допустимо знаете, User.find(1) в свою очередь вызывает уйма способов, написанных на Ruby.

Но GIL делает атомарными только способы, реализованные на C. Для способов на Ruby никаких гарантий нет.

Является ли вызов Array#<< все еще атомарным в новом примере? Да, но не забывайте о том, что еще необходимо исполнить правостороннее выражение. Другими словам, вначале необходимо сделать вызов способа User.find(1), тот, что не является атомарным, и только потом значение, возвращенное им, будет передано в Array#<<.

Что все это значит для меня?

В первой части статьи мы увидели, что может случиться, если переключение контекста придется куда-нибудь на середину функции. GIL предотвращает сходственные обстановки — даже если переключение контекста происходит, другие потоки не сумеют продолжить выполнение, так как будут обязаны ждать освобождения GIL. Все это происходит только при условии, что способ реализован на C, не обращается к коду на Ruby и не освобождает GIL сам (в комментариях к подлинной статье приводят пример — реализованное на C добавление элемента к ассоциативному массиву (Hash) не атомарно, так как обращается к коду на Ruby для того, Дабы получить хэш элемента — прим. пер.)

GIL делает немыслимым состояние гонки внутри реализации MRI, но не делает код на Ruby потокобезопасным. Дозволено сказать, что GIL — это легко специфика MRI, предуготовленная для охраны внутреннего состояния интерпретатора.

Переводчик будет рад услышать примечания и конструктивную критику.

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB