Mã không đồng bộ, các biến được chia sẻ, luồng xử lý luồng và luồng an toàn của luồng


8

Khi tôi viết mã không đồng bộ bằng async / await, thường là ConfigureAwait(false)để tránh nắm bắt bối cảnh, mã của tôi sẽ chuyển từ một chuỗi luồng này sang luồng tiếp theo sau mỗi chuỗi await. Điều này làm tăng mối quan tâm về an toàn chủ đề. Mã này có an toàn không?

static async Task Main()
{
    int count = 0;
    for (int i = 0; i < 1_000_000; i++)
    {
        Interlocked.Increment(ref count);
        await Task.Yield();
    }
    Console.WriteLine(count == 1_000_000 ? "OK" : "Error");
}

Biến inày không được bảo vệ và được truy cập bởi nhiều luồng xử lý nhóm *. Mặc dù mô hình truy cập là không đồng thời, nhưng về mặt lý thuyết, mỗi luồng sẽ tăng giá trị được lưu trong bộ nhớ cache cục bộ i, dẫn đến hơn 1.000.000 lần lặp. Tôi không thể tạo ra kịch bản này trong thực tế. Mã ở trên luôn in OK trong máy của tôi. Điều này có nghĩa là mã là chủ đề an toàn? Hoặc tôi nên đồng bộ hóa quyền truy cập vào ibiến bằng cách sử dụng a lock?

(* một chuyển đổi luồng xảy ra trung bình cứ sau 2 lần lặp, theo các thử nghiệm của tôi)


1
Tại sao bạn nghĩ ilà lưu trữ trong mỗi chủ đề? Xem điều này SharpLab IL để đào sâu hơn.
AndreasHassing

1
@AndreasHassing Mối quan tâm của tôi được nêu lên bởi các câu lệnh như sau: Trình biên dịch, CLR hoặc CPU có thể đưa ra các tối ưu hóa bộ đệm để các phép gán cho các biến sẽ không hiển thị cho các luồng khác ngay lập tức. Phần 4: Luồng nâng cao
Theodor Zoulias

Câu trả lời:


2

Vấn đề với an toàn chủ đề là về đọc / ghi bộ nhớ. Ngay cả khi điều này có thể tiếp tục trên một luồng khác, không có gì ở đây được thực hiện đồng thời.


Về mặt lý thuyết, một luồng có thể đọc và ghi vào bộ đệm cục bộ thay vì RAM chính, thiếu cách này một bản cập nhật được tạo bởi một luồng khác. Biến ikhông được khai báo volatilecũng như không được bảo vệ bởi khóa, vì vậy theo hiểu biết của tôi về trình biên dịch, Jitter và phần cứng (CPU) đều được phép thực hiện tối ưu hóa như thế này.
Theodor Zoulias

@TheodorZoulias Hoán đổi một chuỗi để tiếp tục tiếp tục không giống như truy cập đồng thời. Trong sharplab được liên kết ở trên, bạn có thể thấy toàn bộ máy trạng thái, đóng gói các địa phương trong các trường riêng được chuyển đến luồng sẽ thực hiện tiếp tục. Chỉ có 1 chủ đề được truy cập itại bất kỳ thời điểm nào.
JohanP

@JohanP trường private int <i>5__2trong máy trạng thái không được khai báo volatile. Mối quan tâm của tôi không phải là về một chủ đề làm gián đoạn một chủ đề khác đang ở giữa cập nhật i. Điều này là không thể xảy ra trong trường hợp này. Mối quan tâm của tôi là về một luồng sử dụng giá trị cũ i, được lưu trong bộ đệm cục bộ của lõi CPU, được đặt ở đó từ vòng lặp trước, thay vì lấy giá trị mới itừ RAM chính. Truy cập bộ đệm cục bộ rẻ hơn so với truy cập RAM chính, do đó, tối ưu hóa BẬT những điều như vậy là có thể (theo những gì tôi đã đọc).
Theodor Zoulias

@TheodorZoulias bạn có cùng mối quan tâm nếu vòng lặp này không có asyncmã trong đó không?
JohanP

2
@TheodorZoulias Chủ đề A chạy, tăng i. Mã truy cập await, Chủ đề A chuyển tất cả trạng thái sang Chủ đề B và quay lại nhóm. Gia số B tăng i. Lượt truy cập await. Thread B sau đó vượt qua tất cả của nhà nước để chủ C, nó đi xuống cái hồ vv vv Tại không có điểm là có truy cập đồng thời tới i, không có chủ đề an toàn cần thiết, nó không quan trọng mà một công tắc đề xảy ra, tất cả các trạng thái cần thiết được chuyển vào luồng mới chạy tiếp tục. Không có trạng thái chia sẻ đó là lý do tại sao bạn không cần đồng bộ hóa.
JohanP

0

Tôi tin rằng bài viết này của Stephen Toub có thể làm sáng tỏ điều này. Đặc biệt, đây là một đoạn có liên quan về những gì xảy ra trong quá trình chuyển đổi ngữ cảnh:

Bất cứ khi nào mã chờ một người chờ đợi mà người phục vụ nói rằng nó chưa hoàn thành (tức là IsCompleted trả về sai), phương thức cần tạm dừng và nó sẽ tiếp tục thông qua việc tiếp tục tắt của người phục vụ. Đây là một trong những điểm không đồng bộ mà tôi đã đề cập trước đó, và do đó, ExecutContext cần phải chuyển từ mã phát hành chờ đợi sang thực thi của đại biểu tiếp tục. Điều đó được xử lý tự động bởi Framework. Khi phương thức async sắp tạm dừng, cơ sở hạ tầng sẽ bắt giữ ExecutContext. Đại biểu được chuyển đến người phục vụ có tham chiếu đến thể hiện ExecutContext này và sẽ sử dụng nó khi tiếp tục phương thức. Đây là những gì cho phép thông tin quan trọng về môi trường xung quanh của Wap được thể hiện bởi ExecutContext để chờ đợi.

Đáng lưu ý rằng YieldAwaitabletrả lại bởi Task.Yield()luôn luôn trở lại false.


Cảm ơn Daniel đã trả lời. Thành thật mà nói, tôi sẽ ngạc nhiên nếu dòng chảy ExecutionContexttừ luồng này sang luồng được phục vụ như là một cơ chế để vô hiệu hóa bộ đệm cục bộ của luồng. Nhưng nó cũng không phải là không thể.
Theodor Zoulias

Có lẽ một chuyên gia như @RaymondChen có thể khẳng định nếu câu trả lời của bạn là đúng hay sai. Tôi tin rằng rất ít người trên thế giới có thể đóng vai trò là nguồn thông tin đáng tin cậy về vấn đề này.
Theodor Zoulias

"Vô hiệu hóa bộ nhớ cache cục bộ của luồng" sẽ ngụ ý rằng khi một luồng thực hiện chuyển đổi ngữ cảnh, bằng cách nào đó nó cũng duy trì bộ đệm dành riêng cho bối cảnh này. Điều đó có nghĩa là dữ liệu được lưu trong bộ nhớ cache này phải được lưu trữ trong một cái gì đó giống với bối cảnh ... nhưng tại sao, khi bối cảnh thực có sẵn cho luồng sẽ phải thực hiện nó? Nó cũng sẽ mang đến vấn đề xác định hai bối cảnh nào là "giống nhau", nhưng chỉ đại diện cho một điểm sau này trong thực thi. Tất nhiên tôi không tự nhận mình là một chuyên gia, chỉ cố gắng suy luận về vấn đề này như một bài tập tinh thần.
Daniel Crha

Ngoài ra, trong trường hợp tôi sai, tôi có thể viện dẫn luật của Cickyham: "Cách tốt nhất để có câu trả lời đúng trên Internet là không đặt câu hỏi, đó là đăng câu trả lời sai."
Daniel Crha

1
Nhưng một bộ đệm phần cứng không phải là chủ đề cụ thể. Trong thực tế, ngay cả mã đơn luồng cũng có thể bị buộc phải tạo ra bằng cách đa nhiệm được ưu tiên từ phía HĐH và nó có thể tiếp tục thực thi trên một bộ xử lý khác (và do đó, bộ đệm L1 và L2 khác nhau). Vô hiệu bộ đệm này không cụ thể asynchoặc await. Vô hiệu hóa bộ đệm trong khi chuyển đổi ngữ cảnh sẽ ảnh hưởng đến mã đơn và đa luồng theo cùng một cách.
Daniel Crha
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.