在进行多线程处理中共享对象,多个线程对同一个对象同时进行修改,有可能出现不一致的状态,使用时要注意。
例子:
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