2009-07-09 15 views
8

Tôi đến từ phần lớn nền C++, nhưng tôi nghĩ rằng câu hỏi này áp dụng cho luồng trong bất kỳ ngôn ngữ nào. Đây là kịch bản:Làm thế nào để hệ thống luồng đối phó với dữ liệu được chia sẻ đang được lưu trữ bởi cpus khác nhau?

  1. Chúng tôi có hai chủ đề (ThreadA và ThreadB), và một giá trị x trong bộ nhớ chia sẻ

  2. Giả sử rằng quyền truy cập vào x được kiểm soát một cách thích hợp bởi một mutex (hoặc kiểm soát đồng bộ thích hợp khác)

  3. Nếu chủ đề xảy ra để chạy trên các bộ vi xử lý khác nhau, điều gì sẽ xảy ra nếu ThreadA thực hiện thao tác ghi, nhưng bộ xử lý của nó đặt kết quả trong bộ đệm L2 thay vì bộ nhớ chính? Sau đó, nếu ThreadB cố gắng đọc giá trị, nó sẽ không chỉ nhìn vào bộ đệm chính L1/L2/bộ nhớ chính của nó và sau đó làm việc với bất kỳ giá trị cũ nào đã có?

Nếu không phải vậy, thì vấn đề này được quản lý như thế nào?

Nếu đúng như vậy, thì bạn có thể làm gì để khắc phục sự cố?

Trả lời

9

Ví dụ của bạn sẽ hoạt động tốt.

Nhiều bộ xử lý sử dụng coherency protocol chẳng hạn như MESI để đảm bảo dữ liệu vẫn được đồng bộ giữa các bộ đệm. Với MESI, mỗi dòng bộ nhớ cache được coi là được sửa đổi, được lưu giữ độc quyền, được chia sẻ giữa CPU hoặc không hợp lệ. Viết một dòng bộ nhớ cache được chia sẻ giữa các bộ vi xử lý buộc nó trở thành không hợp lệ trong các CPU khác, giữ cho bộ nhớ cache đồng bộ.

Tuy nhiên, điều này là không đủ. Các bộ vi xử lý khác nhau có khác nhau memory models và hầu hết các bộ vi xử lý hiện đại đều hỗ trợ một số cấp truy cập lại bộ nhớ. Trong những trường hợp này, cần memory barriers.

Ví dụ nếu bạn có Chủ đề A:

DoWork(); 
workDone = true; 

Và Thread B:

while (!workDone) {} 
DoSomethingWithResults() 

Với cả hai chạy trên bộ vi xử lý riêng biệt, không có đảm bảo rằng viết thực hiện trong vòng DoWork() sẽ hiển thị với luồng B trước khi ghi vào workDone và DoSomethingWithResults() sẽ tiến hành với trạng thái không nhất quán. Các rào cản bộ nhớ đảm bảo một số thứ tự của các lần đọc và ghi - thêm một rào cản bộ nhớ sau DoWork() trong Thread A sẽ buộc tất cả các lần đọc/ghi được thực hiện bởi DoWork để hoàn thành trước khi ghi vào workDone, sao cho Thread B có được một khung nhìn nhất quán. Mutexes vốn đã cung cấp một rào cản bộ nhớ, để đọc/ghi không thể vượt qua một cuộc gọi để khóa và mở khóa.

Trong trường hợp của bạn, một bộ xử lý sẽ báo hiệu cho những người khác rằng nó làm bẩn một dòng bộ nhớ cache và buộc các bộ xử lý khác tải lại từ bộ nhớ. Nhận được mutex để đọc và ghi giá trị đảm bảo rằng sự thay đổi bộ nhớ được hiển thị cho bộ xử lý khác theo thứ tự mong đợi.

+0

Cảm ơn bạn rất nhiều vì phản hồi này. Tôi đã tự hỏi liệu một số loại cơ chế cấp phần cứng có phải được đưa vào chơi ở đây không, bởi vì dường như có những giới hạn thực tế về những gì có thể đạt được ở cấp độ ngôn ngữ/trình biên dịch. – csj

1

Hầu hết các nguyên thủy khóa như mutexes ngụ ý memory barriers. Những lực lượng này sẽ làm cho bộ nhớ cache bị tuôn ra và tải lại.

Ví dụ,

ThreadA { 
    x = 5;   // probably writes to cache 
    unlock mutex; // forcibly writes local CPU cache to global memory 
} 
ThreadB { 
    lock mutex; // discards data in local cache 
    y = x;   // x must read from global memory 
} 
+1

Tôi không tin rằng các rào cản buộc bộ nhớ cache bị tuôn ra, chúng buộc các ràng buộc về thứ tự hoạt động của bộ nhớ. Việc xóa bộ nhớ cache sẽ không giúp bạn nếu ghi vào X có thể vượt qua việc mở khóa mutex. – Michael

+0

Các rào cản sẽ trở nên vô dụng nếu trình biên dịch sắp xếp lại các hoạt động bộ nhớ trên chúng, hmm? Ít nhất cho GCC, tôi nghĩ rằng điều này thường được thực hiện với một bộ nhớ clobber, mà nói với GCC "vô hiệu hóa bất kỳ giả định về bộ nhớ". – ephemient

+0

Ồ, tôi hiểu bạn đang nói gì. Việc xóa bộ nhớ cache là không cần thiết, miễn là việc đặt hàng được tôn trọng đúng cách giữa các bộ xử lý. Vì vậy, tôi cho rằng giải thích này là một cái nhìn đơn giản, và bạn sẽ tìm hiểu thêm về các chi tiết phần cứng. – ephemient

0

Nhìn chung, trình biên dịch hiểu khi bộ nhớ chia sẻ, và có nỗ lực đáng kể để đảm bảo rằng bộ nhớ chia sẻ được đặt ở một nơi thể chia sẻ. Các trình biên dịch hiện đại rất phức tạp theo cách chúng đặt hàng các hoạt động và truy cập bộ nhớ; họ có xu hướng hiểu bản chất của luồng và bộ nhớ chia sẻ. Đó không phải là để nói rằng họ đang hoàn hảo, nhưng nói chung, phần lớn các mối quan tâm được đưa về chăm sóc bởi trình biên dịch.

0

C# có một số bản dựng hỗ trợ cho loại sự cố này. Bạn có thể đánh dấu một biến với từ khóa volatile, từ đó buộc nó phải được đồng bộ hóa trên tất cả các cpu.

public static volatile int loggedUsers; 

Phần khác là một wrappper cú pháp xung quanh phương pháp NET gọi Threading.Monitor.Enter (x) và Threading.Monitor.Exit (x), trong đó x là một biến để khóa. Điều này làm cho các chủ đề khác cố gắng khóa x phải chờ cho đến khi các chủ đề khóa gọi Exit (x).

public list users; 
// In some function: 
System.Threading.Monitor.Enter(users); 
try { 
    // do something with users 
} 
finally { 
    System.Threading.Monitor.Exit(users); 
}