Ruby on Rails 4.0 的 rc 版本已经 release 了,Rails 4 时代最大的变化当属默认开启多线程模式。Rails 4 的多线程将给大家带来什么好处呢?至少有两方面:
更高的系统吞吐量 Web 站点多是 IO 密集型的,多线程可以让 Application 在等待 IO 操作的同时,还能接收处理请求,大大提升系统吞吐量,增强系统稳定性和安全性。
更省内存 Ruby on Rails 是内存消耗大户,一个 Applicaion 占用几百兆是常事,以前使用仅使用多进程的并发模式时,整体内存消耗巨大,使用多进程+多线程的并发模式,不单系统吞吐量大大提供,系统整体使用内存也大幅下降。
但是天下没有免费的午餐,在享用这些好处的同时,我们也必须付出一定的代价,代价就是要应付多线程编程带来的复杂性。程序中需要处理多线程可能导致问题的地方,如果程序中出现问题也变得更加难以发现调试。
好在需要注意的地方也不是太多,下面把这几个需要注意的地方一一说明。
代码加载
Ruby 的 require 并非线程安全的,在多线程中 require,可能会导致严重的不可预期的错误。例如下面的一个演示程序在 Ruby 1.9 环境下执行会发生死锁。
a.rb 文件:
1
2
3
4
puts "#{Thread.current}: a.rb"
sleep 1
puts "#{Thread.current}: requiring b"
require 'b'
b.rb 文件:
1
2
3
puts "#{Thread.current}: b.rb"
puts "#{Thread.current}: requiring a"
require 'a'
test.rb 文件:
1
2
3
4
5
6
$LOAD_PATH << "."
t1 = Thread.new { require 'a' }
t2 = Thread.new { require 'b' }
t1.join
t2.join
在 Ruby 1.9 环境下运行 test.rb 将报死锁错误:
1
2
3
4
5
6
#<Thread:0x007fd2da90a268>: b.rb
#<Thread:0x007fd2da90a268>: requiring a
#<Thread:0x007fd2da90a2e0>: a.rb
#<Thread:0x007fd2da90a2e0>: requiring b
test.rb:5:in
</span>join<span class="s1">': deadlock detected (fatal)</span>
</span><span class='line'><span class="s1"> from test.rb:5:in
<main>'
因为 Ruby require 不是线程安全的,所以 Rails 中使用多线程环境时,需要对 require 做一定的限制,简单的说就是在 Application 启动的时候,把所有需要加载的代码全部加载完成,避免启动后还 require。Rails 4 的生产环境配置中该选项已经默认生效。需要注意的时,如果你的代码不在 Rails 默认的几个目录中,你需要手动配置你的目录进入 eager_load_path,例如:
1
config.eager_load_paths << "#{Rails.root}/lib"
全局变量和类变量写操作
在 Rails 多线程环境,所有的全局变量(包括 $var @@var 和 类实例变量),在实例方法中都应该是只读的,尽量应该避免写操作。
下面是一个在实例方法中写类变量导致问题的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class HomeController < ApplicationController
before_filter :set_site
def index
end
private
def set_site
@site = Site.find_by_subdomain(request.subdomains.first)
if @site.layout?
self.class.layout(@site.layout_name)
else
self.class.layout('default_lay')
end
end
end
上面代码的意图是根据域名设置不同的 layout。self.class.layout(value)
中,Rails 把 value 保存在类变量 @@layout_
,然后在 render 的时候使用。
设想这样一种情况 UserA 的 subdomain 是 foo1,他的 layout 应该是 foo1, UserB 的 subdomain 是 foo2,他的 layout 应该是 foo2。
UserA 和 UserB 同时请求应用,他们的请求分别在 Thread1 和 Thread2 中执行,执行顺序可能是:
- Thread1, 执行进入 set_site 方法,设置
@@layout_
为 foo1; - Thread2, 执行进入 set_site 方法,设置
@@layout_
为 foo2; - Thread1, render response,使用最新的
@@layout_
foo2 render; - Thread2,render response,使用最新的
@@layout_
foo2 render;
我们期望 Thread1 使用 foo1 layout render,这样的执行结果和期望的不相符。
线程安全的写法是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class HomeController < ApplicationController
before_filter :set_site
layout :site_layout
def index
end
private
def set_site
@site = Site.find_by_subdomain(request.subdomains.first)
end
def site_layout
if @site.layout?
@site.layout_name
else
'default_lay'
end
end
end
程序在每次需要使用 layout 时,调用实例方法 site_layout
,避免写类变量。
IO Connection
Rails 应用通常会用到多个 IO Connection,比如 ActiveRecord 的数据库 Connection,缓存 Memcached 的 Connection,Redis 的 Connection 等等。这些 IO Connection 在 Rails 多线程环境下并不都是线程安全的。
ActiveRecord 的 Connection 是线程安全的,而且 ActiveRecord 还可配置 Connection Pool,这样可以更高效率的利用数据库连接。
Memcached 的 Connection memchached-client 并不是线程安全的,最新的 dalli 是线程安全的。不过 dalli 的线程安全机制是在每个读写操作时加上互斥信号控制,这意味着同一时间只有一个线程可以操作,如果操作非常频繁的话,可能有性能上的问题,这个时候可以使用一个单独的 Connection Pool Gem 解决。
这个 Connection Pool Gem 的地址是 https://github.com/mperham/connection_pool。
Redis 的 Connection 和 dalli 类似,本身通过加上互斥信号控制保证线程安全,可以通过 Connection Pool 增强效率。
使用互斥信号控制非线程安全操作
在程序中,如果存在某些不希望多个线程同时执行的操作,可以使用互斥信号控制其执行,这样当已经有一个线程进入执行时,其他进入的 thread 都会被 block 住,等前面的进程执行结束后才会进入执行,从而保证在一个时间只有一个线程会执行。
示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class HomeController < ApplicationController
@@lock = Mutex.new
def index
@@lock.synchronize do
thread_unsafe_code
end
end
private
def thread_unsafe_code
if @@something == 'hello'
do_hello
elsif @@something == 'world'
do_world
else
@@something = 'nothing'
end
end
end
总之,Rails 的多线程为我们提供了简便的提升系统伸缩性的能力,这也意味的程序复杂性的增加,有几处地方使我们需要注意的,只有这样才能很好的利用 Rails 多线程能力。
参考: