程序人生

写优雅的程序,做优雅的人

Ruby on Rails 线程安全代码

| Comments

Ruby on Rails 4.0 的 rc 版本已经 release 了,Rails 4 时代最大的变化当属默认开启多线程模式。Rails 4 的多线程将给大家带来什么好处呢?至少有两方面:

  1. 更高的系统吞吐量 Web 站点多是 IO 密集型的,多线程可以让 Application 在等待 IO 操作的同时,还能接收处理请求,大大提升系统吞吐量,增强系统稳定性和安全性。

  2. 更省内存 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">&#39;: 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 中执行,执行顺序可能是:

  1. Thread1, 执行进入 set_site 方法,设置 @@layout_ 为 foo1;
  2. Thread2, 执行进入 set_site 方法,设置 @@layout_ 为 foo2;
  3. Thread1, render response,使用最新的 @@layout_ foo2 render;
  4. 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 多线程能力。

参考:

Comments