ruby多线程共享对象之线程安全

在进行多线程处理中共享对象,多个线程对同一个对象同时进行修改,有可能出现不一致的状态,使用时要注意。

例子:

test.rb

x = 0

10.times.map do |i|
  Thread.new do
    puts "before (#{ i }): #{ x }"
    x += 1
    puts "after (#{ i }): #{ x }"
  end
end.each(&:join)

puts "\ntotal: #{ x }"
执行 ruby test.rb

输出:

before (1): 0
after (1): 1
before (0): 1
after (0): 2
before (2): 2
after (2): 3
before (4): 3
after (4): 4
before (6): 4before (5): 3before (9): 3
before (8): 3before (7): 3
after (7): 6
before (3): 3

after (3): 7
after (9): 5

after (8): 8

after (6): 9after (5): 10


total: 10

你可能注意到了,在第四次循环,i = 3,输出:

before (3): 3 #x等于3
after (3): 7 #x等于7

Mutex是一个类,它实现了一个简单的一些共享资源的互斥访问的信号锁定。也就是说,只有一个线程可以持有锁在一个给定的时间。其他线程可能会选择等待锁线,变为可用,或者可能干脆选择错误,指示立即得到锁不能使用。
通过将所有访问一个互斥的控制下共享数据,我们可以确保一致性和原子操作,将代码放到Mutex对象的synchorize方法中。

修改test.rb文件内容如下:

x = 0
mutex = Mutex.new

10.times.map do |i|
  Thread.new do
    mutex.synchronize do
      puts "before (#{ i }): #{ x }"
      x += 1
      puts "after (#{ i }): #{ x }"
    end
  end
end.each(&:join)

puts "\ntotal: #{ x }"

执行 ruby test.rb

输出:

before (0): 0
after (0): 1
before (9): 1
after (9): 2
before (2): 2
after (2): 3
before (3): 3
after (3): 4
before (4): 4
after (4): 5
before (5): 5
after (5): 6
before (6): 6
after (6): 7
before (7): 7
after (7): 8
before (8): 8
after (8): 9
before (1): 9
after (1): 10

total: 10
下面是个计数器的例子:

app.rb文件内容如下:

class Counter
  attr_reader :total
  
  def initialize
    puts 'initialized...'
    @total = 0
    @mutex = Mutex.new
  end
  
  def increment!
    @mutex.synchronize { @total += 1 }
  end
end

class Application
  def counter
    @counter ||= Counter.new
  end
  
  def increment!
    counter.increment!
  end
  
  def total
    counter.total
  end
end

app = Application.new
10.times.map do |i|
  Thread.new do
    app.increment!
  end
end.each(&:join)
puts app.total
执行 ruby app.rb有时候会出现这种结果:
initialized...
initialized...
initialized...
initialized...
initialized...
initialized...
initialized...
initialized...initialized...

1
这种结果是错误的,Counter使用了安全线程,但是最终的应用却没有,因为我们使用了 ||=,它不是原子的。Application的两个实例看到的@counter都是nil,于是都是实例化Counter,所以结果就是错的。

正确的是将Application修改成这样:

class Application
  def initialize
    @counter = Counter.new
  end
  
  def counter
    @counter
  end
  
  def increment!
    counter.increment!
  end
  
  def total
    counter.total
  end
end
这样在Application实例化的时候就将Counter实例化了。

再次执行,结果如下:

initialized...
10

处理死锁:

当我们开始使用互斥对象的线程排斥,我们必须小心地避免死锁。死锁条件时所发生的所有线程正在等待获取另一个线程持有的资源。因为所有的线程被阻塞,他们不能释放持有的锁。因为他们不能释放的锁,没有其他线程可以获取这些锁。
条件变量是一个简单的相关资源和用于特定互斥锁的保护范围内的信号。当你需要的资源是不可用的,你等待一个条件变量。这一操作释放相应的互斥锁。当其他一些线程信号的资源是可用的,原来的线程来等待,同时恢复对临界区锁。

ConditionVariable类实现了支持线程同步的状态变量的功能。ConditionVariable对象就是把线程的等待条件具体化的产物。

mutex = Mutex.new
cv = ConditionVariable.new

Thread.start {
    mutex.synchronize {
      ...
      while (尚未满足条件时)
        cv.wait(m)
      end
      ...
    }
}
如上所示,若某线程尚未满足条件,就调用wait方法将其挂起,并让其他线程执行
Thread.start {
    mutex.synchronize {
      # 进行某些操作使上述条件得到满足
      cv.signal
    }
}

然后调用signal方法通知正在wait的线程上述条件已成立。这是比较典型的用法。

ConditionVariable使用的例子:

test.rb 文件内容为:

require 'thread'
mutex = Mutex.new
cv = ConditionVariable.new
resource = nil

a = Thread.new {
  mutex.synchronize {
    # Thread 'a' now needs the resource
    cv.wait(mutex) if resource.nil?
    # 'a' can now have the resource
    puts "获得resource,继续往下执行..."
    puts "resouce的值为: #{resource.inspect}"
  }
}


b = Thread.new {
  mutex.synchronize {
    # Thread 'b' has finished using the resource
    puts "正在分配resource..."
    resource = "45"
    cv.signal
  }
}

[a, b].each &:join

执行 ruby test.rb

输出:

正在分配resource...
获得resource,继续往下执行...
resouce的值为: "45"

参考:http://lucaguidi.com/2014/03/27/thread-safety-with-ruby.html



你可能感兴趣的:(线程安全,ruby多线程)