Rails Rails 多线程的坑,请问怎么破?

happyming9527 · August 25, 2016 · Last by happyming9527 replied at August 28, 2016 · 6133 hits

写了段多线程的同步数据的 rake 任务。但是启动的时候,不定时的会报错,之前写单线程的时候是没啥问题的,改成多线程就出问题了。

代码如下:

task :move_user  => [:environment] do
  TempSettings::MOVE_KIND = 'batch'

  tread_num = 10
  last_id = User.with_phone.last.id
  interval = (last_id - 10000)/tread_num
  (1..tread_num).to_a.map do |i|
    Thread.new do
      puts "开始thread #{i} 同步用户------------------"
      relation = User.with_phone.where(['id > ? and id <= ?', 10000 + (i-1)*interval, 10000+i*interval])
      relation.find_in_batches do |group|
        group.each do |user|
          user.async_self
        end
      end
    end
  end.map(&:join)

  (1..tread_num).to_a.map do |i|
    Thread.new do
      puts "开始thread #{i} 同步用户其他信息------------------"
      relation = User.with_phone.where(['id > ? and id <= ?', 10000 + (i-1)*interval, 10000+i*interval])
      relation.find_in_batches do |group|
        group.each do |user|
          next if user.dm_user.is_success?
          user.async_other
        end

      end
    end
  end.map(&:join)

end

报错如下:

Circular dependency detected while autoloading constant UserKid

RAILS_ENV=production 这样的参数也设置过,甚至还加过 config.allow_concurrency = false 这样的设置,但是还是时好时坏,请问怎么破?

代码写的不对,thread 里面使用主线程的变量 i 不线程安全。

把你的 UserKid 文件头部和所在目录结构贴一下,还有 user.async_self 都干了啥

报错很明显 不像是线程安全的锅,或者 autoload 就是线程不安全

@nowherekai thread 里面使用主线程的变量,但是 i 是局部变量啊!会有啥问题?

@nouse 我是 RAILS_ENV=production 模式跑的,为啥常量的加载没有在启动前就进行完毕,而是运行一段时间后才报错?

@yanhao 我用的是 rails5,创建线程加时间间隔有什么作用么?

这个应该是只有在 autoload(线程不安全)的时候才会报的错误,按理说在 production 下应该是不会报错。一般为了避免这种错误,我都是在线程创建的代码之前先手动触发加载,虽然丑陋,但是好用:

UserKid    # do nothing, just for trigger loading
Thread.new do
  UserKid.do_something()
end

Thread.new do
  UserKid.do_something_else()
end

有可能你 Thread.new 创建线程 t1 的时候,i=1, 然后线程 t1 并没有立即执行,而是主线程执行了,这时候 i 变成了 2,这时候你的 t1 开始执行,i 的值就不对了。你遇到的问题并不是上述情况引起的,我是说这样写有风险。 要这样写Thread.new(i) { |number| ...}

@martin91虽然我现在也不明白,为啥 RAILS_ENV=production 起不了作用,但是按你方法解决了,.

@nowherekai 这个 i 是局部变量,在主线程内,一个个的循环内,i 都是不同的变量,其他线程的 i 变量难道会改变?

我搞错了,可能没问题,不太确定。

#12 楼 @nowherekai 不会有问题,前面有 join,主线程会等待前面线程都执行完才接着执行,如果没有 join,你说的就是对的了

@martin91 这段代码,子线程访问的都是主线程循环内的局部变量,如果不加 join,会有问题么?

#14 楼 @happyming9527 不会,两个 i 是各自对应的 block 的局部变量,代码块也有自己的作用域,线程里引用的变量 i 是受 .each 的代码块的作用域保护的,与主线程中另一个代码块中的 i 不是同一个变量了。

@happyming9527 @nowherekai 为了直观理解,我写了个示例代码,可以看下:

# test file
array = [10]
threads = []
i = 1000          # 主线程中变量
former_i = []
latter_i = []

array.each do |i|     # block 变量
  threads << Thread.new do
    10.times { former_i << (i += 1) }
  end
end

array.each do |i|     # block 变量
  threads << Thread.new do
    10.times { latter_i << (i -= 1) }
  end
end

threads.each(&:join)                   # 在继续执行前等待其他线程执行完毕
puts "former_i: #{former_i}"
puts "latter_i: #{latter_i}"
puts "i in main thread: #{i}"

运行结果是:

据此,可以得出:

  1. ruby block 有自己的作用域,比如两个 .each 方法调用中的 block 中的变量 i 都没有污染主线程中的变量;
  2. 创建线程时,线程共享创建线程的代码的作用域以及内存空间等,所以线程里的 i 引用的是各自上下文代码块里的 i 变量,并非之前理解的主线程变量;
  3. 因为有作用域的保护,主线程中的 i 在整个运行过程中未被修改,仍然是 1000
  4. 代码规范上尽量避免有歧义的代码,尽管我们明确知道了 i 变量是有作用域保护,互无关系。

@martin91 谢谢你的解答

You need to Sign in before reply, if you don't have an account, please Sign up first.