Ruby synchronisation: How to make threads work one after another in proper order?

坚强是说给别人听的谎言 提交于 2019-12-04 11:58:28

Using Queue as a PV Semaphore

You can abuse Queue, using it like a traditional PV Semaphore. To do this, you create an instance of Queue:

require 'thread'
...
sem = Queue.new

When a thread needs to wait, it calls Queue#deq:

# waiting thread
sem.deq

When some other thread wants to unblock the waiting thread, it pushes something (anything) onto the queue:

# another thread that wants to unblock the waiting thread
sem.enq :go

A Worker class

Here's a worker class that uses Queue to synchronize its start and stop:

class Worker

  def initialize(worker_number)  
    @start = Queue.new
    Thread.new do
      @start.deq
      puts "Thread #{worker_number}"
      @when_done.call
    end
  end

  def start
    @start.enq :start
  end

  def when_done(&block)
    @when_done = block
  end

end

When constructed, a worker creates a thread, but that thread then waits on the @start queue. Not until #start is called will the thread unblock.

When done, the thread will execute the block that was called to #when_done. We'll see how this is used in just a moment.

Creating workers

First, let's make sure that if any threads raise an exception, we get to find out about it:

Thread.abort_on_exception = true

We'll need six workers:

workers = (1..6).map { |i| Worker.new(i) }

Telling each worker what to do when it's done

Here's where #when_done comes into play:

workers.each_cons(2) do |w1, w2|
  w1.when_done { w2.start }
end

This takes each pair of workers in turn. Each worker except the last is told, that when it finishes, it should start the worker after it. That just leaves the last worker. When it finishes, we want it to notify this thread:

all_done = Queue.new
workers.last.when_done { all_done.enq :done }

Let's Go!

Now all that remains is to start the first thread:

workers.first.start

and wait for the last thread to finish:

all_done.deq

The output:

Thread 1
Thread 2
Thread 3
Thread 4
Thread 5
Thread 6

If you're just getting started with threads, you might want to try something simple. Let the 1st thread sleep for 1 second, the 2nd for 2 seconds, the 3rd for 3 seconds and so on:

$stdout.sync = true

threads = []
(1..6).each do |i|
  threads << Thread.new {
    sleep i
    puts "Hi from thread #{i}"
  }
end

threads.each(&:join)

Output (takes 6 seconds because the threads run in parallel):

Hi from thread 1
Hi from thread 2
Hi from thread 3
Hi from thread 4
Hi from thread 5
Hi from thread 6

You can assign each a number, which will denote its place in the queue, and check it to see whose turn it is:

class QueuedWorker

  def initialize(mutex, condition_variable, my_turn)
    @mutex = mutex
    @my_turn = my_turn
    @condition_variable = condition_variable
  end

  def self.turn
    @turn ||= 0
  end

  def self.done
    @turn = turn + 1
  end

  def run
    loop do
      @mutex.synchronize do
        if QueuedWorker.turn == @my_turn
          # do actual work
          QueuedWorker.done
          @condition_variable.signal
          return
        end
        @condition_variable.signal
        @condition_variable.wait(@mutex)
      end
    end
  end
end

mutex = Mutex.new
cv = ConditionVariable.new

(0..10).each do |i|
  Thread.new do
    QueueWorker.new(mutex, cv, i).run
  end
end

That being said, the implementation is awkward, since threading are specifically not built for serial work. If you need something to work serially, do it in a single thread.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!