Sự khác biệt giữa lập trình không đồng bộ và đa luồng là gì?


234

Tôi nghĩ rằng về cơ bản chúng giống nhau - viết các chương trình phân chia nhiệm vụ giữa các bộ xử lý (trên các máy có bộ xử lý 2+). Sau đó, tôi đang đọc này , trong đó nói:

Các phương thức Async được dự định là các hoạt động không chặn. Biểu thức chờ trong phương thức async không chặn luồng hiện tại trong khi tác vụ được chờ đang chạy. Thay vào đó, biểu thức đăng ký phần còn lại của phương thức dưới dạng tiếp tục và trả lại quyền điều khiển cho người gọi phương thức async.

Các từ khóa không đồng bộ và chờ đợi không gây ra các chủ đề bổ sung được tạo. Các phương thức async không yêu cầu đa luồng vì một phương thức async không chạy trên luồng của chính nó. Phương thức chạy trên bối cảnh đồng bộ hóa hiện tại và chỉ sử dụng thời gian trên luồng khi phương thức được kích hoạt. Bạn có thể sử dụng Task.Run để di chuyển công việc gắn với CPU sang luồng nền, nhưng luồng nền không giúp ích gì cho quá trình chỉ chờ kết quả khả dụng.

và tôi tự hỏi liệu ai đó có thể dịch nó sang tiếng Anh cho tôi không. Nó dường như rút ra sự khác biệt giữa tính không đồng bộ (đó có phải là một từ không?) Và phân luồng và ngụ ý rằng bạn có thể có một chương trình có các tác vụ không đồng bộ nhưng không có đa luồng.

Bây giờ tôi hiểu ý tưởng về các tác vụ không đồng bộ như ví dụ trên pg. C # của Jon Skeet C # In Depth, Ấn bản thứ ba

async void DisplayWebsiteLength ( object sender, EventArgs e )
{
    label.Text = "Fetching ...";
    using ( HttpClient client = new HttpClient() )
    {
        Task<string> task = client.GetStringAsync("http://csharpindepth.com");
        string text = await task;
        label.Text = text.Length.ToString();
    }
}

Các asyncphương tiện từ khóa " chức năng này, bất cứ khi nào nó được gọi là, sẽ không được gọi là trong bối cảnh, trong đó hoàn thành là cần thiết cho tất cả mọi thứ sau khi cuộc gọi của nó được gọi."

Nói cách khác, viết nó ở giữa một số nhiệm vụ

int x = 5; 
DisplayWebsiteLength();
double y = Math.Pow((double)x,2000.0);

, vì DisplayWebsiteLength()không có gì để làm với xhoặc y, sẽ khiến DisplayWebsiteLength()được thực thi "trong nền", như

                processor 1                |      processor 2
-------------------------------------------------------------------
int x = 5;                                 |  DisplayWebsiteLength()
double y = Math.Pow((double)x,2000.0);     |

Rõ ràng đó là một ví dụ ngu ngốc, nhưng tôi đúng hay tôi hoàn toàn bối rối hay sao?

(Ngoài ra, tôi bối rối về lý do senderekhông bao giờ được sử dụng trong phần thân của chức năng trên.)


13
Đây là một lời giải thích hay: blog.stephencleary.com/2013/11/there-is-no-thread.html
Jakub Lortz

sendeređang gợi ý rằng đây thực sự là một công cụ xử lý sự kiện - gần như là nơi duy nhất async voidđược mong muốn. Rất có thể, điều này được gọi bằng một lần bấm nút hoặc đại loại như thế - kết quả là hành động này xảy ra hoàn toàn không đồng bộ đối với phần còn lại của ứng dụng. Nhưng đó vẫn là tất cả trên một luồng - luồng UI (với một chút thời gian nhỏ trên luồng IOCP đăng cuộc gọi lại lên luồng UI).
Luaan


3
Một lưu ý rất quan trọng đối với DisplayWebsiteLengthmẫu mã: Bạn không nên sử dụng HttpClienttrong một usingcâu lệnh - Trong một tải nặng, mã có thể làm cạn kiệt số lượng ổ cắm có sẵn dẫn đến lỗi SocketException. Thông tin thêm về Khởi tạo không đúng cách .
Gan

1
@JakubLortz Tôi không biết bài báo thực sự là của ai. Không dành cho người mới bắt đầu, vì nó đòi hỏi kiến ​​thức tốt về các luồng, ngắt, các thứ liên quan đến CPU, v.v. Không dành cho người dùng nâng cao, vì đối với họ mọi thứ đã rõ ràng. Tôi chắc chắn rằng nó sẽ không giúp bất cứ ai hiểu tất cả những gì về nó - mức độ trừu tượng cao.
Loreno

