11 Chủ đề trong C # - Phần 8: Bối cảnh đồng bộ hóa

(Post 26/10/2007) Thay vì khóa thủ công, người ta có thể khóa một cách khai báo. Bằng cách bắt nguồn từ ContextBoundObject và áp dụng thuộc tính Đồng bộ hóa, người ta hướng dẫn CLR áp dụng khóa tự động.

  • Phần 1: Tổng quan và khái niệm
  • Phần 2: Tạo và bắt đầu chủ đề
  • Phần 3: Cơ bản về đồng bộ hóa
  • Phần 4: Khóa và an toàn chỉ
  • Phần 5: Ngắt và hủy bỏ
  • Phần 6: Trạng thái luồng
  • Phần 7: Xử lý Chờ

Đây là một ví dụ:

sử dụng Hệ thống;
sử dụng System.Threading;
sử dụng System.Runtime.Remoting.Contexts;

[Đồng bộ hóa] public class AutoLock: ContextBoundObject {
public void Demo () {
Console.Write (“Start …”);
Thread.Sleep (1000); // Chúng ta không thể bắt trước ở đây
Console.WriteLine (“end”); // nhờ khóa tự động!
}
}

public class Test {
public static void Main () {
AutoLock safeInstance = new AutoLock ();
mới Thread (safeInstance.Demo) .Start (); // Gọi
Thread mới Demo (safeInstance.Demo) .Start (); // phương thức 3 lần
safeInstance.Demo (); // kiêm nhiệm.
}
}

 

Bắt đầu … kết thúc
Bắt đầu … kết thúc
Bắt đầu … kết thúc

CLR đảm bảo rằng chỉ một luồng có thể thực thi mã trong safeInstance tại một thời điểm. Nó thực hiện điều này bằng cách tạo một đối tượng đồng bộ hóa duy nhất – và khóa nó xung quanh mọi lệnh gọi đến từng phương thức hoặc thuộc tính của safeInstance. Phạm vi của khóa – trong trường hợp này – đối tượng safeInstance – được gọi là ngữ cảnh đồng bộ hóa.

Vì vậy, làm thế nào để điều này hoạt động? Một manh mối nằm trong không gian tên của thuộc tính Đồng bộ hóa: System.Runtime.Remoting.Contexts. ContextBoundObject có thể được coi là một đối tượng “từ xa” – nghĩa là tất cả các lệnh gọi phương thức đều bị chặn. Để thực hiện việc chặn này, khi chúng tôi khởi tạo AutoLock, CLR thực sự trả về một proxy – một đối tượng có cùng phương thức và thuộc tính của một đối tượng AutoLock, đóng vai trò trung gian. Thông qua trung gian này mà khóa tự động diễn ra. Nhìn chung, việc đánh chặn thêm khoảng một micro giây cho mỗi lệnh gọi phương thức.

Đồng bộ hóa tự động không thể được sử dụng để bảo vệ các thành viên kiểu tĩnh, cũng như các lớp không có nguồn gốc từ ContextBoundObject (ví dụ: Biểu mẫu Windows).

Khóa được áp dụng nội bộ theo cùng một cách. Bạn có thể mong đợi rằng ví dụ sau sẽ mang lại kết quả giống như ví dụ trước:

[Đồng bộ hóa] public class AutoLock: ContextBoundObject {
public void Demo () {
Console.Write (“Start …”);
Thread.Sleep (1000);
Console.WriteLine (“end”);
}

public void Test () {
new Thread (Demo) .Start ();
new Thread (Demo) .Start ();
new Thread (Demo) .Start ();
Console.ReadLine ();
}

public static void Main () {
new AutoLock (). Test ();
}
}

(Lưu ý rằng chúng tôi đã xem lén một câu lệnh Console.ReadLine). Bởi vì chỉ một luồng có thể thực thi mã tại một thời điểm trong một đối tượng của lớp này, ba luồng mới sẽ vẫn bị chặn ở phương thức Demo cho đến khi phương thức Kiểm tra kết thúc – điều này yêu cầu ReadLine hoàn thành. Do đó, chúng tôi kết thúc với cùng một kết quả như trước, nhưng chỉ sau khi nhấn phím Enter. Đây là một chiếc búa an toàn luồng gần như đủ lớn để loại trừ mọi hoạt động đa luồng hữu ích trong một lớp!

Hơn nữa, chúng tôi chưa giải quyết được vấn đề được mô tả trước đó: ví dụ: nếu AutoLock là một lớp tập hợp, chúng tôi vẫn sẽ yêu cầu khóa xung quanh một câu lệnh như sau, giả sử nó chạy từ một lớp khác:

if (safeInstance.Count> 0) safeInstance.RemoveAt (0);

trừ khi bản thân lớp của mã này là một ContextBoundObject được đồng bộ hóa!

Một ngữ cảnh đồng bộ hóa có thể mở rộng ra ngoài phạm vi của một đối tượng. Theo mặc định, nếu một đối tượng được đồng bộ hóa được khởi tạo từ bên trong mã của một đối tượng khác, thì cả hai đều chia sẻ cùng một ngữ cảnh (nói cách khác, một khóa lớn!) Hành vi này có thể được thay đổi bằng cách chỉ định cờ số nguyên trong hàm tạo của thuộc tính đồng bộ hóa, sử dụng một trong các hằng số được xác định trong lớp SynchronizationAttribute:

