0

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

celluloid_logo


Ở thời điểm vài năm trước đây, có một phương pháp rất đơn giản để cải thiện code. Nếu bạn cảm thấy những dòng code cần xử lý nặng của mình chạy chậm hơn những gì mình mong muốn, cách đơn giản nhất là chỉ việc ngồi đợi đến phiên bản nâng cấp phần cứng tiếp theo, tốc độ CPU sẽ được cải thiện và chương trình của bạn đột nhiên chạy nhanh hơn (yaoming) Tuy nhiên, tất cả chỉ còn là quá khứ khi phần cứng đi dần đến giới hạn. Lúc này sẽ không thể tăng tốc độ của CPU được nữa nếu vẫn muốn duy trì một mức giá hợp lý (khoc2)

Một phương pháp thông minh hơn đã ra đời để giải quyết vấn đề trên. Thay vì cố gắng ép một processor chạy ở tốc độ cao, chúng ta sẽ chia sẻ công việc sang cho nhiều processors gánh vác. Quả là một ý tưởng tuyệt vời (honho)

Đối với những người viết phần mềm hiện nay, việc hiểu và sử dụng tốt xử lý đồng thời (concurrency) là một yếu tố rất quan trọng. Để có thể cải thiện hiệu suất cùng với phần cứng, bạn PHẢI sử dụng concurrency. Kỹ thuật phổ biến nhất cho concurrency hiện tại có lẽ là sử dụng threads, khá là phức tạp và một lỗi nhỏ cũng có thể khiến bạn phải trả giá đắt. Nếu bạn thực sự có hứng thú với vấn đề này, hãy tham khảo ở một số link sau:

(dead) Locking

Thử tưởng tượng có 2 người có một bản copy của một cuốn sách mà cả 2 cùng muốn đọc. Giả sử cả 2 không thể đồng thời đọc cùng cuốn sách tại cùng thời điểm, vì thế mà một người sẽ phải đợi cho đến khi người kia đọc xong. Bây giờ, nếu người này đều nghĩ là người kia chưa đọc xong nên quyết định mình chưa bắt đầu đọc vội, thì rốt cuộc là chả có ai đọc sách cả.

Tình huống trên được gọi là deadlock. (huhuhu)

Nếu chương trình của bạn có 2 threads cùng ghi vào một file. Vì cả 2 ko thể đồng thời ghi vào file cùng một thời điểm, nên file bị locked cho đến khi một thread tiến hành ghi. Đương nhiên, tình huống này vẫn bình thường, nhưng vấn đề sẽ phát sinh khi xuất hiện một vài lỗi nào đó khiến file bị locked đối với cả 2 threads. Nếu bạn đã từng làm việc với threads, bạn sẽ thấy hiện tượng này xảy ra rất nhiều. Thực sự là rất phiền phức khi phải ngồi dò và fix lỗi này(khoc)

Race conditions

Trong trường hợp deadlocking, 2 threads đợi lẫn nhau mãi mãi. Tuy nhiên, deadlocking còn có một người bà con cũng tệ không kém, được gọi là race condition.

Race condition xảy ra khi 2 threads cố gắng truy cập và ghi vào một biến tại cùng một thời điểm. Điều này giống như 2 threads đang chạy đua xem ai ghi trước/sau vào biến đó vậy (honho)

Solution?

Cả 2 tình huống nêu trên chỉ đơn giản là đỉnh của tảng băng trôi mà thôi, sẽ còn rất nhiều các vấn đề khác mà bạn phải đối mặt khi sử dụng threads.

Từ những năm 1980, các chuyên gia đã bắt đầu tìm hiểu về nguồn gốc vấn đề và cách giải quyết. Họ đã nhận ra rằng hầu hết các vấn đề đều xuất phát từ việc chia sẻ state (của biến, files, v.vv..) và locking. Nếu bạn quên lock một tài nguyên dùng chung, nó sẽ giống như một mối hiểm hoạ đang trôi nổi đợi chờ bạn vậy (kill)

Có rất nhiều giải pháp đã được đưa ra, một trong số đó là evented I/O như EventMachine đối với các Rubyists.

Bên cạnh đó, một vài chuyên gia đã phát triển một model mới dựa trên các ý tưởng của vật lý lượng tử. Thay vì chia sẻ state, hệ thống sẽ được xoay quanh việc truyền messages. Họ gọi đó là actor model.

Actor model

Trong actor model, mọi object đều là một actor. Mỗi actor có thể gửi và nhận messages từ các actors khác, và cũng có thể khởi tạo các actors khác (nếu cần). Điểm mấu chốt ở đây là các communications không đồng bộ và không cần chia sẻ state giữa các actors. Điều đó có nghĩa là các messages có thể đang được gửi trong khi những cái khác đang được nhận. Khi một actor gửi một message thì không cần phải đợi response. State được chia sẻ giữa các processes dược thực hiện hoàn toàn thông qua việc sử dụng messages.