Câu trả lời:


589

Hiểu lầm của bạn là vô cùng phổ biến. Nhiều người được dạy rằng đa luồng và không đồng bộ là cùng một thứ, nhưng thực tế không phải vậy.

Một sự tương tự thường giúp. Bạn đang nấu ăn trong một nhà hàng. Một đơn đặt hàng đến cho trứng và bánh mì nướng.

  • Đồng bộ: bạn nấu trứng, sau đó bạn nướng bánh mì nướng.
  • Không đồng bộ, đơn luồng: bạn bắt đầu nấu trứng và đặt hẹn giờ. Bạn bắt đầu nấu bánh mì nướng và đặt hẹn giờ. Trong khi cả hai đang nấu ăn, bạn dọn dẹp nhà bếp. Khi bộ hẹn giờ tắt, bạn lấy trứng ra khỏi bếp và nướng bánh mì ra khỏi lò nướng bánh và phục vụ chúng.
  • Không đồng bộ, đa luồng: bạn thuê thêm hai đầu bếp, một để nấu trứng và một để nấu bánh mì nướng. Bây giờ bạn có vấn đề phối hợp các đầu bếp để họ không xung đột với nhau trong bếp khi chia sẻ tài nguyên. Và bạn phải trả tiền cho họ.

Bây giờ nó có ý nghĩa rằng đa luồng chỉ là một loại không đồng bộ? Threading là về công nhân; không đồng bộ là về các nhiệm vụ . Trong quy trình làm việc đa luồng, bạn giao nhiệm vụ cho công nhân. Trong các luồng công việc đơn luồng không đồng bộ, bạn có một biểu đồ các tác vụ trong đó một số tác vụ phụ thuộc vào kết quả của các tác vụ khác; khi mỗi tác vụ hoàn thành, nó gọi mã lập lịch cho tác vụ tiếp theo có thể chạy, đưa ra kết quả của tác vụ vừa hoàn thành. Nhưng bạn (hy vọng) chỉ cần một công nhân để thực hiện tất cả các nhiệm vụ, không phải một công nhân cho mỗi nhiệm vụ.

Nó sẽ giúp nhận ra rằng nhiều tác vụ không bị ràng buộc bởi bộ xử lý. Đối với các tác vụ giới hạn bộ xử lý, nên thuê bao nhiêu công nhân (luồng) vì có bộ xử lý, gán một tác vụ cho mỗi công nhân, gán một bộ xử lý cho mỗi công nhân và để mỗi bộ xử lý thực hiện công việc khác ngoài việc tính toán kết quả như càng nhanh càng tốt Nhưng đối với các tác vụ không chờ đợi trên bộ xử lý, bạn không cần chỉ định một công nhân. Bạn chỉ cần đợi tin nhắn đến là kết quả có sẵn và làm một cái gì đó khác trong khi bạn chờ đợi . Khi tin nhắn đó đến, bạn có thể lên lịch tiếp tục nhiệm vụ đã hoàn thành như là điều tiếp theo trong danh sách việc cần làm của bạn để kiểm tra.

Vì vậy, hãy nhìn vào ví dụ của Jon chi tiết hơn. Chuyện gì xảy ra

  • Ai đó gọi DisplayWebSiteLpm. WHO? Chúng tôi không quan tâm.
  • Nó đặt nhãn, tạo một ứng dụng khách và yêu cầu khách hàng tìm nạp một cái gì đó. Máy khách trả về một đối tượng đại diện cho nhiệm vụ tìm nạp một cái gì đó. Nhiệm vụ đó đang được tiến hành.
  • Có phải trong tiến trình trên một chủ đề khác? Chắc là không. Đọc bài viết của Stephen về lý do tại sao không có chủ đề.
  • Bây giờ chúng tôi chờ đợi nhiệm vụ. Chuyện gì xảy ra Chúng tôi kiểm tra xem liệu nhiệm vụ đã hoàn thành giữa thời gian chúng tôi tạo ra nó và chúng tôi chờ đợi nó. Nếu có, sau đó chúng tôi lấy kết quả và tiếp tục chạy. Hãy giả sử nó chưa hoàn thành. Chúng tôi đăng ký phần còn lại của phương pháp này là sự tiếp tục của nhiệm vụ đó và trở lại .
  • Bây giờ kiểm soát đã trở lại cho người gọi. Nó làm gì? Bất cứ điều gì nó muốn.
  • Bây giờ giả sử nhiệm vụ hoàn thành. Làm thế nào mà nó làm điều đó? Có thể nó đang chạy trên một luồng khác, hoặc có thể người gọi mà chúng ta vừa quay lại cho phép nó chạy để hoàn thành trên luồng hiện tại. Bất kể, bây giờ chúng tôi có một nhiệm vụ hoàn thành.
  • Tác vụ đã hoàn thành yêu cầu luồng chính xác - một lần nữa, có thể là luồng duy nhất - để chạy tiếp tục tác vụ.
  • Kiểm soát chuyển ngay lập tức trở lại phương thức chúng ta vừa rời khỏi điểm chờ đợi. Bây giờ có một kết quả có sẵn vì vậy chúng tôi có thể gán textvà chạy các phần còn lại của phương pháp này.

