0

Giới Thiệu Về Celluloid - Part 2

celluloid_logo


Đây là phần 2 trong series về Celluloid, nếu các bạn chưa xem phần 1, hãy xem ở đây nhé:

Giới thiệu về Celluloid - Part 1

Celluloid có rất nhiều những công cụ hữu ích giúp việc lập trình đồng thời (concurrent programming) trở nên dễ dàng hơn bao giờ hết (honho)

Futures

Rất nhiều trường hợp chúng ta không muốn bỏ đi giá trị trả lại của một method vừa call ở một actor, mà muốn giữ lại để sử dụng tại một nơi khác. Để đáp ứng cho yêu cầu này, Celluloid cung cấp futures.

Thử viết một đoạn script để tính toán giá trị SHA1 checksum của một mảng các files rồi output ra console.

require 'celluloid/current'
require 'digest/sha1'

class SHAPutter
  include Celluloid

  def initialize(filename)
    @filename = filename
  end

  def output(checksum_future)
    puts "#{@filename} - #{checksum_future.value}"
  end

  def checksum
    @file_contents = File.read(@filename)
    Digest::SHA1.hexdigest @file_contents
  end

end

files = ["test1.txt", "test2.txt", "test3.txt", "test4.txt", "test5.txt"]

files.each do |file|
  sha = SHAPutter.new file
  checksum_future = sha.future :checksum
  sha.output checksum_future
end

Đầu tiên, hãy nhìn qua method checksum. Rất đơn giản, chỉ là sử dụng Digest::SHA1 để tính toán checksum cho nội dung file mà actor nhận.

Điều thú vị nằm ở trong vòng lặp (honho)

Sau khi chỉ định file cho actor, thay vì chỉ gọi checksum một cách đơn thuần, chúng ta sử dụng future. Bằng cách này, ngay lập tức một object Celluloid::Future được return. Tiếp đó, future object được chuyển cho method output bên trong actor. output cần giá trị checksum, nên nó sẽ lấy từ giá trị của method trong future object.

Có thể các bạn sẽ đặt câu hỏi "ơ ví dụ này chả khác j ví dụ cuối của Part 1 cả?" tuy nhiên nếu nhìn kỹ, bạn sẽ thấy ở ví dụ trước, để không đồng bộ các xử lý, chúng ta đã phải gộp hết tất cả vào một method duy nhất. Còn với futures, chúng ta có thể tách các đoạn code ra thoải mái (dance2)

Ngoài ra, có những trường hợp bắt buộc phải dùng futures. Ví dụ nếu bạn muốn viết một thư viện, thì giá trị của hàm checksum phải là future kể từ khi người sử dụng add vào source code của họ.

Tạo các block chạy đồng thời

Với futures, chúng ta có thể đẩy các code block sang thread khác ngon lành (honho)

require 'celluloid'

def some_method(future)
  #do something crazy
  val = future.value
  #do something with val
end

future = Celluloid::Future.new do
  #incredibly complex computation
end

some_method(future)

Sử dụng Celluloid::Future để đẩy block đó sang thread riêng , Celluloid sẽ quản lý mọi thứ liên quan đến thread đó, để chúng ta có thể sử dụng giá trị return vào sau này. Chức năng này của Celluloid có thể đưa vào bất cứ một chương trình nào, và nếu bạn có thể thành thạo được thì sẽ vô cùng hữu ích đấy (yeah)

Catching errors với Supervisors

Trong một ngày đẹp trời thì các threads chạy mượt mà không lỗi, thế nhưng hôm mưa nào đó tự dưng có lỗi xảy ra thì sao? Chúng ta phải làm gì để có thể kiểm soát được tình huống đó? (huhuhu)

Celluloid cung cấp cho chúng ta một giải pháp được gọi là supervisor.

Thử áp dụng với ví dụ ở đầu bài viết:

class SHAPutter
  include Celluloid

  def initialize(filename)
    @filename = filename
  end

  def output(checksum_future)
    puts "#{@filename} - #{checksum_future.value}"
  end

  def checksum
    @file_contents = File.read(@filename)
    Digest::SHA1.hexdigest @file_contents
  end

end

files = ["test1.txt", "test2.txt", "test3.txt", "test4.txt", "test5.txt"]

files.each do |file|
  supervisor = SHAPutter.supervise_as :sp, file
  sha = Celluloid::Actor[:sp]
  checksum_future = sha.future :checksum
  sha.output checksum_future
end

