2010-10-29 68 views
58

Tôi không thấy sự khác biệt giữa các tính năng không đồng bộ mới của C# (và VB), và Task Parallel Library của .NET 4.0. Lấy ví dụ, Eric Lippert của mã from here:Tính năng không đồng bộ của C# 5.0 khác với TPL như thế nào?

async void ArchiveDocuments(List<Url> urls) { 
    Task archive = null; 
    for(int i = 0; i < urls.Count; ++i) { 
     var document = await FetchAsync(urls[i]); 
     if (archive != null) 
      await archive; 
     archive = ArchiveAsync(document); 
    } 
} 

Dường như từ khóa await đang phục vụ hai mục đích khác nhau. Sự xuất hiện đầu tiên (FetchAsync) có nghĩa là, "Nếu giá trị này được sử dụng sau trong phương thức và nhiệm vụ của nó chưa hoàn thành, hãy chờ cho đến khi nó hoàn tất trước khi tiếp tục." Ví dụ thứ hai (archive) có nghĩa là, "Nếu tác vụ này chưa hoàn tất, hãy đợi ngay bây giờ cho đến khi hoàn thành." Nếu tôi sai, hãy sửa tôi.

Không thể dễ dàng viết như thế này?

void ArchiveDocuments(List<Url> urls) { 
    for(int i = 0; i < urls.Count; ++i) { 
     var document = FetchAsync(urls[i]);  // removed await 
     if (archive != null) 
      archive.Wait();      // changed to .Wait() 
     archive = ArchiveAsync(document.Result); // added .Result 
    } 
} 

tôi đã thay thế await đầu tiên với một Task.Result trong đó giá trị thực sự là cần thiết, và lần thứ hai await với Task.Wait(), nơi chờ đợi là thực sự xảy ra. Chức năng là (1) đã được triển khai và (2) ngữ nghĩa gần hơn với những gì thực sự xảy ra trong mã.

Tôi nhận ra rằng phương pháp async được viết lại dưới dạng máy trạng thái, tương tự như trình vòng lặp, nhưng tôi cũng không thấy những lợi ích mang lại. Bất kỳ mã nào yêu cầu một luồng khác để hoạt động (như tải xuống) sẽ vẫn yêu cầu một chuỗi khác và bất kỳ mã nào không (chẳng hạn như đọc từ một tệp) vẫn có thể sử dụng TPL để làm việc chỉ với một chuỗi duy nhất.

Tôi rõ ràng thiếu thứ gì đó rất lớn ở đây; ai có thể giúp tôi hiểu điều này tốt hơn một chút không?

+0

Xem thêm http://stackoverflow.com/questions/3513432/task-parallel-library-replacement-for-backgroundworker ? lq = 1 và http://stackoverflow.com/questions/12414601/async-await-vs-backgroundworker?lq=1 – nawfal

+0

Xem liên kết này có các ví dụ gọn gàng: http://richnewman.wordpress.com/2012/12/03/tutorial-asynchronous-programming-async-and-await-for-beginners/ – nawfal

Trả lời

69

Tôi nghĩ rằng sự hiểu lầm nảy sinh ở đây:

Dường như từ khóa đang chờ đợi được phục vụ hai mục đích khác nhau. Sự xuất hiện đầu tiên (FetchAsync) có vẻ có nghĩa là, "Nếu giá trị này được sử dụng sau này trong phương thức và nhiệm vụ của nó không được hoàn thành, hãy chờ cho đến khi nó hoàn thành trước khi tiếp tục." Ví dụ thứ hai (lưu trữ) có vẻ có nghĩa là, "Nếu nhiệm vụ này chưa được hoàn thành, hãy chờ ngay bây giờ cho đến khi nó hoàn thành." Nếu tôi sai, hãy sửa tôi.

Điều này thực sự hoàn toàn không chính xác. Cả hai đều có cùng ý nghĩa.

Trong trường hợp đầu tiên của bạn:

var document = await FetchAsync(urls[i]); 

gì xảy ra ở đây, là thời gian chạy "Bắt đầu gọi FetchAsync, sau đó trở về điểm thi hiện tại để thread gọi phương pháp này." Không có "chờ đợi" ở đây - thay vào đó, thực thi trả về ngữ cảnh đồng bộ hóa gọi điện thoại và mọi thứ tiếp tục bị khuấy động. Tại một số thời điểm trong tương lai, Nhiệm vụ của FetchAsync sẽ hoàn thành, và tại thời điểm đó, mã này sẽ tiếp tục trên ngữ cảnh đồng bộ hóa của chuỗi gọi, và câu lệnh tiếp theo (gán biến tài liệu) sẽ xảy ra.

