Cùng với sự phát triển ngày càng phổ biến của mô hình Ajax, một trang trình duyệt có thể sinh ra những yêu cầu lấy dữ liệu từ máy chủ ở nền sau (background) trong khi ở nền trước (foreground), giao diện người dùng vẫn tiếp tục được kích hoạt (do đó mới xuất hiện từ “không đồng bộ” – Asynchronous trong AJAX). Tuy nhiên còn tồn tại một vấn đề là hai hoạt động này thường truy cập đồng thời vào các đoạn mã javascript và cấu trúc dữ liệu DOM dùng chung. Những giải pháp truyền thống cho vấn đề lập trình tương tranh hiện không được javascript hỗ trợ. Bài viết này miêu tả cách áp dụng mới của một phương pháp đã được chứng minh: phương pháp loại trừ lẫn nhau (mutual exclusion), cho phép loại bỏ những hạn chế của Javascript
Lý do chọn cơ chế loại trừ lẫn nhau?
Bất kỳ lúc nào cũng có những thread của chương trình truy cập vào cùng một dữ liệu, hoạt động trong cùng một thời gian làm nảy sinh vấn đề. Những chương trình này thường cho rằng dữ liệu mà nó tác động qua lại không làm thay đổi phía dưới nền của chúng. Những phần của đoạn mã truy cập vào những cấu trúc dữ liệu dùng chung này được biết đến như một đoạn găng và kỹ thuật cho phép chỉ một đoạn mã duy nhất chạy trong một thời điểm được gọi là mutual exclusion (loại trừ lẫn nhau). Tình huống này nảy sinh trong những ứng dụng AJAX khi đoạn mã xử lý dữ liệu trả về từ một XMLHttpRequest một cách không đồng bộ trong khi dữ liệu đó cũng đang được sử dụng bởi một đoạn mã từ phía giao diện người dùng. Dữ liệu dùng chung này có thể là những biến Javacript được sử dụng trong việc cài đặt mô hình dữ liệu MVC và/hoặc đối tượng DOM của chính trang web đó. Tính logic của mỗi đoạn mã sẽ bị phá vỡ nếu một trong hai đoạn mã không phối hợp với nhau trong việc thay đổi những dữ liệu dùng chung.
Có thể bạn sẽ đặt câu hỏi: “Tại sao tôi chưa từng gặp vấn đề này bao giờ?” Thật không may những vấn đề này lại phụ thuộc vào thời gian (cũng được biết đến như là những điều kiện chạy đua) vì vậy chúng không thường xuyên (hoặc không bao giờ) xảy ra. Xác suất xuất hiện của chúng phụ thuộc vào nhiều yếu tố. Để tăng thêm sức mạnh, các ứng dụng Internet giàu tính năng cần phải ngăn chặn những tình huống này bằng cách đảm bảo rằng những vấn đề này không thể xảy ra.
Vì vậy, một cơ chế loại trừ lẫn nhau là cần thiết để đảm bảo rằng chỉ một đoạn găng được bắt đầu và kết thúc trước khi một đoạn găng khác bắt đầu. Trong hầu hết các ngôn ngữ lập trình chủ đạo và các framework thực thi có tồn tại một vài cơ chế loại trừ lẫn nhau, nhưng thật không may là trong Javascript phía trình duyệt lại không có. Mặc dù tồn tại những thuật toán cổ điển cài đặt cơ chế loại trừ lẫn nhau mà không đòi hỏi hỗ trợ đặc biệt từ ngôn ngữ lập trình hoặc từ môi trường hoạt động, tuy nhiên những thuật toán này cũng đòi hỏi một số yếu tố cơ bản mà không tồn tại trong Javascript và các trình duyệt như Internet Explorer. Thuật toán cổ điển sau sẽ được sửa đổi để có thể vượt qua được những hạn chế của Javascript và của trình duyệt.
Thuật toán Bakery
Trong số các thuật toán loại trừ lẫn nhau của khoa học máy tính, có một thuật toán mang tên Bakery của Lamport làm việc với nhiều luồng cạnh tranh quyền điều khiển trong khi cơ chế giao tiếp giữa chúng chỉ là vùng nhớ dùng chung (Tức là không cần đến các cơ chế đặc biệt như semaphore, thiết lập và kiểm tra…). Thuật toán này được mô tả như sau: một hiệu bánh mỳ mà yêu cầu khách hàng lấy một số hiệu và đợi cho đến khi số hiệu của họ được gọi. Khung của thuật toán được liệt kê trong Đoạn mã 1, đảm bảo từng thread đi vào hoặc đi ra đoạn găng mà không bị xung đột.
// declaration & initial values of global variables
Enter, Number: array [1..N] of integer = {0};
// logic used by each thread...
// where "(a, b) < (c, d)"
// means "(a < c) or ((a == c) and (b < d))"
Thread(i) {
while (true) {
Enter [i] = 1;
Number[i] = 1 + max(Number[1],...,Number[N]);
Enter [i] = 0;
for (j=1; j<=N; ++j) {
while (Enter[j] != 0) {
// wait until thread j receives its number
}
while ((Number[j]!=0)
&& ((Number[j],j) < (Number[i],i))) {
// wait until threads with smaller numbers
// or with the same number, but with higher
// priority, finish their work
}
}
// critical section...
Number[i] = 0;
// non-critical section...
}
}
Đoạn mã 1. Giả mã thuật toán bakery của Lamport
Thuật toán Bakery giả định rằng mỗi thread biết được số hiệu mà nó sở hữu (hằng số i) và tổng số thread đang hoạt động (hằng số N). Nó cũng giả định rằng có một phương pháp đợi hoặc ngủ, tức là có một cách để chuyển quyền điều khiển của CPU cho các thread khác một cách tạm thời. Thật không may là trên trình duyệt Internet Explorer, Javascript lại không có những khả năng này. Tuy nhiên thuật toán Bakery không bị phá vỡ nếu như các phần khác nhau của mã nguồn chạy trên cùng một thread thực nhưng lại giống như chạy trên nhiều thread ảo khác nhau. Javascript cũng có một cơ chế đặt lịch cho các hàm để chúng chạy sau một khoảng thời gian trễ nhất định, vì vậy thuật toán Bakery có thể được tinh chỉnh để sử dụng những hàm thay thế này.
Thuật toán Wallace biến thể
Cản trở lớn nhất khi cài đặt thuật toán Bakery trên Javascript là không tồn tại giao tiếp lập trình ứng dụng (API) cho luồng. Không có cách nào để biết luồng nào đang chạy hoặc bao nhiêu luồng đang ở trạng thái hoạt động; không có cách nào để chuyển CPU sang cho luồng khác; và không có cách nào để tạo ra một luồng mới để quản lý các luồng khác. Bởi vì lý do này, chúng ta thậm chí không thể xác minh một sự kiện của trình duyệt (ví dụ, click vào 1 nút bấm, kết quả XML trả về đã sẵn sàng) được gán cho luồng nào.
Một cách để vượt qua các cản trở này là sử dụng mẫu thiết kế Command. Bằng cách đặt vào một đối tượng command toàn bộ logic có khả năng đi vào đoạn găng, cùng với những dữ liệu cần thiết để khởi tạo các logic đó. Thuật toán Bakery có thể được sửa lại thành một lớp quản lý các command. Lớp loại trừ lẫn nhau này sẽ gọi tới các đoạn găng (được đóng gói trong các phương thức khác nhau của command) chỉ khi các đoạn găng khác đang không hoạt động, như là từng đoạn đang chạy trong luồng ảo của riêng nó. Hàm setTimeOut() của Javascript được sử dụng để chuyển CPU sang các command đang chờ khác.
Cứ cho là có một lớp căn bản đơn giản cho các đối tượng command (Lớp Command trong Listing 2), một lớp có thể được định nghĩa (Lớp Mutex trong Listing 3), cài đặt biến thể Wallace của thuật toán Bakery. Chú ý rằng, trong khi có nhiều cách để cài đặt các đối tượng dựa trên lớp trong Javascript (và để ngắn gọn, cách đơn giản được sử dụng ở đây), bất cứ một cách nào cũng có thể hoạt động với kỹ thuật này miễn là mỗi đối tượng command có id của riêng nó, và toàn bộ đoạn găng được đóng gói trong một phương thức duy nhất.
1 function Command() {
2 if (!Command.NextID) Command.NextID = 0;
3 this.id = ++Command.NextID;
4 // unsynchronized API
5 this.doit = function(){ ; mso-ansi-language: EN-US; mso-fareast-language: EN-US; mso-bidi-language: AR-SA">Với AJAX và RIA, sự thúc đẩy để xây dựng các giao diện người dùng động đã hướng những nhà phát triển sử dụng những mô hình giống nhau ( ví dụ Model – View – Controller) trước đây gắn với việc tập trung GUI trên các trình khách. Với việc thể hiện và điều khiển được xây dựng theo mô đun, mỗi phần với những sự kiện và những hàm xử lý sự kiện khác nhau (nhưng cùng sử dụng các cấu trúc dữ liệu chung), khả năng đụng độ tăng lên. Bằng cách gói việc xử lý sự kiện vào các lớp Command, không chỉ có thể cài đặt được thuật toán Wallace biến thể mà còn có thể cung cấp các chức năng undo/redo một cách phong phú, thiết lập các kịch bản xây dựng giao diện, và các phương tiện kiểm thử.