2010-07-29 42 views
17

Tôi có một vòng lặp foreach mà tôi đang song song và tôi nhận thấy điều gì đó kỳ lạ. Mã trông giống nhưKết quả tổng kết khác nhau với Parallel.ForEach

double sum = 0.0; 

Parallel.ForEach(myCollection, arg => 
{ 
    sum += ComplicatedFunction(arg); 
}); 

// Use sum variable below 

Khi tôi sử dụng vòng lặp thông thường foreach Tôi nhận được kết quả khác. Có thể có một cái gì đó sâu hơn xuống bên trong ComplicatedFunction nhưng có thể biến số sum đang bị ảnh hưởng không bị ảnh hưởng bởi sự song song?

+1

Xem [ tăng một giá trị đếm bên ngoài phạm vi parallel.foreach ] (http://stackoverflow.com/questions/2394447/increment-a-count-value-outside-parallel-foreach-scope). Về cơ bản, bạn có thể sử dụng [Interlocked] (http://msdn.microsoft.com/en-us/library/55dzx06b.aspx) nếu bạn cần, nhưng tốt hơn nên tránh tác dụng phụ nếu có thể. –

Trả lời

28

có thể biến số tiền đang bị ảnh hưởng không bị ảnh hưởng bởi sự song song?

Có.
Quyền truy cập vào số double không phải là nguyên tử và hoạt động sum += ... không bao giờ là chủ đề an toàn, ngay cả đối với các loại nguyên tử. Vì vậy, bạn có nhiều điều kiện chủng tộc và kết quả là không thể đoán trước.

Bạn có thể sử dụng một cái gì đó như:

double sum = myCollection.AsParallel().Sum(arg => ComplicatedFunction(arg)); 

hoặc, trong một ký hiệu ngắn hơn

double sum = myCollection.AsParallel().Sum(ComplicatedFunction); 
+0

Điều này đã hiệu quả. Đẹp thực hiện sạch bằng cách sử dụng LINQ. –

+0

Mỗi bài đăng gốc, cần phải có 'myCollection' để được an toàn chỉ? –

+0

@Kevin - Không, bất kỳ 'IEnumerable ' sẽ thực hiện. –

4

Nếu bạn nghĩ về điều đó sum += ComplicatedFunction như đang thực sự bao gồm một loạt các hoạt động, nói:

r1 <- Load current value of sum 
r2 <- ComplicatedFunction(...) 
r1 <- r1 + r2 

Vì vậy, bây giờ chúng tôi ngẫu nhiên xen kẽ hai (hoặc nhiều) trường hợp song song của việc này. Một sợi có thể giữ một "giá trị cũ" cũ của tổng mà nó sử dụng để thực hiện tính toán của nó, kết quả của nó ghi lại trên đầu trang của một số phiên bản sửa đổi của tổng. Đó là một điều kiện đua cổ điển, bởi vì một số kết quả đang bị lạc theo một cách không xác định dựa trên cách xen kẽ được thực hiện.

+5

Bạn nói đúng, nhưng tình hình thực tế còn tồi tệ hơn nhiều so với bạn. Nó không chỉ là tải, tính toán và lưu trữ các hoạt động không phải là nguyên tử. Truy cập * bit * trong một đôi không được đảm bảo là nguyên tử! Đặc tả C# chỉ đảm bảo rằng việc truy cập các kiểu số và tham chiếu 32 bit (và nhỏ hơn) là nguyên tử. Đôi là 64 bit và do đó không được đảm bảo là nguyên tử. Chương trình này có thể được thực hiện như: r1 <- tải 32 bit đầu của tổng, r1 <- tải 32 bit dưới cùng của tổng ... nghĩa là các phép toán có thể xen kẽ trong khi * một nửa * số đã được sao chép. –

+0

Được nêu rõ. Tôi đã tìm ra sự đơn giản trong ví dụ này, tôi chỉ giả sử nguyên tử của các hoạt động cơ bản, nhưng rõ ràng như bạn chỉ ra, trường hợp xấu nhất thậm chí còn hoàn toàn kinh khủng hơn. – Gian

11

Giống như các câu trả lời khác được đề cập, cập nhật biến số sum từ nhiều luồng (đó là những gì mà Parallel.ForEach thực hiện) không phải là thao tác an toàn theo luồng. Bản sửa lỗi nhỏ nhặt có được khóa trước khi thực hiện cập nhật sẽ khắc phục sự cố rằng vấn đề.

double sum = 0.0; 
Parallel.ForEach(myCollection, arg => 
{ 
    lock (myCollection) 
    { 
    sum += ComplicatedFunction(arg); 
    } 
}); 

Tuy nhiên, điều đó lại giới thiệu một vấn đề khác. Kể từ khi khóa được mua lại trên mỗi lần lặp lại thì điều đó có nghĩa là việc thực thi mỗi lần lặp lại sẽ được tuần tự một cách hiệu quả. Nói cách khác, sẽ tốt hơn nếu chỉ sử dụng vòng lặp cũ foreach cũ.

Bây giờ, mánh khóe trong việc có được quyền này là phân chia vấn đề trong các chucks riêng biệt và độc lập. May mắn thay đó là siêu dễ thực hiện khi tất cả những gì bạn muốn làm là tổng kết quả của các phép lặp bởi vì phép tính tổng là giao hoán và kết hợp và bởi vì các kết quả trung gian của các phép lặp là độc lập.

Vì vậy, đây là cách bạn làm điều đó.

double sum = 0.0; 
Parallel.ForEach(myCollection, 
    () => // Initializer 
    { 
     return 0D; 
    }, 
    (item, state, subtotal) => // Loop body 
    { 
     return subtotal += ComplicatedFunction(item); 
    }, 
    (subtotal) => // Accumulator 
    { 
     lock (myCollection) 
     { 
      sum += subtotal; 
     } 
    }); 
+1

Tại sao bạn khuyến khích tái tạo một bánh xe tiêu chuẩn? – Novelocrat

+0

@Novelocrat: Xin lỗi, tôi không rõ ràng về những gì bạn đang yêu cầu. Ngoài ra, vì thời gian tôi có thể giả định bạn downvoted câu trả lời này? Nếu vậy thì phần nào của câu trả lời là sai? Tôi đã kiểm tra lại cú pháp mã và chiến lược phân vùng là một cách thực hành khá tốt để thực hiện các hoạt động 'Parallel.For', nhưng có lẽ tôi đã bỏ lỡ một thứ gì đó bắt gặp ánh mắt của bạn. –

+0

Điều mà bạn thực hiện mô tả chính xác là chức năng thư viện mà câu trả lời của Henk mô tả. Hơn nữa, tôi mạnh mẽ nghi ngờ rằng việc giảm tổng phụ của mỗi luồng ('Accumulator') trong việc thực hiện thư viện hiệu quả hơn nhiều so với phương pháp dựa trên khóa của bạn. – Novelocrat

Các vấn đề liên quan