Thực hiện sau đó sẽ tiếp tục cho đến khi cuộc gọi đang chờ - lúc đó, điều tương tự sẽ xảy ra - nếu Task<T> (lưu trữ) không hoàn tất, thực thi sẽ được đưa ra ngữ cảnh gọi - nếu không, lưu trữ sẽ bộ. Trong trường hợp thứ hai, mọi thứ rất khác nhau - ở đây, bạn đang chặn một cách rõ ràng, có nghĩa là bối cảnh đồng bộ hóa cuộc gọi sẽ không bao giờ có cơ hội thực thi bất kỳ mã nào cho đến khi toàn bộ phương thức của bạn hoàn tất. Cấp, vẫn còn không đồng bộ, nhưng không đồng bộ được chứa hoàn toàn trong khối mã này - không có mã nào bên ngoài mã được dán này sẽ xảy ra trên chuỗi này cho đến khi tất cả mã của bạn hoàn tất.

0

Các cuộc gọi đến FetchAsync() vẫn sẽ chặn cho đến khi nó hoàn thành (trừ một tuyên bố trong cuộc gọi await?) Điều quan trọng là kiểm soát được trả về cho người gọi (vì phương pháp ArchiveDocuments tự được khai báo là async). Vì vậy, người gọi có thể vui vẻ tiếp tục xử lý giao diện người dùng, phản hồi sự kiện, v.v.

Khi hoàn tất, người gọi sẽ ngắt kết nối vòng lặp. Nó chạm ArchiveAsync() và các khối, nhưng ArchiveAsync() có thể chỉ tạo ra một nhiệm vụ mới, khởi động nó và trả về tác vụ. Điều này cho phép vòng lặp thứ hai bắt đầu, trong khi nhiệm vụ được xử lý.

Vòng lặp thứ hai chạm FetchAsync() và chặn, trả lại quyền kiểm soát cho người gọi. Khi hoàn thành FetchAsync(), nó lại ngắt người gọi để tiếp tục xử lý. Sau đó, truy cập await archive, sẽ trả về quyền kiểm soát cho người gọi cho đến khi Task được tạo trong vòng lặp 1 hoàn tất. Khi công việc đó hoàn tất, người gọi lại bị gián đoạn và vòng lặp thứ hai gọi số ArchiveAsync(), sẽ nhận nhiệm vụ bắt đầu và bắt đầu vòng lặp 3, lặp lại quảng cáo nauseum.

Điều quan trọng là trả lại quyền kiểm soát cho người gọi trong khi những người nâng hạng nặng đang thực hiện.

+1

Lưu ý rằng "những người nâng hạng nặng" có thể không thực thi. Họ có thể không song song chút nào. Họ có thể là đơn vị công việc sẽ được lên lịch trên chuỗi này bất cứ khi nào chuỗi này không hoạt động, ví dụ. Tôi nhìn thấy một * rất nhiều * của sự không đồng bộ của sự không đồng bộ với sự song đối và tôi háo hức để xua tan mọi người về quan niệm đó; không đồng bộ thường về * không * tạo ra nhiều chuỗi công nhân hơn. Điều quan trọng là thực sự trả lại quyền kiểm soát cho người gọi * sau * "những người nâng hạng nặng" đã làm * một cái gì đó * để đảm bảo rằng nhiệm vụ của họ hoàn thành vào một thời điểm nào đó trong tương lai. –

+0

Hiểu những gì bạn đang nói và đồng ý rằng phương thức "chờ đợi" không cần khởi chạy hoặc lên lịch gì cả. Điều gì khiến tôi đưa ra câu lệnh đó là 'archive.Wait()'. Trong ví dụ này, mã chắc chắn được viết để chờ một chuỗi làm điều gì đó ngay lập tức và hoàn thành. Tuyên bố của tôi có nghĩa là trong bối cảnh của ví dụ này, chỉ. –

+0

@JamesKing, liên quan đến "_... không cần khởi chạy hoặc lên lịch bất cứ điều gì ở all_": nó sẽ _eventually_ lên lịch hoặc khởi chạy s/t, phải không? – bvgheluwe

24

Có một sự khác biệt lớn:

Wait() khối, await không chặn.Nếu bạn chạy phiên bản async của ArchiveDocuments() trên luồng GUI, GUI sẽ vẫn đáp ứng trong khi các hoạt động tìm nạp và lưu trữ đang chạy. Nếu bạn sử dụng phiên bản TPL với Wait(), GUI của bạn sẽ bị chặn.

