Потоки и асинхронные среды изначально немного сложны. Без хорошей ментальной модели для организации взаимодействия легко попасть в неприятности и закончиться неожиданными результатами. Кроме того, тестирование асинхронного кода может быть затруднено без правильных инструментов или тестовых шаблонов.
Представление о потоках как о людях и общих объектах как о «вещах», которыми можно владеть, помогает организовать работу многопоточной системы. В этом выпуске мы рассмотрим пример, чтобы узнать все о тестировании асинхронного кода Ruby.
Если вы используете Rails или Rack или действительно любое приложение, которое в качестве интерфейса веб-браузера вы находитесь в асинхронной среде. Стойка #call
всегда вызывается асинхронно. Так что, знаете вы об этом или нет, велика вероятность, что вы уже используете многопоточные компоненты.
Тестирование: запуск, сбор и проверка
Тестирование API асинхронного обратного вызова можно сделать синхронным, выполнив три этапа; триггер, сбор и проверка. Подумайте, если каждый поток - это отдельный человек, а объекты - это вещи, которые могут принадлежать только одному человеку за раз.
Мы будем использовать пример Бэтмена и его 7 разных костюмов. Потому что это практический пример, и мы можем понять, насколько важно знать, все ли костюмы с Альфредом для стирки, когда вы собираетесь сбежать и спасти город.
Пример: день стирки в пещере летучих мышей.
Примером может служить Альфред, стирающий костюмы Бэтмена. SuitWashScheduler - это планировщик, который вызывает обратный вызов для каждого события стирки. Планировщик выполняет семь обратных вызовов с интервалом в одну секунду, начиная с одной секунды после запуска. Триггер - это создание SuitWashScheduler
.
class SuitWashScheduler def initialize(cnt) Thread.new { cnt.times { sleep(1.0) yield } } end end
Сбор
Сбор результатов должен быть потокобезопасным, чтобы избежать состояний гонки. Любой объект, совместно используемый более чем одним потоком, должен быть защищен. Защита - это способ отслеживания владельца объекта. Только владелец может вносить изменения или просматривать объект. Костюм можно использовать только с Бэтменом для драки или с Альфредом для стирки.
Чтобы оставаться дружелюбным, нить (в метафоре Бэтмена или Альфреда) только на короткое время переходит в собственность, а затем отказывается от нее. Mutex
обычно используется для отслеживания владельца. Обратный вызов SuitwashScheduler
будет владеть счетчиком результатов при увеличении счетчика. Обратный вызов, выполняемый в потоке SuitWashScheduler
, сигнализирует, что все результаты получены, когда счетчик достигает цели.
Написание примера начинается с настройки некоторых глобальных объектов. В реальном приложении глобальные переменные будут заменены атрибутами класса или объекта.
$main_thread = Thread.current $mu = Mutex.new $count = 0 $target = 7
Менеджмент и собственники
$main_thread
и $mu
используются для управления потоками и ожидания завершения теста, в то время как $target
и $count
отслеживают результаты теста. Помните, что это тривиальный тест, поэтому сбор и проверка результатов должны быть простыми.
Тест начинается с создания нового экземпляра SuitWashScheduler
, давая инициализатору $target
количество итераций. В данном случае 7 костюмов, нуждающихся в стирке. Предоставленный блок будет запущен в потоке SuitWashScheduler
. Для каждой итерации $count
увеличивается и печатается.
Забегая вперед, мы понимаем, что основной, тестовый поток также будет проверять $count
, что означает, что ему также потребуется владение $count
, поэтому необходимо средство владения $count
. Экземпляр $mu
Mutex
является токеном владения. В блоке, переданном вызову SuitWashScheduler.new
, блок $mu.synchronize
становится владельцем достаточно долго, чтобы установить $count
и проверить результаты. Подробнее о результатах читайте чуть позже.
SuitWashScheduler.new($target) { $mu.synchronize { $count += 1 puts $count $main_thread.wakeup if $target <= $count } }
Проверить: все ли костюмы сшиты?
Вернувшись к основному потоку, нам нужно дождаться завершения теста. Бэтмену нужно подождать, пока не будут собраны все 7 костюмов. Необходимо проверить два условия; тесты обновляют $count
, как и ожидалось, или Бэтмену становится скучно ждать завершения теста и истечения времени ожидания. Прежде чем проверять $count
, чтобы увидеть, достиг ли он $target
, необходимо владение $count
. Как и в блоке для SuitWashScheduler
, используется вызов $mu.synchronize
.
Но это может быть неправильно, если мы заблокируем основной поток, как поток SuitWashScheduler
может когда-либо изменить $count
? К счастью, для этого есть хитрый прием. Класс Mutex
имеет метод #sleep
, который отказывается от владения и ждет, пока не истечет время ожидания или не проснется. После пробуждения по таймауту или #wakeup
вызов основного потока $mu
пытается снова стать владельцем перед продолжением. Как только право собственности достигнуто, результаты могут быть проверены, и можно будет определить, прошел или не прошел тест.
$mu.synchronize { $mu.sleep($target + 1) if $target != $count puts 'FAILED' else puts 'Passed! All suits are washed and clean' end }
Округлять
Работа с потоками и асинхронной средой становится проще с правильной ментальной моделью. В этом посте мы использовали людей как метафору для потоков и физических объектов (костюмов) как метафору для общих объектов, которые могут принадлежать только одному потоку или человеку одновременно. Мы думаем, что такой способ абстрактного упрощает понимание и запоминание.
Мы надеемся, что примеры с заставят вас вспомнить механизмы асинхронности, но мы надеемся, что образ Бэтмена, убегающего, пока все его костюмы стираются, не задержится у вас надолго.
PS Если вы закончили со всеми метафорами Бэтмена в блоге, дайте нам знать.
Этот пост написал приглашенный автор Peter Ohler. Питер создает довольно много высокопроизводительного кода и тоже время от времени пишет об этом. Он создал драгоценный камень Agoo, который представляет собой довольно крутой высокопроизводительный HTTP-сервер.
Первоначально опубликовано на blog.appsignal.com 6 ноября 2018 г.