Actors và Processes — Khi concurrency không cần shared state (Pragmatic Programmer #35)

Phong

Mở đầu

Ảnh: ThisIsEngineering — Pexels

Lại tới hẹn lại lên — series Pragmatic Programmer tiếp tục với Chapter 6 về Concurrency. Hồi đầu mình cũng không ngờ cuốn sách này dành hẳn 1 chapter dài cho chủ đề này, nhưng càng code nhiều càng thấy nó đúng. Bao nhiêu bug "không thể reproduce" hóa ra đều liên quan tới concurrency cả.

Hôm nay mình muốn kể về Topic 35: Actors and Processes — một cách tiếp cận concurrency hoàn toàn khác với mấy cái lock/semaphore mà dân C/java hay xài.

Actors và Processes — Cái nhìn khác về concurrency

Ảnh: Merlin Lightpainting — Pexels

Trước giờ, khi nghĩ tới concurrency, đa số lập trình viên nghĩ ngay tới threads, shared memory, và mấy cái locks. Cả một thế hệ dev Java (mình cũng từng) lớn lên với synchronized, ReentrantLock, Semaphore — và cũng từng đau đầu với deadlock, livelock, race condition.

Sách giới thiệu một hướng khác: actor model. Ý tưởng cốt lõi là:

  • Mỗi actor là một "virtual processor" độc lập, có state riêng (private) và một mailbox để nhận messages.
  • Actors không chia sẻ memory với nhau. Tuyệt đối không.
  • Chúng giao tiếp bằng cách gửi messages qua mailbox.
  • Mỗi actor xử lý 1 message tại một thời điểm — nên bên trong nó hoàn toàn single-threaded, không cần lock.

Nghe quen không? Nếu bạn từng nghe tới Erlang (hay Elixir) thì đây chính là nền tảng của BEAM VM. Hay Akka bên Scala/Java, Orleans bên .NET, Pony — tất cả đều dựa trên actor model.

Còn process trong sách được định nghĩa là một virtual processor tổng quát hơn, thường do OS cung cấp. Nhưng về mặt ý tưởng, một process có thể bị constrain (bằng convention) để hoạt động giống như một actor.

Cảm nhận của mình

Ảnh: Ivan S — Pexels

Đọc tới topic này mình thấy tâm đắc nhất cái câu: "There's no need to write any code to handle concurrency, as there is no shared state."

Thiệt tình, hồi mới học concurrency bên Java, mình từng mất 3 ngày debug 1 cái deadlock chỉ vì thread A lock resource X rồi chờ resource Y, trong khi thread B lock Y rồi chờ X. Nghe đơn giản, nhưng khi có 5-6 threads cùng chạy thì nó thành cơn ác mộng. Với actor model, chuyện đó không thể xảy ra — vì không có shared state, không có lock, không có deadlock.

Điểm hay nữa là actor model scale được cả chiều ngang (distributed). Một actor trên máy này gửi message cho actor trên máy khác cũng giống hệt như gửi trong cùng process — chỉ khác đường truyền. Đây là lý do Erlang/Elixir nổi tiếng trong mấy hệ thống telecom với yêu cầu availability cực cao (9 "9"s).

Tuy nhiên, sách cũng cảnh báo: kiến trúc kiểu này khó reasoning hơn vì hành động gián tiếp — bạn gửi message đi rồi, ai xử lý, khi nào xong thì… tuỳ. Cần tooling tốt (như actor monitoring, message tracing) để debug.

Kết

Actor model là một trong những ý tưởng mà mình ước mình biết sớm hơn. Nó thay đổi hoàn toàn cách bạn nghĩ về concurrency — từ "bảo vệ shared state" thành "không có shared state ngay từ đầu". Tip 59 của cuốn sách (Use Actors for Concurrency Without Shared State) là một trong những tip mà mình thấy vẫn còn nguyên giá trị tới tận bây giờ.

Hẹn mấy bạn bài sau với topic 36: Blackboards!