Không thay đổiÝ nghĩa
KHÔNG ĐƯỢC HỖ TRỢTương đương với việc không sử dụng thuộc tính Đồng bộ hóa
ĐƯỢC HỖ TRỢTham gia bối cảnh đồng bộ hóa hiện có nếu được khởi tạo từ một đối tượng được đồng bộ hóa khác, nếu không vẫn chưa được đồng bộ hóa
BẮT BUỘC (mặc định)Tham gia bối cảnh đồng bộ hóa hiện có nếu được khởi tạo từ một đối tượng được đồng bộ hóa khác, nếu không sẽ tạo bối cảnh mới
REQUIRES_NEWLuôn tạo bối cảnh đồng bộ hóa mới

Vì vậy, nếu đối tượng của lớp SynchronizedA khởi tạo một đối tượng của lớp SynchronizedB, chúng sẽ được cung cấp các ngữ cảnh đồng bộ hóa riêng biệt nếu SynchronizedB được khai báo như sau:

[Synchronization (SynchronizationAttribute.REQUIRES_NEW)] lớp công khai SynchronizedB: ContextBoundObject {…

Phạm vi bối cảnh đồng bộ hóa càng lớn thì càng dễ quản lý, nhưng càng ít cơ hội cho sự đồng thời hữu ích. Ở đầu kia của thang đo, các ngữ cảnh đồng bộ hóa riêng biệt đưa ra những bế tắc. Đây là một ví dụ:

[Đồng bộ hóa] public class Deadlock: ContextBoundObject {
public DeadLock Other;
public void Demo () {Thread.Sleep (1000); Other.Hello (); }
void Hello () {Console.WriteLine (“xin chào”); }
}

public class Test {
static void Main () {
Deadlock dead1 = new Deadlock ();
Deadlock dead2 = new Deadlock ();
dead1.Other = dead2;
dead2.Other = dead1;
new Thread (dead1.Demo) .Start ();
dead2.Demo ();
}
}

Bởi vì mỗi phiên bản của Deadlock được tạo bên trong Test – một lớp không được đồng bộ hóa – mỗi phiên bản sẽ có ngữ cảnh đồng bộ hóa riêng và do đó, khóa riêng của nó. Khi hai đối tượng gọi lẫn nhau, không mất nhiều thời gian để deadlock xảy ra (chính xác là một giây!) Vấn đề sẽ đặc biệt nguy hiểm nếu các lớp Deadlock và Test được viết bởi các nhóm lập trình khác nhau. Có thể là không hợp lý khi mong đợi những người chịu trách nhiệm cho lớp Kiểm tra thậm chí nhận thức được hành vi vi phạm của họ, chứ đừng nói đến cách tiếp tục giải quyết nó. Điều này trái ngược với các ổ khóa rõ ràng, trong đó các điểm bế tắc thường rõ ràng hơn.

Gần đây

Một phương thức an toàn cho luồng đôi khi được gọi là reentrant, vì nó có thể được sử dụng trước một phần thông qua quá trình thực thi của nó, và sau đó được gọi lại trên một luồng khác mà không ảnh hưởng gì. Theo nghĩa chung, các thuật ngữ an toàn theo luồng và người đăng nhập lại được coi là đồng nghĩa hoặc có liên quan chặt chẽ với nhau.

Reentrancy, tuy nhiên, có một hàm ý nham hiểm hơn trong chế độ khóa tự động. Nếu thuộc tính Đồng bộ hóa được áp dụng với đối số nhập lại true:

[Đồng bộ hóa (true)]

thì khóa của ngữ cảnh đồng bộ hóa sẽ tạm thời được giải phóng khi việc thực thi rời khỏi ngữ cảnh. Trong ví dụ trước, điều này sẽ ngăn chặn deadlock xảy ra; rõ ràng là mong muốn. Tuy nhiên, một tác dụng phụ là trong thời gian tạm thời này, bất kỳ luồng nào cũng có thể tự do gọi bất kỳ phương thức nào trên đối tượng gốc (“nhập lại” ngữ cảnh đồng bộ hóa) và giải phóng những phức tạp của đa luồng mà một luồng đang cố gắng tránh ngay từ đầu. Đây là vấn đề của sự sống lại.

Bởi vì [Synchronization (true)] được áp dụng ở cấp độ lớp, thuộc tính này biến mọi lệnh gọi phương thức ngoài ngữ cảnh được thực hiện bởi lớp thành Trojan cho lần truy cập lại.

Mặc dù lần gần đây nhất có thể nguy hiểm nhưng đôi khi vẫn có một vài lựa chọn khác. Ví dụ: giả sử một người thực hiện đa luồng trong nội bộ một lớp được đồng bộ hóa, bằng cách ủy quyền logic cho các worker đang chạy các đối tượng trong các ngữ cảnh riêng biệt. Những người lao động này có thể bị cản trở một cách bất hợp lý trong việc giao tiếp với nhau hoặc đối tượng ban đầu mà không có sự từ tốn.

Điều này làm nổi bật một điểm yếu cơ bản của đồng bộ hóa tự động: phạm vi rộng rãi mà khóa được áp dụng thực sự có thể tạo ra những khó khăn có thể chưa bao giờ phát sinh. Những khó khăn này – deadlock, reentrancy, và tính toán đồng thời – có thể làm cho việc khóa thủ công trở nên ngon miệng hơn trong bất kỳ điều gì khác ngoài các kịch bản đơn giản.

(Sưu tầm)

FPT Aptech trực thuộc Tổ chức Giáo dục FPT có hơn 25 năm kinh nghiệm đào tạo lập trình viên quốc tế tại Việt Nam, và luôn là sự lựa chọn ưu tiên của các sinh viên và nhà tuyển dụng.
0981578920
icons8-exercise-96