Class SHAPutter không thay đổi gì cả, như vậy là những đoạn code về business logic sẽ không phải chỉnh sửa gì (thatlatuyetvoi)

Giờ đến method supervise_as được gọi trong vòng lặp. Method này sẽ thực hiện 3 điều. Thứ nhất, nó tạo một actor là instance của SHAPutter. Thứ hai, nó trả về một supervisor object. Thứ ba, nó lấy tham số đầu tiên (ở đây là :sp) và đặt tham số đó vào registry.

Celluloid registry giống như cuốn danh bạ điện thoại vậy - bạn có thể sử dụng actors trong đó dựa theo tên. Do đó, ở dòng code tiếp theo, chúng ta sử dụng :sp để tìm actor đó trong registry.

Vậy là chỉ với 2 dòng code thêm vào, Celluloid đã tự động giúp chúng ta trong việc restart và track theo các actors khi chúng crash. Khi có một actors bị exception, actor đó sẽ ngay lập tức đc restart bởi Celluloid core.

Communication giữa các Actors

Trong hầu hết các chương trình, actors không làm việc trong một môi trường cách ly mà sẽ phải giao tiếp với các actors khác.

Ví dụ một cách đơn giản, chúng ta sẽ thử dùng 3 actors để in ra dòng chữ I am superman:

require 'celluloid/current'

class FirstActor
  include Celluloid
  def say_msg
    print "I "
    Celluloid::Actor[:second].say_msg
  end
end

class SecondActor
  include Celluloid
  def say_msg
    print "am "
    Celluloid::Actor[:third].say_msg
  end
end

class ThirdActor
  include Celluloid
  def say_msg
    print "superman.\n"
  end
end

Celluloid::Actor[:second] = SecondActor.new
Celluloid::Actor[:third] = ThirdActor.new
FirstActor.new.say_msg

FirstActor sử dụng registry để tìm instance của SecondActor và gọi say_msg của đối tượng vừa tìm được, sau đó SecondActor lại làm tương tự với ThirdActor.

Tóm lại, giao tiếp giữa các actors được thực hiện qua actor Registry, nơi chúng ta có thể đặt tên cho các actors.

Ngoài ra, còn có một phương pháp nữa giúp các actors có thể làm việc cùng nhau. Đó là sử dụng futures để truyền giá trị trả về giữa các actors.

Blocking call trong actors

Với Celluloid, các actors được đặt trong các threads của mình, do đó việc thực hiện gọi các methods gây block sẽ không gặp vấn đề gì, nó chỉ block một actor đó thôi.

Tuy nhiên phải cẩn thận với việc gọi những blocks vô hạn trong actors, điều đó sẽ khiến cho các actors đó không thể tiếp tục nhận được các messages khác.

Pooling

Pools của Celluloid thực sự là tuyệt vời (tanghoa)

Chúng ta sẽ xem thử một ví dụ về sử dụng Pools của Celluloid:

require 'celluloid/current'
require 'mathn'

class PrimeWorker
  include Celluloid

  def prime(number)
    if number.prime?
      puts number
    end
  end
end

pool = PrimeWorker.pool

(2..1000).to_a.map do |i|
  pool.async.prime i
end

sleep 100

Method prime trong PrimeWorker sẽ in ra nếu số đó là số nguyên tố.

Điều thú vị nằm ở dòng code sử dụng pool của PrimeWorker. Object pool mang đủ các methods của PrimeWorker, số lượng instances của PrimeWorker được tạo ra ngang số nhân của CPU. Nếu bạn sử dụng một quad core CPU, sẽ có 4 actors được tạo ra. Celluloid sẽ quyết định actor nào rời khỏi pool để xử lý.

Wow, vậy là chỉ cần 4-5 dòng code, bạn đã có thể tạo ra các xử lý đồng thời (concurrent) để chia sẻ khối lượng công việc cho processor của bạn. (thatlatuyetvoi)

Cuối cùng là việc call hàm sleep ở cuối chương trình. Do hàm prime được gọi không đồng bộ (asynchronously) nên nếu thread chính kết thúc trước khi các actors kịp xử lý thì số nguyên tố của actors đó sẽ không thể được in ra màn hình. Câu lệnh sleep chính là để thread chính có thể tồn tại đủ lâu cho đến khi tất cả các actors thực hiện xong xử lý của mình. (yeah)

Mọi thứ sẽ còn tiếp diễn ở phần 3 (hihi)


Source: An Introduction to Celluloid, Part II


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí