Giới thiệu gem parallel

Ở bài viết kỳ trước mình đã giới thiệu về process, thread và điểm khác biệt giữa hai khái niệm này. Bài viết này sẽ giới thiệu cơ bản gem parallel, một gem sử dụng library của ruby để chạy bất kì đoạn code nào theo thread hoặc process một cách đơn giản.

Ruby và single thread

Ruby developer đều biết là code MRI chỉ cho phép chạy single thread. Tại sao lại có điều này? MRI có một thành phần gọi là GIL (Global Interpreter Lock). Thành phần này sẽ khóa code bất kì đoạn code ruby đang được thực thi. Điều này có nghĩa là trong trường hợp chạy multi-thread, chỉ có một thread trên một process có thể thực thi ruby code trong một thời điểm.

ruby-threading

Vì thế nếu chúng ta có 8 thread hoạt động trên một máy tính có 8 core thì chỉ có 1 thread và 1 core là được sử dụng tại một thời điểm bất kỳ. GIL tồn tại để bảo vệ ruby khỏi trường hợp tương tranh (race condition) có thể làm hỏng dữ liệu. Đây là điểm mạnh cũng như điểm yếu của ruby.

Bài viết này sẽ không đề cập chi tiết tới GIL. Các bạn có thể đọc thêm về GIL ở loạt bài viết Nobody understands the GIL của Jesse Storimer.

Gem parallel

Ruby có 2 thư viện cơ bản để thực hiện multi-threading và multi-processing là Thread và Process. Tuy nhiên có rất nhiều vấn đề khi sử dụng 2 thư viện này mà developer cần phải giải quyết như race condition. Có một vài thư viện mở có cách sử dụng khá đơn giản như celluloid, concurrent-ruby hay parallel. Tuy nhiên khi muốn thực thi những tác vụ nhỏ như đọc và ghi dữ liệu thì gem parallel lại được ưa chuộng do tính đơn giản và “light-weight” của nó.

Gem parallel có hai chức năng cơ bản là phân chia tác vụ theo thread và theo process.

# 2 CPUs -> work in 2 processes (a,b + c)
results = Parallel.map(['a','b','c']) do |one_letter|
  expensive_calculation(one_letter)
end

# 3 Processes -> finished after 1 run
results = Parallel.map(['a','b','c'], in_processes: 3) { |one_letter| ... }

# 3 Threads -> finished after 1 run
results = Parallel.map(['a','b','c'], in_threads: 3) { |one_letter| ... }

Tương tự có thể sử dụng với each

Parallel.each(['a','b','c']) { |one_letter| ... }

ActiveRecord

Các bạn có thể áp dụng parallel cho ActiveRecord

# reproducibly fixes things (spec/cases/map_with_ar.rb)
Parallel.each(User.all, in_processes: 8) do |user|
  user.update_attribute(:some_attribute, some_value)
end
User.connection.reconnect!

# maybe helps: explicitly use connection pool
Parallel.each(User.all, in_threads: 8) do |user|
  ActiveRecord::Base.connection_pool.with_connection do
    user.update_attribute(:some_attribute, some_value)
  end
end

# maybe helps: reconnect once inside every fork
Parallel.each(User.all, in_processes: 8) do |user|
  @reconnected ||= User.connection.reconnect! || true
  user.update_attribute(:some_attribute, some_value)
end

Break

Sử dụng để dùng những tác vụ đã được thực hiện

Parallel.map(User.all) do |user|
  raise Parallel::Break # -> stops after all current items are finished
end

Kill

Chỉ nên sử dụng cách này nếu sub-process an toàn để kill process ở bất kỳ thời điểm nào

Parallel.map([1,2,3]) do |x|
  raise Parallel::Kill if x == 1# -> stop all sub-processes, killing them instantly
  sleep 100
end

Worker number

Sử dụng Parallel.worker_number để tìm ra vị trí của worker mà tác vụ đang được thực thi.

Parallel.each(1..5, :in_processes => 2) do |i|
  puts "Item: #{i}, Worker: #{Parallel.worker_number}"
end

# Item: 1, Worker: 1
# Item: 2, Worker: 0
# Item: 3, Worker: 1
# Item: 4, Worker: 0
# Item: 5, Worker: 1

Trên đây là giới thiệu sơ bộ về gem parallel. Source code của gem khá đơn giản, các bạn có thể đọc thêm tại lib/parallel.rblib/parallel/processor_count.rb.

Một vài tài liệu về process và thread trong ruby: