Trong lập trình đôi khi chúng ta phải đối mặt với trường hợp cần ngăn chặn một yêu cầu đang muốn truy cập vào một biến, file, hay một cấu trúc dữ liệu nào đó mà yêu cầu khác đang nắm giữ. Việc các yêu cầu có quyền truy cập đồng thời vào một tài nguyên và sửa đổi chúng tự do sẽ gây ra khá nhiều vấn đề hay thậm chí là lỗi mà chúng ta không hề mong muốn.
Trong môi trường cân bằng tải - tức là bạn cố gắng tạo ra nhiều phiên bản máy chủ (instance) API giống nhau để tăng khả năng chịu lỗi cũng như xử lý đồng thời nhiều yêu cầu. Khi đó chúng ta càng khó ngăn chặn việc tranh chấp tài nguyên hơn bởi vì mỗi instance là độc lập và rất khó khăn để tạo giao tiếp giữa chúng. Chưa kể đến vấn đề hiệu suất cũng như độ phức tạp trong khi triển khai.
Tất cả điều trên buộc bạn phải tìm kiếm một giải pháp. Đó là lúc bạn cần biết đến một thuật ngữ có tên là Mutex, vậy thì Mutex là gì và nó nên được áp dụng trong những trường hợp như thế nào?
Race condition xảy ra khi có hai hay nhiều yêu cầu có thể truy cập dữ liệu được chia sẻ và chúng cố gắng thay đổi nó cùng một lúc. Vì thuật toán lập lịch luồng có thể hoán đổi giữa các luồng bất kỳ lúc nào, nên bạn không biết thứ tự mà các luồng sẽ cố gắng truy cập vào dữ liệu được chia sẻ. Do đó, kết quả của sự thay đổi dữ liệu lại phụ thuộc vào thuật toán lập lịch luồng, tức là cả hai luồng đang "chạy đua" để truy cập/thay đổi dữ liệu.
Vì lẽ đó, Race condition có thể gây ra lỗi không mong muốn trong lập trình, vì thế chúng ta cần phải tìm ra cách để giải quyết việc tranh chấp này. Hay chí ít là phải xác định được yêu cầu nào có quyền thao tác với dữ liệu. Đó là lí do Mutex ra đời.
Mutex - Mutual Exclusion hay còn gọi là "loại trừ lẫn nhau" được tạo ra nhằm ngăn chặn Race condition. Với mục tiêu một luồng không bao giờ được truy cập vào tài nguyên mà một luồng thực thi đang nắm giữ.
Tài nguyên được chia sẻ là một đối tượng dữ liệu, mà hai hoặc nhiều luồng đồng thời đang cố gắng sửa đổi. Thuật toán Mutex đảm bảo rằng nếu một quy trình đang chuẩn bị thao tác sửa đổi trên một đối tượng dữ liệu thì không một quy trình/luồng nào khác được phép truy cập hay sửa đổi cho đến khi nó hoàn tất và giải phóng đối tượng để các quy trình khác có thể tiếp tục.
Trong lập trình, Mutex được thể hiện tuỳ thuộc vào ngôn ngữ hay công cụ lập trình.
Node.js không có khái niệm rõ ràng về Mutex. Có thể chúng ta đã nghe ở đâu đó Node.js chỉ có một luồng vậy thì trường hợp tranh chấp tài nguyên đâu có xảy ra? Thực ra đúng là Node.js chỉ có một luồng và luồng đó được sử dụng để xử lý mã JS, nhưng còn các tác vụ I/O đa số được thực hiện bởi các luồng song song hay còn gọi là Worker Pool có trong libuv, thế nên khả năng tranh chấp vẫn có khả năng xảy ra tại đây.
Một số thư viện hỗ trợ triển khai mutex như async-mutex, về cơ bản nó vận dụng các giải pháp đánh dấu một luồng đang truy cập tài nguyên để loại trừ lẫn nhau, sử dụng Promise để chờ đợi cho đến khi được giải phóng (resolve)... Mọi thứ hoạt động, có lẽ vấn đề lo ngại lúc này chỉ là hiệu suất. Nhưng hãy dừng lại một chút, chúng ta đang mới nói đến trường hợp chỉ có 1 instance. Vậy trong môi trường có nhiều instance như môi trường phân tán (distributed) thì sao?
Môi trường phân tán là môi trường khi bạn "nhân bản" nhiều phiên bản instance giống nhau và các instance này không nhất thiết phải nằm trên cùng một máy chủ. Hay thậm chí bạn còn tận dụng được đa lõi CPU để chạy từng instance trên chúng. Khi đó những thư viện chỉ hỗ trợ liên lạc nội bộ như trên có lẽ sẽ không thể giải quyết mutex theo cách thông thường được, vì chúng không tạo ra môi trường dành cho việc liên lạc giữa các instance với nhau.
Thực tế nếu ứng dụng của bạn chỉ cần 1 instance thì không cần bận tâm đến trường hợp phân tán nữa. Nhưng ai biết trước một ngày nào đó ứng dụng của bạn phát triển vượt trội, việc "nhân bản" chúng lên để cân bằng tải là điều khó tránh khỏi. Dù sớm hay muộn đó vẫn là vấn đề bạn cần phải biết cách giải quyết.
Có nhiều phương pháp để xử lý mutex trong trường hợp phân tán. Mỗi cách sẽ thể hiện ra những ưu nhược điểm để chúng ta áp dụng cho từng bài toán sao cho phù hợp. Cách đơn giản nhất là chỉ chạy một instance tập trung xử lý nghiệp vụ liên quan đến tài nguyên chia sẻ. Mô hình này có thể được thể hiện qua việc dùng message queue hay stream... đầy những yêu cầu cần xử lý vào queue và xử lý chúng lần lượt. Dĩ nhiên rằng như thế sẽ không có việc xung đột ở đây nữa.
Nhưng không phải lúc nào bạn cũng có thể tạo ra những instance riêng biệt như vậy, buộc bạn phải tìm một giải pháp khác phù hợp hơn. Một trong số đó là tận dụng tốc độ của redis để làm kênh trao đổi giữa các Instance. Về cơ bản cách này hoạt động trên nguyên tắc tạo ra một "key" và luồng xử lý nào nhanh tay nắm được key sẽ có quyền truy cập vào tài nguyên chia sẻ trước. Sau khi toàn tất nó sẽ giải phóng key để nhường cho các luồng xử lý tiếp theo.
Bạn có thể tự triển khai một thuật toán Mutex cho riêng mình hoặc sử dụng thư viện triển khai mutex có trên npm. warlock hay live-mutex là những ví dụ. Trong khi warlock sử dụng Redis để tạo mối liên kết giữa các instance thì live-mutex tự triển khai cho mình một hệ thống liên kết riêng và cung cấp client để sử dụng. Nhìn chung những thư viện này có thể đáp ứng nhu cầu sử dụng ở một mức độ nào đó. Trong một hệ thống thông tin, khả năng "tin cậy", "chịu lỗi" và phục hồi sau sự cố luôn là mối quan tâm hàng đầu.
Redis cũng có một sản phẩm gọi là Distributed Locks mà theo họ triển khai một thuận toán gọi là Redlock theo mô hình sử dụng nhiều máy chủ redis tuân thủ 2 nguyên tắc "Safety" và "Liveness" cho mức độ tin cậy cao cùng khả năng chịu lỗi trong môi trường phân tán.
Ngoài ra mutex còn được thể hiện trong việc giải quyết xung đột dữ liệu ở các dịch vụ (services) như là Database. Khi chúng ta có thể tạo ra các khoá (locks) để toàn quyền truy cập vào một bảng hay một hàng dữ liệu mà các truy vấn tiếp theo phải chờ đợi cho đến khi khoá được giải phóng.
Chúng ta có thể tận dụng locks trong cơ sở dữ liệu để giải quyết Race condition, nhưng tài nguyên bị khoá liên tục trong thời gian dài là không mấy hiệu quả. Khi đó bạn cần phải cân bằng hoặc tìm ra một giải pháp khác sao cho hợp lý.
Trong lập trình đặc biệt là lập trình trên nhiều luồng xử lý dữ liệu đòi hỏi chúng ta phải giải quyết vấn đề tranh chấp tài nguyên. Mutex là một trong những kĩ thuật được tạo ra nhằm giải quyết những lỗi lầm do Race condition gây ra. Hiểu được mutex sẽ giúp bạn có thêm một kỹ năng giải quyết xung đột.
Xin chào, tôi tên là Hoài - một anh Dev kể chuyện bằng cách viết ✍️ và làm sản phẩm 🚀. Với nhiều năm kinh nghiệm lập trình, tôi đã đóng góp một phần công sức cho nhiều sản phẩm mang lại giá trị cho người dùng tại nơi đang làm việc, cũng như cho chính bản thân. Sở thích của tôi là đọc, viết, nghiên cứu... Tôi tạo ra trang Blog này với sứ mệnh mang đến những bài viết chất lượng cho độc giả của 2coffee.dev.Hãy theo dõi tôi qua các kênh LinkedIn, Facebook, Instagram, Telegram.
Bình luận (0)