Nó giống như trong sự tương tự của tôi. Ai đó hỏi bạn một tài liệu. Bạn gửi đi trong thư cho tài liệu, và tiếp tục làm công việc khác. Khi nó đến trong thư bạn được báo hiệu và khi bạn cảm thấy thích nó, bạn sẽ thực hiện phần còn lại của quy trình - mở phong bì, trả phí giao hàng, bất cứ điều gì. Bạn không cần phải thuê một công nhân khác để làm tất cả điều đó cho bạn.


8
@ user5648283: Phần cứng là cấp độ sai khi nghĩ về các tác vụ. Một tác vụ chỉ đơn giản là một đối tượng (1) thể hiện rằng một giá trị sẽ có sẵn trong tương lai và (2) có thể chạy mã (trên luồng chính xác) khi giá trị đó khả dụng . Làm thế nào bất kỳ nhiệm vụ cá nhân có được kết quả trong tương lai là tùy thuộc vào nó. Một số sẽ sử dụng phần cứng đặc biệt như "đĩa" và "card mạng" để làm điều đó; một số sẽ sử dụng phần cứng như CPU.
Eric Lippert

13
@ user5648283: Một lần nữa, hãy nghĩ về sự tương tự của tôi. Khi ai đó yêu cầu bạn nấu trứng và bánh mì nướng, bạn sử dụng phần cứng đặc biệt - bếp và máy nướng bánh mì - và bạn có thể làm sạch bếp trong khi phần cứng đang hoạt động. Nếu ai đó hỏi bạn về trứng, bánh mì nướng và phê bình ban đầu của bộ phim Hobbit cuối cùng, bạn có thể viết đánh giá của mình trong khi trứng và bánh mì nướng đang nấu, nhưng bạn không cần sử dụng phần cứng cho điều đó.
Eric Lippert

9
@ user5648283: Bây giờ đối với câu hỏi của bạn về "sắp xếp lại mã", hãy xem xét điều này. Giả sử bạn có một phương thức P có lợi tức lợi tức và phương thức Q thực hiện một nghiên cứu về kết quả của P. Bước qua mã. Bạn sẽ thấy rằng chúng tôi chạy một chút Q sau đó một chút P sau đó một chút Q ... Bạn có hiểu ý nghĩa của điều đó không? chờ đợi về cơ bản là lợi nhuận trở lại trong trang phục ưa thích . Bây giờ thì rõ ràng hơn?
Eric Lippert

10
Máy nướng bánh mì là phần cứng. Phần cứng không cần một chủ đề để phục vụ nó; đĩa và card mạng và không có gì chạy ở mức thấp hơn nhiều so với các luồng của hệ điều hành.
Eric Lippert

5
@ShivprasadKoirala: Điều đó hoàn toàn không đúng chút nào . Nếu bạn tin điều đó, thì bạn có một số niềm tin rất sai lầm về sự không đồng bộ . Toàn bộ điểm không đồng bộ trong C # là nó không tạo ra một luồng.
Eric Lippert

27

Javascript trong trình duyệt là một ví dụ tuyệt vời về chương trình không đồng bộ không có luồng.

Bạn không phải lo lắng về việc nhiều đoạn mã chạm vào cùng một đối tượng cùng một lúc: mỗi chức năng sẽ kết thúc chạy trước khi bất kỳ javascript nào khác được phép chạy trên trang.

Tuy nhiên, khi thực hiện một cái gì đó như yêu cầu AJAX, không có mã nào chạy cả, vì vậy các javascript khác có thể đáp ứng những thứ như sự kiện nhấp chuột cho đến khi yêu cầu đó quay trở lại và gọi lại cuộc gọi lại liên quan đến nó. Nếu một trong những trình xử lý sự kiện khác này vẫn chạy khi yêu cầu AJAX trở lại, trình xử lý của nó sẽ không được gọi cho đến khi chúng được thực hiện. Chỉ có một "luồng" JavaScript đang chạy, mặc dù bạn có thể tạm dừng hiệu quả việc bạn đang làm cho đến khi bạn có thông tin bạn cần.

