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

Представление о потоках как о людях и общих объектах как о «вещах», которыми можно владеть, помогает организовать работу многопоточной системы. В этом выпуске мы рассмотрим пример, чтобы узнать все о тестировании асинхронного кода 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 г.