Shared State Is Incorrect State — Trạng thái dùng chung là trạng thái sai (Pragmatic Programmer #34)
Mở đầu
Chào mấy bạn, lại là mình đây! Mình đang tiếp tục hành trình đọc cuốn "The Pragmatic Programmer" (20th Anniversary Edition) của Dave Thomas và Andy Hunt — một cuốn sách mà mình nghĩ bất kỳ lập trình viên nào cũng nên đọc ít nhất một lần trong đời. Hôm nay chúng ta sẽ nói về Topic 34 thuộc Chapter 6: Concurrency — chủ đề về "Shared State Is Incorrect State". Nghe cái tên thôi là thấy "máu" rồi đúng không nào?
Ảnh: Christina Morillo — Pexels
Concurrency (tạm dịch là "xử lý đồng thời") là một trong những chủ đề khó nhằn nhất trong lập trình. Nếu mấy bạn từng debug một cái bug "ma" — hôm nay chạy OK, mai chạy fail, chạy lại thì OK — thì rất có thể mấy bạn đang đối mặt với một vấn đề về shared state đấy.
Shared State Is Incorrect State
Tác giả mở đầu topic này bằng một câu chuyện rất dễ hình dung: bạn đang ngồi trong một nhà hàng, ăn xong món chính và hỏi phục vụ còn bánh apple pie không. Anh phục vụ nhìn vào tủ trưng bày, thấy còn một miếng, và bảo "còn ạ". Cùng lúc đó, ở bàn bên kia, một khách khác cũng hỏi một người phục vụ khác câu tương tự, và cô ấy cũng thấy còn một miếng. Cả hai người phục vụ đều hứa với khách của mình là sẽ có bánh. Nhưng chỉ có một miếng bánh — một trong hai khách sẽ phải thất vọng.
Ảnh: MikeGz — Pexels
Câu chuyện này chính là một ví dụ kinh điển về race condition — một tình huống mà kết quả của chương trình phụ thuộc vào thứ tự thực thi không kiểm soát được của các thread. Cả hai "waiter" (thread) đều thực hiện thao tác "check-then-act": kiểm tra số bánh, nếu còn thì hứa với khách. Nhưng giữa lúc "check" và "act" thì một waiter khác đã nhảy vào và thay đổi dữ liệu, khiến cho cái "truth" ban đầu không còn đúng nữa.
Trong code, vấn đề này trông như thế này:
if display_case.pie_count > 0
promise_pie_to_customer()
display_case.take_pie()
give_pie_to_customer()
end
Khi hai thread chạy đoạn code này đồng thời, cả hai đều thấy pie_count > 0 (vì lúc đó nó vẫn là 1), và cả hai đều hứa bánh với khách. Kết quả: một người phục vụ lấy được bánh, người kia rơi vào trạng thái lỗi. Đây không phải là vấn đề vì hai process ghi vào cùng một vùng nhớ — mà là không process nào có thể đảm bảo rằng góc nhìn của nó về dữ liệu là nhất quán.
Giải pháp: Semaphores (hay Mutex)
Cuốn sách đề xuất giải pháp đầu tiên là dùng semaphore — một "vật sở hữu" mà chỉ một thread có thể nắm giữ tại một thời điểm. Trong câu chuyện nhà hàng, đó là một chú leprechaun nhựa đặt trên tủ bánh. Ai muốn lấy bánh thì phải cầm leprechaun trong tay. Hết việc thì trả lại.
Trong code, đó là các thao tác lock và unlock:
case_semaphore.lock()
if display_case.pie_count > 0
promise_pie_to_customer()
display_case.take_pie()
give_pie_to_customer()
end
case_semaphore.unlock()
Tuy nhiên, giải pháp này có nhược điểm là chỉ hiệu quả khi tất cả mọi người đều tuân thủ quy ước dùng semaphore. Nếu có một developer quên lock, thì... quay lại thời kỳ hỗn loạn.
Giải pháp tốt hơn: Resource Transactional
Thay vì để bên ngoài tự quản lý, chúng ta nên thiết kế API sao cho thao tác "kiểm tra + lấy" là một thao tác atomic duy nhất:
slice = display_case.get_pie_if_available()
if slice
give_pie_to_customer()
end
Nhưng ngay cả giải pháp này cũng cần semaphore bên trong. Và nếu có exception xảy ra giữa lúc lock/unlock thì semaphore sẽ không bao giờ được giải phóng — dẫn đến deadlock. Vì thế, luôn luôn dùng try/ensure (hoặc finally) để đảm bảo unlock được gọi.
Ảnh: Markus Spiske — Pexels
Bài học mở rộng
Tác giả chỉ ra rằng vấn đề shared state không chỉ giới hạn ở bộ nhớ dùng chung. Nó xuất hiện ở bất cứ đâu mà code của bạn chia sẻ tài nguyên có thể thay đổi: file, database, external services... Có một câu chuyện rất thú vị trong sách: khi viết bản edition mới, tác giả đã song song hóa toolchain build bằng threads — và build bắt đầu fail một cách ngẫu nhiên. Lý do? Một vài đoạn code tạm thời thay đổi current directory. Trong phiên bản không parallel, việc restore lại directory sau khi dùng xong là đủ. Nhưng trong phiên bản parallel, một thread đổi directory, thread khác chạy vào và thấy sai directory!
Điều này dẫn đến một câu nói bất hủ trong cuốn sách:
"Doctor, it hurts when I do this. Then don't do it."
Đau ở đâu thì tránh đấy. Hay nói cách khác, nếu shared state gây đau đầu, hãy tránh shared state. Đây là lý do mà các topic tiếp theo (Topic 35: Actors, Topic 36: Blackboards) đề xuất những cách tiếp cận không dùng shared state.
Cảm nhận của mình
Cá nhân mình thấy topic này cực kỳ giá trị. Không phải vì nó dạy mình điều gì mới — race condition và mutex là kiến thức cơ bản trong lập trình — mà vì cách tác giả đặt vấn đề rất hay. Câu chuyện nhà hàng với apple pie khiến cho một khái niệm trừu tượng như "shared state" trở nên siêu dễ hiểu, ai cũng hình dung được.
Mình đã từng gặp một bug kiểu này trong dự án thực tế: có một service xử lý file upload, và khi nhiều request đến cùng lúc, code kiểm tra dung lượng ổ đĩa rồi mới ghi file — nhưng giữa lúc check và write, một request khác cũng check và thấy còn dung lượng, dẫn đến cả hai cùng ghi và làm đầy ổ đĩa. Nghe quen không? Giống hệt câu chuyện apple pie!
Điều mình tâm đắc nhất là câu "Doctor, it hurts when I do this. Then don't do it." — một triết lý rất "pragmatic" (thực dụng). Nếu concurrency với shared state quá khó, đừng làm shared state. Hãy dùng Actors, message passing, hoặc immutable data. Các ngôn ngữ như Rust hay Erlang/Elixir đã áp dụng triết lý này rất thành công.
Một điểm trừ nhỏ: topic này hơi thiếu ví dụ về các ngôn ngữ hiện đại hơn. Nếu có thêm ví dụ về goroutine trong Go, hoặc async/await trong Rust/C# thì sẽ tuyệt hơn. Nhưng dù sao đây là cuốn sách về "nguyên lý" chứ không phải "công nghệ cụ thể", nên mình cũng thông cảm.
Kết
Tóm lại, Shared State Is Incorrect State (Trạng thái dùng chung là trạng thái sai). Khi làm việc với concurrency, hãy luôn nhớ: hai thread nhìn vào cùng một dữ liệu không đồng nghĩa với việc chúng thấy cùng một giá trị. Thao tác check-then-act không atomic. Dùng semaphore, mutex, hoặc tốt hơn — tránh shared state hoàn toàn.
Hẹn mấy bạn ở bài sau với Topic 35: "Random Failures Are Often Concurrency Issues" — những bug "ma" không phải do "ma" mà do concurrency đấy. 😄
Tags: #PragmaticProgrammer #Concurrency #SharedState #RaceCondition #SoftwareEngineering