Do You Test Ruby Code for Thread Safety?

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

Вы являетесь разработчиком Ruby? Если да, то я уверен, что у вас есть очень смутное представление о том, что такое параллелизм и потокобезопасность. Не обижайтесь, но именно это я понял, работая с кодом на Ruby и общаясь с программистами Ruby за последние полгода. Я активно пишу на Ruby в последнее время и мне нравится язык и экосистема, сопутствующая ему. Zold, экспериментальная криптовалюта, которую мы создаем, практически полностью написана на Ruby. Что это вам говорит? Мне нравится Ruby. Но когда речь заходит о параллелизме, есть пустые места. Очень много.

Взгляните на этот класс Ruby:

Это простой веб-сервер. Он работает — попробуйте запустить его так (вам понадобится установленный Ruby 2.3+).

Затем откройте http://localhost:4567 и вы увидите счетчик. Обновите страницу, и счетчик увеличится. Попробуйте снова. Это работает. Счетчик находится в файле idx.txt и является, по сути, глобальной переменной, которую мы увеличиваем при каждом HTTP-запросе.

Давайте создадим модульный тест для этого, чтобы убедиться, что он автоматически тестируется:

ОК, это не юнит-тест, скорее, это интеграционный тест. Сначала мы запускаем веб-сервер в отдельном потоке. Затем мы ждем одну секунду, чтобы этому потоку было достаточно времени для инициализации сервера. Я знаю, это очень неэстетичный подход, но у меня нет ничего лучше для этого небольшого примера. Затем мы отправляем HTTP-запрос и сравниваем его с ожидаемым числом 1. Наконец, мы останавливаем веб-сервер.

Пока все идет хорошо. Теперь вопрос в том, что произойдет, когда к серверу будет отправлено много запросов? Будет ли он по-прежнему возвращать правильные последовательные числа? Давайте проверим:

Здесь мы делаем тысячу запросов и помещаем все полученные числа в массив. Затем мы применяем функцию uniq к массиву и считаем количество его элементов. Если их количество равно тысяче, значит все работает правильно и мы получили список последовательных, уникальных чисел. Я только что проверил это и оно работает.

Но мы делаем запросы один за другим, поэтому наш сервер не имеет никаких проблем. Мы не делаем запросы одновременно. Они строго следуют друг за другом. Давайте попробуем использовать несколько дополнительных потоков для симуляции параллельного выполнения HTTP-запросов.

Прежде всего, мы сохраняем список чисел в Concurrent::Set, который является потокобезопасной версией Ruby Set. Во-вторых, мы запускаем пять фоновых потоков, каждый из которых делает 200 HTTP-запросов. Они все выполняются параллельно, и мы ждем их завершения, вызывая join для каждого из них. Наконец, мы извлекаем числа из Set и проверяем правильность списка.

Неудивительно, что это не удалось.

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

Чтобы упростить этот тип тестирования, я создал threads, простой Ruby-гем. Вот как это работает:

Это всё. Эта одна строка с Threads.new() заменяет все остальные строки, где нам нужно создавать потоки, убедиться, что они начинаются одновременно, а затем собирать их результаты и убедиться, что их трассы стека видны в консоли, если они “падают” (по умолчанию, журнал ошибок фонового потока не виден).

Попробуйте этот gem в своих проектах, он уже довольно хорошо протестирован, и я использую его во всех своих тестах параллелизма.

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-12-27 at 14:03

sixnines availability badge   GitHub stars