Một thư viện tên là Celluloid đã được sinh ra theo ý tưởng này.


Celluloid_logo


Celluloid mang actor model đến Ruby, khiến việc viết các ứng dụng xử lý đồng thời trở nên dễ dàng hơn rất nhiều (yeah)

Đầu tiên, Celluloid được tích hợp sẵn việc chống deadlock, bởi vì tất cả các messages giữa các actors được xử lý theo cách mà gần như không thể phát sinh deadlock, trừ khi bạn cố thực hiện điều gì đó "thần thánh" với native code (vd: C)

Nếu bạn biết tới Erlang thì bạn sẽ sớm cảm thấy quen thuộc vì Celluloid mượn một trong những ý tưởng quan trọng nhất của Erlang: dung sai lỗi (fault tolerance). Celluloid tự động restart và xử lý các actors bị crashed, do đó bạn không phải lo lắng về việc đến cuối cùng thì kết quả lại bị sai.

Còn có rất nhiều chức năng khác (linking, futures v.vv..) giúp việc sử dụng threads trở nên dễ thở hơn (honho)

Let's start

Trước hết, chúng ta sẽ bắt đầu với việc viết một actor nhỏ có thể đọc một file được chỉ định và hiển thị nội dung file khi ta mong muốn.

require 'celluloid/current'

class FilePutter
  include Celluloid

  def initialize(filename)
    @filename = filename
  end

  def load_file
    @file_contents = File.read @filename
  end

  def print
    p @file_contents
  end

end

fp = FilePutter.new "test1.txt"
fp.load_file
fp.print

Thử chạy và bạn sẽ thấy nội dung của file test1.txt sẽ được in ra.

"test1.txt\n\nIt's just a test file for Celluloid\n\n1\n"

Celluloid tự động đẩy instance được tạo ra của actor FilePutter vào thread của nó. Cách gọi hàm không thay đổi so với việc sử dụng class thông thường, sau khi sử dụng load_file để đọc file, gọi print để in ra nội dung file. Không có gì là phức tạp cả (nod)

Giờ chúng ta sẽ thử với trường hợp nhiều files một lúc (honho)

require 'celluloid/current'

class FilePutter
  include Celluloid

  def initialize(filename)
    @filename = filename
  end

  def load_file
    @file_contents = File.read @filename
  end

  def print
    p @file_contents
  end

end

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

files.each do |file|
  fp = FilePutter.new file
  fp.load_file
  fp.print
end

Trong ví dụ trên, ta đã tạo ra 5 threads để mỗi thread chịu trách nhiệm đọc một file trong mảng files.

"test1.txt\n\nIt's just a test file for Celluloid\n\n1\n"
"test2.txt\n\nIt's just a test file for Celluloid\n\n2\n"
"test3.txt\n\nIt's just a test file for Celluloid\n\n3\n"
"test4.txt\n\nIt's just a test file for Celluloid\n\n4\n"
"test5.txt\n\nIt's just a test file for Celluloid\n\n5\n"

Tuy nhiên, tất cả các methods chúng ta vừa gọi đều mang tính đồng bộ (synchronously), có nghĩa là phải đợi đến đến khi kết thúc mới có thể thực hiện tiếp. Vậy làm cách nào để quăng các xử lý sang một bên để tiếp tục bình thường các xử lý khác?

Câu trả lời với Celluloid rất đơn giản:

require 'celluloid/current'

class FilePutter
  include Celluloid

  def initialize(filename)
    @filename = filename
  end

  def load_file_and_print
    @file_contents = File.read @filename
    p @file_contents
  end

end

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

files.each do |file|
  fp = FilePutter.new file
  fp.async.load_file_and_print
end

Đầu tiên, chúng ta gộp xử lý load file và in nội dung file vào một method, đặt tên là load_file_and_print. Sau đó, trong vòng lặp thay vì gọi load_file_and_print, hãy gọi async.load_file_and_print và xem kết quả:

"test1.txt\n\nIt's just a test file for Celluloid\n\n1\n"
"test3.txt\n\nIt's just a test file for Celluloid\n\n3\n"
"test2.txt\n\nIt's just a test file for Celluloid\n\n2\n"
"test5.txt\n\nIt's just a test file for Celluloid\n\n5\n"
"test4.txt\n\nIt's just a test file for Celluloid\n\n4\n"

Khi bạn sử dụng async trước method, Celluloid sẽ chạy method đó ở trạng thái không đồng bộ (asynchronously), cho phép chương trình chạy tiếp tục mà không cần đợi file được load xong hay xử lý xong việc in nội dung.

Lúc này, lại có một vấn đề nảy sinh: nếu xuất hiện lỗi khiến message không thể gửi đi được, hay actor không thể respond được, v.vv.. thì sao? Làm sao chúng ta có thể phát hiện được điều đó khi chương trình vẫn đang tiếp tục chạy (?)

Câu trả lời sẽ có ở phần 2, mời các bạn đón xem (hihi)


Source: An Introduction to Celluloid, Part I


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í