Обратный отсчет — это абстракция параллелизма, которая позволяет одному или нескольким потокам ждать, пока все другие потоки не выполнят то, что они делают. Поэтому защелка обратного отсчета часто упоминается как примитив синхронизации потока .
Чем полезны защелки обратного отсчета?
Допустим, у вас есть куча тем, которые извлекают, скажем, шутки Чака Норриса. Вы создаете десять потоков и хотите убедиться, что все они завершены, прежде чем переходить к следующему шагу.
В этом случае вы можете создать защелку обратного отсчета со счетчиком десять. Затем, прямо перед следующим шагом в коде, вы можете дождаться завершения ваших потоков. До тех пор ваш код не может продолжаться, пока не завершены все потоки.
Когда каждый поток получает шутку, он уменьшает счетчик. В конце концов, когда все десять потоков закончили извлекать шутки, счетчик в защелке обратного отсчета в конечном итоге достигнет нуля. Это когда код разрешено продолжить.
Внедрение вашей собственной блокировки обратного отсчета: сначала протестируйте!
В предыдущей статье мы рассмотрели, как реализовать Futures в Ruby . В этой статье мы собираемся протестировать реализацию механизма обратного отсчета. Давайте начнем!
Настройка
Давайте загрузим новый проект Ruby. Мой любимый способ — создать драгоценный камень Ruby:
% bundle gem countdown_latch -t Creating gem 'countdown_latch'... MIT License enabled in config create countdown_latch/Gemfile create countdown_latch/.gitignore create countdown_latch/lib/countdown_latch.rb create countdown_latch/lib/countdown_latch/version.rb create countdown_latch/countdown_latch.gemspec create countdown_latch/Rakefile create countdown_latch/README.md create countdown_latch/bin/console create countdown_latch/bin/setup create countdown_latch/LICENSE.txt create countdown_latch/.travis.yml create countdown_latch/.rspec create countdown_latch/spec/spec_helper.rb create countdown_latch/spec/countdown_latch_spec.rb Initializing git repo in /Users/benjamintan/workspace/countdown_latch
Вы заметили, что
-t
квартира добавлена? Этот флаг добавляет RSpec как зависимость разработки.
Далее перейдите в каталог проекта:
% cd countdown_latch
Запустите bin/setup
чтобы установить зависимости:
% bin/setup Resolving dependencies... Using rake 10.4.2 Using bundler 1.10.6 Using countdown_latch 0.1.0 from source at . Using diff-lcs 1.2.5 Using rspec-support 3.3.0 Using rspec-core 3.3.2 Using rspec-expectations 3.3.1 Using rspec-mocks 3.3.2 Using rspec 3.3.0 Bundle complete! 4 Gemfile dependencies, 9 gems now installed. Use `bundle show [gemname]` to see where a bundled gem is installed.
Создание защелки обратного отсчета
Теперь самое интересное! Так как мы делаем этот тест в первую очередь . Тесты помогут нам перейти к реализации, которую мы конкретизируем, чтобы пройти тесты.
В качестве проверки работоспособности мы можем быстро убедиться, что все подключено, как и ожидалось:
% rspec CountdownLatch has a version number does something useful (FAILED - 1) Failures: 1) CountdownLatch does something useful Failure/Error: expect(false).to eq(true) expected: true got: false (compared using ==) # ./spec/countdown_latch_spec.rb:9:in `block (2 levels) in <top (required)>' Finished in 0.01957 seconds (files took 0.07757 seconds to load) 2 examples, 1 failure Failed examples: rspec ./spec/countdown_latch_spec.rb:8 # CountdownLatch does something useful
Отлично! RSpec работает нормально! Теперь откройте тестовый файл, расположенный в spec / countdown latch spec.rb. Мы собираемся написать первый тест.
В качестве аргумента обратного отсчета требуется неотрицательное целое число
Вы должны удалить все в spec / countdown latch spec.rb и заменить его следующим скелетом:
module CountdownLatch describe CountdownLatch do end end
Первый тест — убедиться, что аргумент, переданный в конструктор защелки обратного отсчета, является неотрицательным целым числом:
module CountdownLatch describe CountdownLatch do it "requires a non-negative integer as an argument" do latch = CountdownLatch.new(3) expect(latch.count).to eq(3) end end end
Чтобы это передать, нам нужен конструктор, который принимает неотрицательное целое число в качестве аргумента. Пройти этот тест очень просто:
module CountdownLatch class CountdownLatch def initialize(count) if count.is_a?(Fixnum) && count > 0 @count = count end end end end
В этот момент тест пройдёт. Давайте также удостоверимся, что 0 принято:
module CountdownLatch describe CountdownLatch do it "zero is a valid argument" do latch = CountdownLatch.new(0) expect(latch.count).to eq(0) end end end
Упс! Наш тест поймал что-то:
Failures: 1) CountdownLatch::CountdownLatch zero is a valid argument Failure/Error: expect(latch.count).to eq(0) expected: 0 got: nil (compared using ==)
Оказывается, у нас есть ошибка на одну ошибку при сравнении count
и нуля:
module CountdownLatch class CountdownLatch def initialize(count) if count.is_a?(Fixnum) && count >= 0 @count = count end end end end
Защелка обратного отсчета не может быть инициализирована с отрицательным числом или, например, с Float
, так как эти два случая не имеют смысла. Давайте добавим тесты:
module CountdownLatch describe CountdownLatch do it "throws ArgumentError for negative numbers" do expect { CountdownLatch.new(-1) }.to raise_error(ArgumentError) end it "throws ArgumentError for non-integers" do expect { CountdownLatch.new(1.0) }.to raise_error(ArgumentError) end end end
Теперь тесты не пройдут:
Failures: 1) CountdownLatch::CountdownLatch throws ArgumentError for negative numbers Failure/Error: expect { CountdownLatch.new(-1) }.to raise_error(ArgumentError) expected ArgumentError but nothing was raised # ./spec/countdown_latch_spec.rb:17:in `block (2 levels) in <module:CountdownLatch>' 2) CountdownLatch::CountdownLatch throws ArgumentError for non-integers Failure/Error: expect { CountdownLatch.new(1.0) }.to raise_error(ArgumentError) expected ArgumentError but nothing was raised # ./spec/countdown_latch_spec.rb:21:in `block (2 levels) in <module:CountdownLatch>'
К счастью, это тоже легко исправить. Нам просто нужно получить инициализатор, чтобы вызвать ArgumentError
для ошибок в аргументе. В lib / countdown latch / countdown latch.rb :
module CountdownLatch class CountdownLatch def initialize(count) if count.is_a?(Fixnum) && count >= 0 @count = count @mutex = Mutex.new @condition = ConditionVariable.new else raise ArgumentError end end end end
Теперь все тесты должны пройти.
Отсчет
Защелка обратного отсчета должна знать, как отсчитать свой внутренний счетчик.
module CountdownLatch describe CountdownLatch do it "#count decreases when #count_down is called" do latch = CountdownLatch.new(3) latch.count_down expect(latch.count).to eq(2) end end end
Теперь нам нужен доступ к полю @count
. Это достаточно просто:
module CountdownLatch class CountdownLatch def initialize(count) # ... end def count @count end end end
Метод count_down
выглядит почти упрощенно:
module CountdownLatch class CountdownLatch def count_down @count -= 1 end end end
Не поддавайтесь искушению добавить больше поведения и «разумной» логики, пока тесты не заставят вас что-то с этим сделать. В этом случае тесты теперь должны быть зелеными.
Однако наши тесты не завершены. Задвижка обратного отсчета должна только отсчитывать до нуля и не меньше Давайте добавим тест для этого:
module CountdownLatch describe CountdownLatch do it "#count never reaches below zero" do latch = CountdownLatch.new(0) latch.count_down expect(latch.count).to eq(0) end end end
Вышеуказанный тест не пройден:
1) CountdownLatch::CountdownLatch #count never reaches below zero Failure/Error: expect(latch.count).to eq(0) expected: 0 got: -1 (compared using ==)
Нам просто нужно проверить, @count
ли @count
нулю:
module CountdownLatch class CountdownLatch def count_down unless @count.zero? @count -= 1 end end end end
Все должно быть зеленым!
В ожидании Темы
До сих пор у нас просто есть прославленный объект обратного отсчета. Это даже не потокобезопасный! Это связано с тем, что count_down
может вызываться несколькими потоками, а @count
в любом случае не синхронизируется, могут возникнуть условия гонки.
Это первый тест, который заставит нас реализовать функции параллелизма в нашей защелке обратного отсчета:
module CountdownLatch describe CountdownLatch do it "#await will wait for a thread to finish its work" do latch = CountdownLatch.new(1) Thread.new do latch.count_down end latch.await expect(latch.count).to eq(0) end end end
Тест создает защелку обратного отсчета, инициализированную в 1
. Затем мы создаем поток, который уменьшает защелку. Вне latch.await
мы называем latch.await
.
По сути, это говорит о том, что программа будет ожидать завершения работы потока. Опять же, как только поток завершит свою работу, он вызовет latch.count_down
. Поэтому мы ожидаем, что count
защелок будет равно нулю.
Чтобы увидеть, что тест не пройден правильно , нам нужна пустая реализация await
:
module CountdownLatch class CountdownLatch def await end end end
При запуске тестов произойдет следующее:
Failures: 1) CountdownLatch::CountdownLatch #await will wait for a thread to finish its work Failure/Error: expect(latch.count).to eq(0) expected: 0 got: 1 (compared using ==)
Поскольку await
ничего не делает, программа не ждет, пока поток сообщит защелке обратного отсчета, что все в порядке, чтобы продолжить. Вместо этого программа просто проходит прямо через. Чтобы пройти этот тест, вам нужно знать об условных переменных .
Переменные условия
Переменная условия по сути является примитивом синхронизации (напомним, что защелка обратного отсчета также является примитивом синхронизации), который позволяет потокам ждать, пока не произойдет какое-либо условие. В случае защелки обратного отсчета это условие, когда @count
достигает нуля.
Когда это происходит, метод broadcast
должен вызываться для переменной условия, чтобы сообщить всем ожидающим потокам, что они могут прекратить ожидание и продолжить.
Во-первых, мы добавим переменную условия в реализацию и скажем, чтобы она передавалась всем ожидающим потокам, когда условие будет выполнено. Не забудьте также включить require "thread"
:
require "thread" # <---- module CountdownLatch class CountdownLatch def initialize(count) if count.is_a?(Fixnum) && count >= 0 @count = count @condition = ConditionVariable.new # <---- else raise ArgumentError end end def count_down unless @count.zero? @count -= 1 else @condition.broadcast # <---- end end end end
Теперь, чтобы разобраться с другой стороной уравнения: заставить потоки приостановить выполнение и дождаться выполнения условия. Вот первая попытка ConditionVariable
имеет метод под названием wait
:
module CountdownLatch class CountdownLatch def await @condition.wait end end end
Однако это не работает, потому что ConditionVariable#wait
требует аргумент, который принимает объект Mutex
. Мьютекс — это, по сути, блокировка, которая защищает часть кода от ввода более чем одним потоком. Напомним, что я упоминал, что @count
до сих пор не является поточно- @count
. Мы собираемся это исправить.
В этом случае нам нужно предоставить мьютекс в условную переменную, чтобы только один поток мог одновременно читать / записывать переменную условия.
Сначала мы создадим мьютекс для защелки обратного отсчета:
module CountdownLatch class CountdownLatch def initialize(count) if count.is_a?(Fixnum) && count >= 0 @count = count @condition = ConditionVariable.new @mutex = Mutex.new <---- else raise ArgumentError end end end end
Далее мы будем использовать @mutex.synchronize
для разграничения критического раздела . Критическая секция — это область кода, в которую может войти только один поток. Во-первых, давайте обработаем метод count_down
:
module CountdownLatch class CountdownLatch def count_down @mutex.synchronize { unless @count.zero? @count -= 1 else @condition.broadcast end } end end end
Наконец, у нас есть мьютекс для передачи в @condition.wait
:
module CountdownLatch class CountdownLatch def await @condition.wait(@mutex) end end end
Однако, как и метод count_down
, вам нужен критический раздел. Вот:
module CountdownLatch class CountdownLatch def await @mutex.synchronize { @condition.wait(@mutex) } end end end
Пока мы добавляем его, давайте @count
в методе count
также мьютексом:
module CountdownLatch class CountdownLatch def count @mutex.synchronize { @count } end end end
Запустите тест снова, и мы должны быть зелеными! Woot!
Пробный прогон
Создайте папку в lib с именем sample . В этой папке я создаю файл с именем chucky.rb со следующим содержимым:
require 'open-uri' require 'json' module CountdownLatch class Chucky URL = 'http://api.icndb.com/jokes/random' def get_fact open(URL) do |f| f.each_line { |line| puts JSON.parse(line)['value']['joke'] } end end def get_facts(num) latch = CountdownLatch.new(num) # <---- Latch facts = [] (1..num).each do |x| Thread.new do facts << get_fact latch.count_down end end latch.await facts end end end
Этот класс извлекает шутки из стороннего API, анализирует ответ JSON и возвращает шутку в виде String
. Мы можем использовать тест, чтобы вести Chucky
и получить некоторые факты:
require 'spec_helper' require 'sample/chucky' # <---- Remember to add this! module CountdownLatch describe CountdownLatch do it "sample run", :speed => 'slow' do chucky = Chucky.new facts = chucky.get_facts(5) expect(facts.size).to eq(5) end end end
Как ожидается, испытания пройдут. Попробуйте снять защелку и посмотреть, что произойдет.
Ограничения, благодарности и где узнать больше
Эта реализация не обрабатывает ложные пробуждения , интересный феномен, когда потоки могут проснуться, даже если переменная условия еще не сигнализировала / не транслировала.
Если вы хотите более надежную и утонченную реализацию защелки обратного отсчета, взгляните на фантастическое хранилище Ruby Concurrency GitHub .
Спасибо за прочтение!
Я надеюсь, что вы узнали что-то новое о условных переменных, мьютексах и, конечно же, создании собственной защелки обратного отсчета! Что еще более важно, я надеюсь, что вы получили массу удовольствия. Вы можете получить полный источник здесь . Спасибо за прочтение!