Lưu ý rằng async quản lý để thực hiện điều này mà không cần giới thiệu bất kỳ chủ đề nào - tại điểm của await, điều khiển chỉ đơn giản là được trả về vòng lặp tin nhắn. Khi nhiệm vụ được chờ đợi đã hoàn thành, phần còn lại của phương thức (tiếp tục) được đưa vào vòng lặp tin nhắn và luồng GUI sẽ tiếp tục chạy ArchiveDocuments nơi nó đã dừng lại.

4

Vấn đề ở đây là chữ ký của ArchiveDocuments là gây hiểu lầm. Nó có một sự trở lại rõ ràng của void nhưng thực sự sự trở lại là Task. Để tôi void ngụ ý đồng bộ như không có cách nào để "chờ đợi" cho nó để kết thúc. Hãy xem xét chữ ký thay thế của hàm.

async Task ArchiveDocuments(List<Url> urls) { 
    ... 
} 

Khi tôi viết theo cách này, sự khác biệt rõ ràng hơn nhiều. Hàm ArchiveDocuments không phải là một hàm hoàn thành đồng bộ nhưng sẽ kết thúc sau.

+2

Mã này sẽ có ý nghĩa hơn nếu "lưu trữ" là thành viên hoặc biến tĩnh và không được xác định trong phương thức ... Điều đó đang được nói, void trả về các hàm không đồng bộ là hoàn toàn hợp lệ và có thể chấp nhận được và có "lửa và quên "có nghĩa là, theo tài liệu spec. –

+0

Bạn có đang nói rằng lệnh gọi tới 'ArchiveDocuments()' trông giống như 'Task task = ArchiveDocuments (Danh sách các sốurl);'? Điều đó có vẻ không đúng ... và nếu tôi hiểu đúng, người gọi không thực sự chờ đợi chút nào. Trong thực tế, nếu nó quan tâm đến kết quả của 'ArchiveDocuments()', toàn bộ kịch bản bị xé toạc và không hoạt động. –

+1

@Reed, tôi đồng ý rằng chúng thực sự hợp lệ và chính xác. Tôi chỉ tìm thấy nó rất gây hiểu lầm và có một chút giả định như có lẽ tôi không muốn quên;) – JaredPar

5

Khả năng biến luồng chương trình kiểm soát thành máy trạng thái là điều làm cho các từ khóa mới này bắt đầu. Hãy nghĩ về điều đó là kiểm soát lãi suất, thay vì giá trị.

Khám phá this Channel 9 video của Anders nói về tính năng mới.

23

Anders đun sôi nó xuống một câu trả lời rất ngắn gọn trong cuộc phỏng vấn trực tiếp Kênh 9 ông đã làm. Tôi đặc biệt khuyên bạn nên

Từ khóa Async mới và chờ đợi cho phép bạn phối hợp đồng thời trong các ứng dụng của bạn. Họ không thực sự giới thiệu bất kỳ sự tương tranh nào vào ứng dụng của bạn.

TPL và cụ thể hơn Nhiệm vụ là một chiều bạn có thể sử dụng để thực sự thực hiện thao tác đồng thời. Từ khóa không đồng bộ và chờ đợi mới cho phép bạn soạn các hoạt động đồng thời này theo kiểu "đồng bộ" hoặc "tuyến tính".

Vì vậy, bạn vẫn có thể viết luồng kiểm soát tuyến tính trong các chương trình của mình trong khi máy tính thực tế có thể hoặc có thể không xảy ra đồng thời. Khi tính toán xảy ra đồng thời, chờ và không đồng bộ cho phép bạn soạn các hoạt động này.

+2

Điều này không thực sự * nói * bất cứ điều gì, phải không? Hãy để tôi rephrase: Làm thế nào để trả lời câu hỏi đặt ra? –

+13

Câu hỏi đặt ra là "Tính năng không đồng bộ của C# 5.0 khác với TPL như thế nào?" Câu trả lời của tôi phù hợp với câu hỏi đó IMO –

0

Từ khóa chờ đợi không đưa ra sự đồng thời. Nó giống như từ khóa lợi nhuận, nó cho trình biên dịch để cơ cấu lại mã của bạn thành lambda được điều khiển bởi một máy trạng thái.

Để xem những gì đang chờ đợi mã sẽ trông giống như không có 'await' nhìn thấy liên kết tuyệt vời này: http://blogs.msdn.com/b/windowsappdev/archive/2012/04/24/diving-deep-with-winrt-and-await.aspx

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