Trong các ứng dụng C #, điều tương tự xảy ra bất cứ khi nào bạn xử lý các thành phần UI - bạn chỉ được phép tương tác với các thành phần UI khi bạn ở trên luồng UI. Nếu người dùng nhấp vào nút và bạn muốn phản hồi bằng cách đọc một tệp lớn từ đĩa, một lập trình viên thiếu kinh nghiệm có thể mắc lỗi đọc tệp trong chính trình xử lý sự kiện nhấp chuột, điều này sẽ khiến ứng dụng "đóng băng" cho đến khi tải xong tệp vì nó không được phép phản hồi thêm bất kỳ nhấp chuột, di chuột hoặc bất kỳ sự kiện nào khác liên quan đến giao diện người dùng cho đến khi chủ đề đó được giải phóng.

Một lập trình viên tùy chọn có thể sử dụng để tránh vấn đề này là tạo một luồng mới để tải tệp, sau đó nói với mã của luồng đó rằng khi tệp được tải, nó cần chạy lại mã còn lại trên luồng UI để có thể cập nhật các thành phần UI dựa trên những gì nó tìm thấy trong tập tin Cho đến gần đây, cách tiếp cận này rất phổ biến vì đó là điều mà các thư viện và ngôn ngữ C # làm cho dễ dàng, nhưng về cơ bản nó phức tạp hơn so với nó.

Nếu bạn nghĩ về những gì CPU đang làm khi đọc một tệp ở cấp độ của Phần cứng và Hệ điều hành, thì về cơ bản, nó sẽ đưa ra một hướng dẫn để đọc các phần dữ liệu từ đĩa vào bộ nhớ và đánh vào hệ điều hành bằng một "ngắt" "Khi đọc xong. Nói cách khác, đọc từ đĩa (hoặc bất kỳ I / O thực sự nào) là một hoạt động không đồng bộ vốn có . Khái niệm về một luồng đang chờ I / O hoàn thành là một sự trừu tượng mà các nhà phát triển thư viện đã tạo ra để giúp lập trình dễ dàng hơn. Nó không cần thiết.

Bây giờ, hầu hết các hoạt động I / O trong .NET đều có một ...Async()phương thức tương ứng mà bạn có thể gọi, nó trả về Taskgần như ngay lập tức. Bạn có thể thêm các cuộc gọi lại vào đây Taskđể chỉ định mã mà bạn muốn chạy khi hoạt động không đồng bộ hoàn tất. Bạn cũng có thể chỉ định luồng nào bạn muốn mã đó chạy trên đó và bạn có thể cung cấp mã thông báo mà hoạt động không đồng bộ có thể kiểm tra theo thời gian để xem liệu bạn có quyết định hủy tác vụ không đồng bộ hay không, cho nó cơ hội dừng công việc của nó một cách nhanh chóng và duyên dáng

Cho đến khi các async/awaittừ khóa được thêm vào, C # rõ ràng hơn nhiều về cách mã gọi lại được gọi, bởi vì các cuộc gọi lại đó ở dạng đại biểu mà bạn liên kết với tác vụ. Để vẫn mang lại cho bạn lợi ích của việc sử dụng ...Async()thao tác, đồng thời tránh sự phức tạp trong mã, async/awaittrừu tượng hóa việc tạo ra các đại biểu đó. Nhưng chúng vẫn còn đó trong mã được biên dịch.

Vì vậy, bạn có thể yêu cầu trình xử lý sự kiện UI của mình xử lý awaitI / O, giải phóng luồng UI để thực hiện các thao tác khác và ít nhiều tự động quay lại luồng UI sau khi bạn đọc xong tệp - mà không cần phải đọc tạo một chủ đề mới.


Chỉ có một "luồng" JavaScript đang chạy - không còn đúng với Công nhân web .
oleksii

6
@oleksii: Điều đó đúng về mặt kỹ thuật, nhưng tôi sẽ không đi sâu vào điều đó bởi vì API của Công nhân web không đồng bộ và Công nhân web không được phép tác động trực tiếp đến các giá trị javascript hoặc DOM trên trang web mà họ đã gọi từ, có nghĩa là đoạn thứ hai quan trọng của câu trả lời này vẫn đúng. Từ quan điểm của lập trình viên, có rất ít sự khác biệt giữa việc gọi Công nhân web và yêu cầu AJAX.
StriplingWar Warrior
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.