Việc lặp lại trên một mảng với vòng lặp for có phải là một hoạt động an toàn của luồng trong C # không? Điều gì về việc lặp lại một <T> IEn với một vòng lặp foreach?


8

Dựa trên sự hiểu biết của tôi, được đưa ra một mảng C #, hành động lặp lại trên mảng đồng thời từ nhiều luồng một hoạt động an toàn của luồng.

Bằng cách lặp qua mảng tôi có nghĩa là đọc tất cả các vị trí bên trong mảng bằng một vòng lặp cũ đơn giảnfor . Mỗi luồng chỉ đơn giản là đọc nội dung của một vị trí bộ nhớ bên trong mảng, không ai viết bất cứ điều gì vì vậy tất cả các luồng đều đọc cùng một thứ một cách nhất quán.

Đây là một đoạn mã làm những gì tôi đã viết ở trên:

public class UselessService 
{
   private static readonly string[] Names = new [] { "bob", "alice" };

   public List<int> DoSomethingUseless()
   {
      var temp = new List<int>();

      for (int i = 0; i < Names.Length; i++) 
      {
        temp.Add(Names[i].Length * 2);
      }

      return temp;
   }
}

Vì vậy, sự hiểu biết của tôi là phương thức DoSomethingUseless này là luồng an toànkhông cần thay thế string[]bằng loại an toàn luồng ( ImmutableArray<string>ví dụ như).

Tôi có đúng không?

Bây giờ hãy giả sử rằng chúng ta có một ví dụ về IEnumerable<T>. Chúng ta không biết đối tượng cơ bản là gì, chúng ta chỉ biết rằng chúng ta có một đối tượng đang thực hiện IEnumerable<T>, vì vậy chúng ta có thể lặp lại nó bằng cách sử dụng foreachvòng lặp.

Dựa trên sự hiểu biết của tôi, trong kịch bản này, không có gì đảm bảo rằng việc lặp lại đối tượng này từ nhiều luồng đồng thời là một hoạt động an toàn của luồng. Nói cách khác, hoàn toàn có thể lặp lại qua IEnumerable<T>thể hiện từ các luồng khác nhau cùng một lúc sẽ phá vỡ trạng thái bên trong của đối tượng, để nó bị hỏng.

Tôi có đúng về điểm này không?

Vậy còn IEnumerable<T> thực hiện Arraylớp học thì sao? Nó có an toàn không?

Đặt một cách khác, chủ đề mã sau đây có an toàn không? (đây chính xác là mã giống như trên, nhưng bây giờ mảng được lặp lại bằng cách sử dụng foreachvòng lặp thay vì forvòng lặp)

public class UselessService 
{
   private static readonly string[] Names = new [] { "bob", "alice" };

   public List<int> DoSomethingUseless()
   {
      var temp = new List<int>();

      foreach (var name in Names) 
      {
        temp.Add(name.Length * 2);
      }

      return temp;
   }
}

Có bất kỳ tham chiếu nào nêu rõ các IEnumerable<T>triển khai trong thư viện lớp cơ sở .NET thực sự là luồng an toàn không?


4
@Christopher: Re: Cảm nhận của bạn đầu tiên: trong kịch bản Enrico, nơi một mảng đã được chỉ đọc , và không lưu trữ các tài liệu tham khảo mảng hay các yếu tố của mảng đang được biến đổi, một kịch bản nhiều độc giả nên được an toàn mà không cần thêm rào cản.
Eric Lippert

3
@Christopher "Không, lặp đi lặp lại trên một mảng không phải là lưu chuỗi." - Lặp lại trên một mảng (tức là Array) sẽ là luồng an toàn miễn là tất cả các luồng chỉ đọc nó. Tương tự với List<T>, và có lẽ mọi bộ sưu tập do Microsoft viết. Nhưng có thể làm cho riêng bạn IEnumerablemà không có chủ đề an toàn trong điều tra viên. Do đó, nó không đảm bảo rằng mọi thứ thực hiện IEnumerablesẽ là luồng an toàn.
Gabriel Luci

6
@Christopher: Re: bình luận thứ hai của bạn: không, foreachhoạt động với nhiều hơn chỉ là điều tra viên. Trình biên dịch đủ thông minh để biết rằng nếu nó có thông tin kiểu tĩnh chỉ ra rằng bộ sưu tập là một mảng, nó bỏ qua việc tạo ra liệt kê và chỉ truy cập trực tiếp vào mảng như nó là một forvòng lặp. Tương tự, nếu bộ sưu tập hỗ trợ triển khai tùy chỉnhGetEnumerator , thì việc thực hiện đó được sử dụng thay vì gọi IEnumerable.GetEnumerator. Quan niệm rằng foreachchỉ có buôn bán IEnumerable/IEnumeratorlà sai.
Eric Lippert

3
@Christopher: Re: bình luận thứ ba của bạn: hãy cẩn thận. Mặc dù có thể có một thứ gọi là "cũ", nhưng khái niệm rằng do đó cũng có một thứ gọi là "tươi" là không chính xác. volatilekhông đảm bảo rằng bạn có được bản cập nhật mới nhất có thể cho một biến vì C # cho phép các luồng khác nhau quan sát các thứ tự đọc và ghi khác nhau, và do đó khái niệm "mới nhất" không được định nghĩa toàn cầu . Thay vì lý luận về sự mới mẻ, lý do về những hạn chế volatile về cách viết có thể được sắp xếp lại.
Eric Lippert

6
@EnricoMassone: Tôi không cố gắng trả lời bất kỳ câu hỏi nào của bạn nhưng tôi lưu ý rằng các câu hỏi của bạn cho thấy rằng bạn muốn làm một việc gì đó rất mạo hiểm, cụ thể là viết một giải pháp khóa thấp hoặc không khóa chia sẻ bộ nhớ qua các luồng. Tôi sẽ không cố làm như vậy ngoại trừ trong trường hợp thực sự không có lựa chọn nào khác đáp ứng mục tiêu hiệu suất của tôi và trong trường hợp tôi cảm thấy tự tin để phân tích kỹ lưỡng và chính xác tất cả các thứ tự có thể có của tất cả các lần đọc và viết trong chương trình . Vì bạn đang hỏi những câu hỏi này, tôi nghĩ bạn cảm thấy không tự tin hơn tôi sẽ thấy thoải mái.
Eric Lippert

Câu trả lời:


2

Việc lặp lại trên một mảng với vòng lặp for có phải là một hoạt động an toàn của luồng trong C # không?

Nếu bạn đang nghiêm túc nói về đọc từ nhiều chủ đề , đó sẽ là chủ đề an toàn cho ArrayList<T>và chỉ là về tất cả các bộ sưu tập được viết bởi Microsoft, bất kể nếu bạn đang sử dụng một forhoặc foreachvòng lặp. Đặc biệt trong ví dụ bạn có:

var temp = new List<int>();

foreach (var name in Names)
{
  temp.Add(name.Length * 2);
}

Bạn có thể làm điều đó qua nhiều chủ đề như bạn muốn. Tất cả họ sẽ đọc cùng một giá trị từ Nameshạnh phúc.

Nếu bạn viết cho nó từ một chủ đề khác (đây không phải là câu hỏi của bạn, nhưng nó đáng chú ý)

Lặp lại qua một Arrayhoặc List<T>với mộtfor vòng lặp, nó sẽ tiếp tục đọc và nó sẽ vui vẻ đọc các giá trị thay đổi khi bạn bắt gặp chúng.

Lặp lại với một foreachvòng lặp, sau đó nó phụ thuộc vào việc thực hiện. Nếu một giá trị Arraythay đổi một phần thông qua một foreachvòng lặp, nó sẽ tiếp tục liệt kê và cung cấp cho bạn các giá trị thay đổi.

Với List<T>, nó phụ thuộc vào những gì bạn coi là "chủ đề an toàn". Nếu bạn quan tâm hơn đến việc đọc dữ liệu chính xác, thì đó là loại "an toàn" vì nó sẽ đưa ra một phép liệt kê ngoại lệ và cho bạn biết rằng bộ sưu tập đã thay đổi. Nhưng nếu bạn coi việc ném một ngoại lệ là không an toàn, thì nó không an toàn.

Nhưng điều đáng chú ý là đây là một quyết định thiết kế List<T>, có mã rõ ràng tìm kiếm các thay đổi và đưa ra một ngoại lệ. Quyết định thiết kế đưa chúng ta đến điểm tiếp theo:

Chúng ta có thể cho rằng mọi bộ sưu tập thực hiện IEnumerableđều an toàn để đọc trên nhiều luồng không?

Trong hầu hết các trường hợp sẽ như vậy, nhưng đọc an toàn chủ đề không được đảm bảo. Lý do là bởi vì mỗi IEnumerableyêu cầu thực hiện IEnumerator, quyết định làm thế nào để đi qua các mục trong bộ sưu tập. Và cũng giống như bất kỳ lớp học nào, bạn có thể làm bất cứ điều gì bạn muốn trong đó, bao gồm cả những thứ không an toàn như:

  • Sử dụng biến tĩnh
  • Sử dụng bộ đệm chia sẻ để đọc giá trị
  • Không thực hiện bất kỳ nỗ lực nào để xử lý các trường hợp bộ sưu tập thay đổi giữa liệt kê
  • Vân vân.

Bạn thậm chí có thể làm một cái gì đó kỳ lạ như GetEnumerator()trả lại cùng một ví dụ của điều tra viên của bạn mỗi khi nó được gọi. Điều đó thực sự có thể làm cho một số kết quả không thể đoán trước.

Tôi xem xét một cái gì đó không phải là chủ đề an toàn nếu nó có thể dẫn đến kết quả không thể đoán trước. Bất kỳ điều nào trong số đó có thể gây ra kết quả không thể đoán trước.

Bạn có thể thấy các mã nguồn cho Enumeratorrằng List<T>sử dụng , vì vậy bạn có thể thấy rằng nó không làm bất kỳ những thứ kỳ lạ, mà nói với bạn rằng liệt kê List<T>từ nhiều chủ đề là an toàn.


về cơ bản nếu ai đó cung cấp cho bạn một ví dụ về IEnumerable và điều duy nhất bạn biết về nó là nó triển khai IEnumerable, thì sẽ không an toàn khi liệt kê đối tượng đó từ các luồng khác nhau. Mặt khác, nếu bạn có một thể hiện của lớp BCL, giả sử là một thể hiện của Danh sách <chuỗi>, bạn có thể liệt kê một cách an toàn đối tượng từ nhiều luồng (ngay cả khi chính danh sách <chuỗi> không phải là luồng an toàn trong tất cả các thành viên của nó, bạn có thể chắc chắn rằng việc triển khai giao diện IEnumerable là luồng an toàn).
Enrico Massone

1
tất cả các cuộc thảo luận về sự an toàn của việc liệt kê đồng thời các bộ sưu tập BCL đều dựa trên một giả định: chúng ta có thể đọc mã nguồn được cung cấp bởi microsoft và cho đến ngày nay chúng ta không biết bất kỳ ví dụ nào về lớp BCL không an toàn liên quan đến đồng thời liệt kê. Chúng tôi về cơ bản dựa vào một chi tiết thực hiện. Trừ khi một cái gì đó không được ghi lại rõ ràng, làm giả định không phải là lựa chọn an toàn nhất. Vì vậy, sự lựa chọn an toàn nhất có lẽ là tránh những rắc rối và sử dụng thứ gì đó giống như một chuỗi bất biến <chuỗi> mà chúng ta biết chắc chắn là an toàn cho luồng vì tính bất biến của nó.
Enrico Massone

1
"Sự lựa chọn an toàn nhất có lẽ là tránh những rắc rối và sử dụng thứ gì đó như ImmutableArray <string>" - Nếu bạn có quyền kiểm soát loại bộ sưu tập và bạn biết rằng bạn sẽ chỉ đọc qua các chủ đề, List<T>hoặc Arraylà an toàn như ImmutableArray<string>, vì chúng ta có thể chứng minh đó là những chủ đề an toàn để đọc. Tôi sẽ chỉ xem xét các lựa chọn khác nếu bạn có kế hoạch đọc và viết trên nhiều chủ đề. Bạn có thể sử dụng một bộ sưu tập thích hợp từ System.Collections.Concurrent.
Gabriel Luci

1

Để khẳng định rằng mã của bạn là an toàn cho chuỗi có nghĩa là chúng tôi phải chấp nhận lời nói của bạn rằng không có mã nào bên trong UselessServicesẽ cố gắng thay thế đồng thời nội dung của Namesmảng bằng một cái gì đó như "tom" and "jerry"hoặc (nham hiểm hơn) null and null. Mặt khác, việc sử dụng ImmutableArray<string>sẽ đảm bảo rằng mã an toàn cho luồng và mọi người có thể yên tâm về điều đó chỉ bằng cách xem loại trường chỉ đọc tĩnh, mà không phải kiểm tra cẩn thận phần còn lại của mã.

Bạn có thể thấy những nhận xét thú vị này từ mã nguồn của ImmutableArray<T>, liên quan đến một số chi tiết triển khai của cấu trúc này:

Một mảng chỉ đọc với thời gian tra cứu có thể lập chỉ mục O (1).

Loại này có hợp đồng được chứng minh là chính xác một trường loại tham chiếu có kích thước. System.Collections.Immutable.ImmutableInterlockedLớp học riêng của chúng tôi phụ thuộc vào nó, cũng như những người khác ở bên ngoài.

THÔNG BÁO QUAN TRỌNG CHO NGƯỜI CHÍNH VÀ ĐÁNH GIÁ:

Loại này nên được an toàn chủ đề. Là một cấu trúc, nó không thể bảo vệ các trường của chính nó khỏi bị thay đổi từ một luồng trong khi các thành viên của nó đang thực thi trên các luồng khác vì các cấu trúc có thể thay đổi tại chỗ chỉ bằng cách gán lại trường chứa cấu trúc này. Do đó, điều cực kỳ quan trọng là mọi thành viên chỉ nên hủy bỏ quy định thisONCE. Nếu một thành viên cần tham chiếu trường mảng, điều đó được tính là một sự trung thành của this. Gọi các thành viên thể hiện khác (thuộc tính hoặc phương thức) cũng được tính là hội nghị this. Bất kỳ thành viên nào cần sử dụng thisnhiều lần thay vào đó phải gánthisthay vào đó là một biến cục bộ và sử dụng nó cho phần còn lại của mã. Điều này sao chép một cách hiệu quả một trường trong cấu trúc thành một biến cục bộ để nó được cách ly với các luồng khác.

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.