Nếu async-await không tạo ra bất kỳ chủ đề bổ sung nào, thì làm thế nào để ứng dụng đáp ứng?


241

Hết lần này đến lần khác, tôi thấy nó nói rằng sử dụng async- awaitkhông tạo thêm bất kỳ chủ đề nào. Điều đó không có ý nghĩa bởi vì cách duy nhất mà máy tính có thể xuất hiện là làm nhiều việc 1 lần là

  • Thực tế làm nhiều việc 1 lần (thực hiện song song, sử dụng nhiều bộ xử lý)
  • Mô phỏng nó bằng cách lên lịch các tác vụ và chuyển đổi giữa chúng (làm một chút A, một chút B, một chút A, v.v.)

Vì vậy, nếu async- awaitkhông phải trong số đó, thì làm thế nào để ứng dụng có thể phản hồi? Nếu chỉ có 1 luồng, thì việc gọi bất kỳ phương thức nào có nghĩa là đợi phương thức hoàn thành trước khi thực hiện bất kỳ điều gì khác và các phương thức bên trong phương thức đó phải đợi kết quả trước khi tiếp tục, v.v.


17
Các tác vụ IO không bị ràng buộc CPU và do đó không yêu cầu một luồng. Điểm chính của async là không chặn các luồng trong các tác vụ bị ràng buộc IO.
juharr

24
@jdweng: Không, hoàn toàn không. Ngay cả khi nó tạo ra các luồng mới , điều đó rất khác so với việc tạo ra một quy trình mới.
Jon Skeet

8
Nếu bạn hiểu lập trình không đồng bộ dựa trên cuộc gọi lại, thì bạn hiểu cách thức await/ asynchoạt động mà không tạo ra bất kỳ chủ đề nào.
user253751

6
Nó không chính xác làm cho ứng dụng phản ứng nhanh hơn, nhưng nó không khuyến khích bạn chặn các luồng của bạn, đây là nguyên nhân phổ biến của các ứng dụng không phản hồi.
Owen

6
@RubberDuck: Có, nó có thể sử dụng một luồng từ nhóm luồng để tiếp tục. Nhưng nó không bắt đầu một luồng theo cách mà OP tưởng tượng ở đây - không giống như nó nói "Hãy thực hiện phương pháp thông thường này, bây giờ chạy nó trong một luồng riêng biệt - ở đó, đó là không đồng bộ." Nó tinh tế hơn thế nhiều.
Jon Skeet

Câu trả lời:


299

Trên thực tế, async / await không phải là điều kỳ diệu. Toàn bộ chủ đề khá rộng nhưng để có câu trả lời đủ nhanh nhưng đầy đủ cho câu hỏi của bạn, tôi nghĩ chúng ta có thể quản lý.

Hãy giải quyết một sự kiện nhấn nút đơn giản trong ứng dụng Windows Forms:

public async void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before awaiting");
    await GetSomethingAsync();
    Console.WriteLine("after awaiting");
}

Tôi rõ ràng sẽ không nói về bất cứ điều gìGetSomethingAsync sẽ trở lại bây giờ. Chúng ta hãy nói rằng đây là thứ sẽ hoàn thành sau 2 giây.

Trong một thế giới truyền thống, không đồng bộ, trình xử lý sự kiện nhấn nút của bạn sẽ trông giống như thế này:

public void button1_Click(object sender, EventArgs e)
{
    Console.WriteLine("before waiting");
    DoSomethingThatTakes2Seconds();
    Console.WriteLine("after waiting");
}

Khi bạn nhấp vào nút trong biểu mẫu, ứng dụng sẽ xuất hiện đóng băng trong khoảng 2 giây, trong khi chúng tôi chờ phương pháp này hoàn tất. Điều gì xảy ra là "bơm thông điệp", về cơ bản là một vòng lặp, bị chặn.

Vòng lặp này liên tục hỏi các cửa sổ "Có ai đã làm gì đó chưa, như di chuyển chuột, nhấp vào cái gì đó? Tôi có cần sơn lại cái gì không? Nếu vậy, hãy nói cho tôi biết!" và sau đó xử lý "cái gì đó". Vòng lặp này có một thông báo mà người dùng đã nhấp vào "button1" (hoặc loại tin nhắn tương đương từ Windows) và cuối cùng đã gọi button1_Clickphương thức của chúng tôi ở trên. Cho đến khi phương thức này trở lại, vòng lặp này hiện đang bị kẹt chờ đợi. Điều này mất 2 giây và trong thời gian này, không có tin nhắn nào đang được xử lý.

Hầu hết mọi thứ liên quan đến windows đều được thực hiện bằng cách sử dụng tin nhắn, điều đó có nghĩa là nếu vòng lặp tin nhắn ngừng bơm tin nhắn, thậm chí chỉ trong một giây, nó sẽ nhanh chóng được người dùng chú ý. Chẳng hạn, nếu bạn di chuyển notepad hoặc bất kỳ chương trình nào khác lên trên chương trình của chính bạn, rồi lại đi, một loạt các thông điệp sơn được gửi đến chương trình của bạn cho biết khu vực nào của cửa sổ đột nhiên xuất hiện trở lại. Nếu vòng lặp thông báo xử lý các tin nhắn này đang chờ một cái gì đó, bị chặn, thì không có bức tranh nào được thực hiện.

Vì vậy, nếu trong ví dụ đầu tiên, async/awaitkhông tạo chủ đề mới, làm thế nào để làm điều đó?

Vâng, những gì xảy ra là phương pháp của bạn được chia thành hai. Đây là một trong những loại chủ đề rộng lớn vì vậy tôi sẽ không đi sâu vào chi tiết nhưng đủ để nói phương pháp này được chia thành hai điều sau:

  1. Tất cả các mã dẫn đến await, bao gồm cả lệnh gọi đếnGetSomethingAsync
  2. Tất cả các mã sau đây await

Hình minh họa:

code... code... code... await X(); ... code... code... code...

Sắp xếp lại:

code... code... code... var x = X(); await X; code... code... code...
^                                  ^          ^                     ^
+---- portion 1 -------------------+          +---- portion 2 ------+

Về cơ bản phương thức thực hiện như thế này:

  1. Nó thực thi mọi thứ lên đến await
  2. Nó gọi GetSomethingAsyncphương thức, thực hiện công việc của nó và trả về một cái gì đó sẽ hoàn thành trong 2 giây trong tương lai

    Cho đến nay chúng ta vẫn ở trong cuộc gọi ban đầu đến button1_Click, xảy ra trên luồng chính, được gọi từ vòng lặp tin nhắn. Nếu mã dẫn đến awaitmất nhiều thời gian, giao diện người dùng sẽ vẫn đóng băng. Trong ví dụ của chúng tôi, không quá nhiều

  3. Những gì awaittừ khóa, cùng với một số phép thuật trình biên dịch thông minh, về cơ bản là nó giống như "Ok, bạn biết gì không, tôi sẽ chỉ đơn giản trở về từ trình xử lý sự kiện nhấn nút ở đây. Khi bạn (như, điều chúng ta ' Đang chờ) hãy đi xung quanh để hoàn thành, hãy cho tôi biết vì tôi vẫn còn một số mã để thực thi ".

    Trên thực tế, nó sẽ cho lớp SyncizationContext biết rằng nó đã được thực hiện, tùy thuộc vào bối cảnh đồng bộ hóa thực tế đang diễn ra ngay bây giờ, sẽ xếp hàng để thực thi. Lớp ngữ cảnh được sử dụng trong chương trình Windows Forms sẽ xếp hàng nó bằng cách sử dụng hàng đợi mà vòng lặp thông báo đang bơm.

  4. Vì vậy, nó trở lại vòng lặp tin nhắn, giờ đây miễn phí để tiếp tục bơm tin nhắn, như di chuyển cửa sổ, thay đổi kích thước hoặc nhấp vào các nút khác.

    Đối với người dùng, giao diện người dùng hiện đang phản hồi lại, xử lý các lần nhấp nút khác, thay đổi kích thước và quan trọng nhất là vẽ lại , do đó, nó dường như không bị đóng băng.

  5. 2 giây sau, điều chúng tôi đang chờ hoàn tất và điều xảy ra bây giờ là nó (tốt, bối cảnh đồng bộ hóa) đặt một tin nhắn vào hàng đợi mà vòng lặp tin nhắn đang xem, nói rằng "Này, tôi có thêm một số mã cho bạn thực thi "và mã này là tất cả mã sau khi chờ đợi.
  6. Khi vòng lặp thông báo đến được thông báo đó, về cơ bản nó sẽ "nhập lại" phương thức đó khi nó dừng lại, ngay sau đó awaitvà tiếp tục thực hiện phần còn lại của phương thức. Lưu ý rằng mã này được gọi lại từ vòng lặp thông báo, vì vậy nếu mã này xảy ra để làm gì đó dài mà không sử dụng async/awaitđúng cách, nó sẽ lại chặn vòng lặp thông báo

Có nhiều bộ phận chuyển động dưới mui xe ở đây vì vậy đây là một số liên kết đến nhiều thông tin hơn, tôi sẽ nói "nếu bạn cần nó", nhưng chủ đề này khá rộng và khá quan trọng để biết một số bộ phận chuyển động đó . Lúc nào bạn cũng sẽ hiểu rằng async / await vẫn là một khái niệm rò rỉ. Một số hạn chế và vấn đề cơ bản vẫn rò rỉ vào mã xung quanh và nếu không, bạn thường phải gỡ lỗi một ứng dụng bị hỏng ngẫu nhiên mà dường như không có lý do chính đáng.


OK, vậy điều gì sẽ xảy ra nếu GetSomethingAsyncquay lên một chuỗi sẽ hoàn thành sau 2 giây? Vâng, sau đó rõ ràng là có một chủ đề mới trong chơi. Tuy nhiên, luồng này không phải vì tính không đồng bộ của phương thức này, mà là do người lập trình của phương thức này đã chọn một luồng để triển khai mã không đồng bộ. Hầu như tất cả các I / O không đồng bộ không sử dụng một luồng, chúng sử dụng những thứ khác nhau. async/await tự chúng không quay các luồng mới nhưng rõ ràng "những điều chúng ta chờ đợi" có thể được thực hiện bằng các luồng.

Có nhiều thứ trong .NET không nhất thiết phải tự tạo một luồng nhưng vẫn không đồng bộ:

  • Yêu cầu web (và nhiều thứ khác liên quan đến mạng cần có thời gian)
  • Đọc và ghi tệp không đồng bộ
  • và nhiều hơn nữa, một dấu hiệu tốt là nếu lớp / giao diện trong câu hỏi đã được đặt tên phương pháp SomethingSomethingAsynchay BeginSomethingEndSomethingvà có một IAsyncResulttham gia.

Thông thường những thứ này không sử dụng một chủ đề dưới mui xe.


OK, vì vậy bạn muốn một số "công cụ chủ đề rộng"?

Chà, hãy hỏi Thử Roslyn về nút bấm của chúng tôi:

Hãy thử Roslyn

Tôi sẽ không liên kết trong lớp được tạo đầy đủ ở đây nhưng đó là thứ khá đẫm máu.


10
Vì vậy, về cơ bản, cái mà OP mô tả là " Mô phỏng thực thi song song bằng cách lên lịch các tác vụ và chuyển đổi giữa chúng ", phải không?
Bergi

4
@Bergi Không hẳn. Việc thực thi thực sự song song - tác vụ I / O không đồng bộ đang diễn ra và không yêu cầu tiến hành (đây là thứ đã được sử dụng từ lâu trước khi Windows xuất hiện - MS DOS cũng sử dụng I / O không đồng bộ, mặc dù nó không có đa luồng!). Tất nhiên, await có thể được sử dụng theo cách bạn mô tả, nhưng nói chung là không. Chỉ các cuộc gọi lại được lên lịch (trên nhóm luồng) - giữa cuộc gọi lại và yêu cầu, không cần luồng nào.
Luaan

3
Đó là lý do tại sao tôi muốn tránh nói quá nhiều về phương pháp đó đã làm gì, vì câu hỏi là về async / await cụ thể, không tạo ra các chủ đề riêng của nó. Rõ ràng, chúng có thể được sử dụng để chờ đợi các chủ đề hoàn thành.
Lasse V. Karlsen

5
@ LasseV.Karlsen - Tôi đang đọc câu trả lời tuyệt vời của bạn, nhưng tôi vẫn gác máy ở một chi tiết. Tôi hiểu rằng xử lý sự kiện tồn tại, như trong bước 4, cho phép các máy bơm thông điệp tiếp tục bơm, nhưng khinơi nào để "điều mà phải mất hai giây" tiếp tục thực hiện nếu không muốn nói về một chủ đề riêng biệt? Nếu nó được thực thi trên luồng UI, thì dù sao nó cũng sẽ chặn bơm thông báo trong khi nó thực thi vì nó phải thực thi một thời gian trên cùng một luồng .. [tiếp tục] ...
rory.ap 8/12/2016

3
Tôi thích lời giải thích của bạn với máy bơm tin nhắn. Làm thế nào để giải thích của bạn khác nhau khi không có máy bơm tin nhắn như trong ứng dụng bảng điều khiển hoặc máy chủ web? Làm thế nào để reentrace của một phương pháp đạt được?
Puchacz

94

Tôi giải thích nó đầy đủ trong bài viết trên blog của tôi Không có chủ đề .

Tóm lại, các hệ thống I / O hiện đại sử dụng rất nhiều DMA (Truy cập bộ nhớ trực tiếp). Có các bộ xử lý đặc biệt, chuyên dụng trên card mạng, card màn hình, bộ điều khiển ổ cứng, cổng nối tiếp / song song, v.v ... Những bộ xử lý này có quyền truy cập trực tiếp vào bus bộ nhớ và xử lý đọc / ghi hoàn toàn độc lập với CPU. CPU chỉ cần thông báo cho thiết bị về vị trí trong bộ nhớ chứa dữ liệu và sau đó có thể tự thực hiện cho đến khi thiết bị tăng ngắt thông báo cho CPU rằng việc đọc / ghi đã hoàn tất.

Khi hoạt động trong chuyến bay, CPU không có việc gì để thực hiện và do đó không có luồng nào.


Chỉ cần làm cho nó rõ ràng .. Tôi hiểu mức độ cao của những gì xảy ra khi sử dụng async-await. Về việc không tạo luồng - không có luồng nào trong các yêu cầu I / O đối với các thiết bị như bạn nói có bộ xử lý riêng xử lý yêu cầu đó không? Chúng ta có thể giả sử TẤT CẢ các yêu cầu I / O được xử lý trên các bộ xử lý độc lập như vậy có nghĩa là sử dụng Task.Run CHỈ trên các hành động bị ràng buộc của CPU không?
Yonatan Nir

@YonatanNir: Không chỉ là về bộ xử lý riêng biệt; bất kỳ loại phản ứng hướng sự kiện nào là không đồng bộ một cách tự nhiên. Task.Runlà thích hợp nhất cho các hành động gắn với CPU , nhưng nó cũng có một số ứng dụng khác.
Stephen Cleary

1
Tôi đã đọc xong bài viết của bạn và vẫn còn một số điều cơ bản mà tôi không hiểu vì tôi không thực sự quen thuộc với việc triển khai hệ điều hành cấp thấp hơn. Tôi đã nhận được những gì bạn đã viết cho đến nơi bạn đã viết: "Thao tác viết hiện đang là trên máy bay. Có bao nhiêu luồng đang xử lý nó? Không có gì." . Vì vậy, nếu không có chủ đề, thì làm thế nào để hoạt động chính nó được thực hiện nếu không trên một chủ đề?
Yonatan Nir

5
Đây là phần còn thiếu trong hàng ngàn lời giải thích !!! Thực sự có ai đó đang thực hiện công việc ở chế độ nền với các thao tác I / O. Nó không phải là một chủ đề mà là một thành phần phần cứng chuyên dụng khác làm công việc của nó!
the_dark_destructor

2
@PrabuWeerasinghe: Trình biên dịch tạo ra một cấu trúc chứa các biến trạng thái và cục bộ. Nếu một sự chờ đợi cần phải mang lại (nghĩa là quay trở lại với người gọi của nó), cấu trúc đó được đóng hộp và sống trên đống.
Stephen Cleary

87

cách duy nhất mà máy tính có thể thực hiện nhiều hơn 1 việc một lúc là (1) Thực tế làm nhiều việc 1 lần, (2) mô phỏng nó bằng cách lập lịch tác vụ và chuyển đổi giữa chúng. Vì vậy, nếu async-await không có ai trong số đó

Nó không phải là chờ đợi cả những điều đó. Hãy nhớ rằng, mục đích của việc awaitkhông làm cho mã đồng bộ trở nên không đồng bộ một cách kỳ diệu . Đó là cho phép sử dụng các kỹ thuật tương tự mà chúng tôi sử dụng để viết mã đồng bộ khi gọi vào mã không đồng bộ . Await là về việc làm cho mã sử dụng các hoạt động có độ trễ cao trông giống như mã sử dụng các hoạt động có độ trễ thấp . Các hoạt động có độ trễ cao này có thể là trên các luồng, chúng có thể là trên phần cứng cho mục đích đặc biệt, chúng có thể xé công việc của chúng thành từng mảnh nhỏ và đưa nó vào hàng đợi tin nhắn để xử lý bởi luồng UI sau này. Họ đang làm gì đó để đạt được sự không đồng bộ, nhưng họlà những người đang làm điều đó. Chờ đợi chỉ cho phép bạn tận dụng sự không đồng bộ đó.

Ngoài ra, tôi nghĩ rằng bạn đang thiếu một lựa chọn thứ ba. Những người già chúng ta - những đứa trẻ ngày nay với nhạc rap của họ sẽ rời khỏi bãi cỏ của tôi, v.v. - hãy nhớ thế giới Windows vào đầu những năm 1990. Không có máy đa CPU và không có bộ lập lịch luồng. Bạn muốn chạy hai ứng dụng Windows cùng một lúc, bạn phải nhượng bộ . Đa nhiệm đã hợp tác . HĐH cho biết một quy trình mà nó sẽ chạy, và nếu nó hoạt động kém, nó sẽ bỏ qua tất cả các quy trình khác để được phục vụ. Nó chạy cho đến khi nó mang lại, và bằng cách nào đó nó phải biết cách chọn nơi nó rời đi vào lần tiếp theo khi hệ điều hành cầm tay điều khiển trở lại nó. Mã không đồng bộ đơn luồng rất giống như vậy, với "await" thay vì "ield". Chờ đợi có nghĩa là "Tôi sẽ nhớ nơi tôi rời khỏi đây và để người khác chạy một lúc; gọi lại cho tôi khi nhiệm vụ tôi đang chờ hoàn thành và tôi sẽ chọn nơi tôi rời đi." Tôi nghĩ bạn có thể thấy cách điều đó làm cho các ứng dụng phản ứng nhanh hơn, giống như đã làm trong Windows 3 ngày.

gọi bất kỳ phương thức nào có nghĩa là chờ phương thức hoàn thành

Có chìa khóa mà bạn đang thiếu. Một phương thức có thể trở lại trước khi công việc của nó hoàn thành . Đó là bản chất của sự không đồng bộ ngay tại đó. Một phương thức trả về, nó trả về một nhiệm vụ có nghĩa là "công việc này đang được tiến hành; cho tôi biết phải làm gì khi hoàn thành". Công việc của phương thức không được thực hiện, mặc dù nó đã trở lại .

Trước toán tử đang chờ, bạn phải viết mã trông giống như spaghetti được luồn qua phô mai Thụy Sĩ để đối phó với thực tế là chúng ta phải làm sau khi hoàn thành, nhưng với việc trả lại và hoàn thành không đồng bộ . Await cho phép bạn viết mã trông giống như trả lại và hoàn thành được đồng bộ hóa, mà không thực sự được đồng bộ hóa.


Các ngôn ngữ cấp cao hiện đại khác cũng hỗ trợ hành vi hợp tác rõ ràng tương tự (ví dụ: hàm thực hiện một số nội dung, mang lại [có thể gửi một số giá trị / đối tượng cho người gọi], tiếp tục khi nó dừng lại khi điều khiển được trả lại [có thể được cung cấp thêm đầu vào] ). Các trình tạo rất nhiều trong Python, vì một điều.
JAB

2
@JAB: Hoàn toàn đúng. Các trình tạo được gọi là "khối lặp" trong C # và sử dụng yieldtừ khóa. Cả hai asyncphương thức và các trình vòng lặp trong C # đều là một dạng coroutine , đây là thuật ngữ chung cho một hàm biết cách tạm dừng hoạt động hiện tại của nó để nối lại sau này. Một số ngôn ngữ có dòng điều khiển coroutine hoặc coroutine ngày nay.
Eric Lippert

1
Sự tương tự về năng suất là một điều tốt - đó là đa nhiệm hợp tác trong một quy trình. (và do đó tránh các vấn đề ổn định hệ thống của đa nhiệm hợp tác toàn hệ thống)
user253751

3
Tôi nghĩ rằng khái niệm "ngắt cpu" đang được sử dụng cho IO, không biết về rất nhiều "lập trình viên" modem, do đó họ nghĩ rằng một luồng cần phải đợi từng bit IO.
Ian Ringrose

@EricLippert Phương thức Async của WebClient thực sự tạo ra luồng bổ sung, xem tại đây stackoverflow.com/questions/48366871/
Kẻ

27

Tôi thực sự vui mừng khi có ai đó hỏi câu hỏi này, vì trong thời gian dài nhất tôi cũng tin rằng các chủ đề là cần thiết để tương tranh. Khi tôi lần đầu tiên nhìn thấy các vòng lặp sự kiện , tôi nghĩ rằng chúng là một lời nói dối. Tôi tự nghĩ "không có cách nào mã này có thể đồng thời nếu nó chạy trong một luồng". Hãy ghi nhớ điều này là sau khi tôi đã trải qua cuộc đấu tranh để hiểu sự khác biệt giữa đồng thời và song song.

Sau khi nghiên cứu của riêng tôi, cuối cùng tôi đã tìm thấy mảnh còn thiếu : select(). Cụ thể, IO ghép, bởi hạt nhân khác nhau thực hiện dưới tên gọi khác nhau: select(), poll(), epoll(), kqueue(). Đây là các cuộc gọi hệ thống , trong khi các chi tiết triển khai khác nhau, cho phép bạn chuyển qua một bộ mô tả tệp để xem. Sau đó, bạn có thể thực hiện một cuộc gọi khác chặn cho đến khi một trong những mô tả tệp đã xem thay đổi.

Do đó, người ta có thể chờ đợi một tập hợp các sự kiện IO (vòng lặp sự kiện chính), xử lý sự kiện đầu tiên hoàn thành và sau đó đưa điều khiển trở lại vòng lặp sự kiện. Rửa sạch và lặp lại.

Cái này hoạt động ra sao? Chà, câu trả lời ngắn gọn là ma thuật cấp hạt nhân và cấp độ phần cứng. Có nhiều thành phần trong máy tính ngoài CPU và các thành phần này có thể hoạt động song song. Nhân có thể điều khiển các thiết bị này và liên lạc trực tiếp với chúng để nhận tín hiệu nhất định.

Các cuộc gọi hệ thống ghép kênh IO này là khối xây dựng cơ bản của các vòng lặp sự kiện đơn luồng như node.js hoặc Tornado. Khi bạn có awaitmột chức năng, bạn đang theo dõi một sự kiện nào đó (hoàn thành chức năng đó) và sau đó mang lại quyền kiểm soát cho vòng lặp sự kiện chính. Khi sự kiện bạn đang xem hoàn thành, chức năng (cuối cùng) sẽ xuất hiện từ nơi nó dừng lại. Các chức năng cho phép bạn tạm dừng và tiếp tục tính toán như thế này được gọi là coroutines .


25

awaitasyncsử dụng Nhiệm vụ không phải Chủ đề.

Khung công tác có một nhóm các luồng sẵn sàng để thực hiện một số công việc dưới dạng các đối tượng Nhiệm vụ ; gửi một tác vụ đến nhóm có nghĩa là chọn một luồng tự do, đã có sẵn 1 , để gọi phương thức hành động tác vụ.
Tạo một tác vụ là vấn đề tạo ra một đối tượng mới, nhanh hơn nhiều so với việc tạo ra một luồng mới.

Có một Nhiệm vụ có thể đính kèm Tiếp tục với nó, đó là một đối tượng Nhiệm vụ mới sẽ được thực thi khi luồng kết thúc.

Kể từ khi async/awaitsử dụng Nhiệm vụ , họ không tạo ra một chủ đề mới .


Mặc dù kỹ thuật lập trình ngắt được sử dụng rộng rãi trong mọi HĐH hiện đại, tôi không nghĩ chúng có liên quan ở đây.
Bạn có thể có hai tác vụ ngoại quan CPU thực thi song song (thực tế xen kẽ) trong một CPU bằng cách sử dụng aysnc/await.
Điều đó không thể giải thích đơn giản với thực tế là HĐH hỗ trợ xếp hàng IORP .


Lần trước tôi đã kiểm tra trình biên dịch chuyển đổi asynccác phương thức thành DFA , công việc được chia thành các bước, mỗi bước kết thúc bằng một awaitlệnh.
Các awaitkhởi động của nó tác và đính kèm nó một sự tiếp nối để thực hiện các bước tiếp theo.

Như một ví dụ về khái niệm, đây là một ví dụ mã giả.
Mọi thứ đang được đơn giản hóa vì mục đích rõ ràng và vì tôi không nhớ chính xác tất cả các chi tiết.

method:
   instr1                  
   instr2
   await task1
   instr3
   instr4
   await task2
   instr5
   return value

Nó được biến thành một thứ như thế này

int state = 0;

Task nextStep()
{
  switch (state)
  {
     case 0:
        instr1;
        instr2;
        state = 1;

        task1.addContinuation(nextStep());
        task1.start();

        return task1;

     case 1:
        instr3;
        instr4;
        state = 2;

        task2.addContinuation(nextStep());
        task2.start();

        return task2;

     case 2:
        instr5;
        state = 0;

        task3 = new Task();
        task3.setResult(value);
        task3.setCompleted();

        return task3;
   }
}

method:
   nextStep();

1 Trên thực tế một nhóm có thể có chính sách tạo nhiệm vụ của nó.


16

Tôi sẽ không cạnh tranh với Eric Lippert hay Lasse V. Karlsen, và những người khác, tôi chỉ muốn thu hút sự chú ý đến một khía cạnh khác của câu hỏi này, mà tôi nghĩ không được đề cập rõ ràng.

Sử dụng awaitriêng nó không làm cho ứng dụng của bạn phản ứng kỳ diệu. Nếu bất cứ điều gì bạn làm trong phương thức bạn đang chờ đợi từ các khối luồng UI, thì nó vẫn sẽ chặn UI của bạn giống như phiên bản không chờ đợi .

Bạn phải viết phương thức chờ đợi của mình một cách cụ thể để nó tạo ra một luồng mới hoặc sử dụng một cái gì đó giống như một cổng hoàn thành (sẽ trả lại sự thực thi trong luồng hiện tại và gọi một cái gì đó khác để tiếp tục bất cứ khi nào cổng hoàn thành được báo hiệu). Nhưng phần này được giải thích tốt trong các câu trả lời khác.


3
Đó không phải là một cuộc thi ở nơi đầu tiên; đó là một sự hợp tác!
Eric Lippert

16

Đây là cách tôi xem tất cả những điều này, nó có thể không chính xác về mặt kỹ thuật nhưng ít nhất nó cũng giúp tôi :).

Về cơ bản có hai loại xử lý (tính toán) xảy ra trên một máy:

  • xử lý xảy ra trên CPU
  • xử lý xảy ra trên các bộ xử lý khác (GPU, card mạng, v.v.), hãy gọi chúng là IO.

Vì vậy, khi chúng ta viết một đoạn mã nguồn, sau khi biên dịch, tùy thuộc vào đối tượng chúng ta sử dụng (và điều này rất quan trọng), việc xử lý sẽ bị ràng buộc CPU hoặc ràng buộc IO , và trên thực tế, nó có thể bị ràng buộc với sự kết hợp của cả hai.

Vài ví dụ:

  • nếu tôi sử dụng phương thức Write của FileStreamđối tượng (là luồng), việc xử lý sẽ có nghĩa là, 1% CPU bị ràng buộc và 99% IO bị ràng buộc.
  • nếu tôi sử dụng phương thức Write của NetworkStreamđối tượng (là luồng), việc xử lý sẽ có nghĩa là, 1% CPU bị ràng buộc và 99% IO bị ràng buộc.
  • nếu tôi sử dụng phương thức Write của Memorystreamđối tượng (là luồng), việc xử lý sẽ bị ràng buộc 100% CPU.

Vì vậy, như bạn thấy, từ quan điểm của một lập trình viên hướng đối tượng, mặc dù tôi luôn truy cập vào một Streamđối tượng, những gì xảy ra bên dưới có thể phụ thuộc rất nhiều vào loại cuối cùng của đối tượng.

Bây giờ, để tối ưu hóa mọi thứ, đôi khi có thể chạy mã song song (lưu ý tôi không sử dụng từ không đồng bộ) nếu có thể và / hoặc cần thiết.

Vài ví dụ:

  • Trong một ứng dụng máy tính để bàn, tôi muốn in một tài liệu, nhưng tôi không muốn đợi nó.
  • Máy chủ web của tôi phục vụ nhiều khách hàng cùng một lúc, mỗi máy chủ nhận được các trang của mình song song (không được tuần tự hóa).

Trước khi async / await, về cơ bản chúng tôi đã có hai giải pháp cho việc này:

  • Chủ đề . Nó tương đối dễ sử dụng, với các lớp Thread và ThreadPool. Chủ đề chỉ bị ràng buộc CPU .
  • Mô hình lập trình không đồng bộ Begin / End / AsyncCallback "cũ" . Nó chỉ là một mô hình, nó không cho bạn biết nếu bạn bị ràng buộc CPU hoặc IO. Nếu bạn xem các lớp Socket hoặc FileStream, nó bị ràng buộc IO, điều này thật tuyệt, nhưng chúng ta hiếm khi sử dụng nó.

Async / await chỉ là một mô hình lập trình phổ biến, dựa trên khái niệm Nhiệm vụ . Nó dễ sử dụng hơn một chút so với luồng hoặc nhóm luồng cho các tác vụ bị ràng buộc CPU và dễ sử dụng hơn nhiều so với mô hình Bắt đầu / Kết thúc cũ. Tuy nhiên, Undercovers là "chỉ" một trình bao bọc đầy đủ tính năng siêu tinh vi trên cả hai.

Vì vậy, phần thắng thực sự chủ yếu thuộc về các nhiệm vụ IO Bound , tác vụ không sử dụng CPU, nhưng async / await vẫn chỉ là một mô hình lập trình, nó không giúp bạn xác định cách thức / nơi xử lý cuối cùng sẽ xảy ra.

Điều đó có nghĩa là không phải vì một lớp có phương thức "DoS SomethingAsync" trả về một đối tượng Nhiệm vụ mà bạn có thể đoán nó sẽ bị ràng buộc CPU (có nghĩa là nó có thể khá vô dụng , đặc biệt là nếu nó không có tham số mã thông báo hủy) hoặc IO Bound (có nghĩa là nó có thể là phải ) hoặc kết hợp cả hai (vì mô hình này khá lan truyền, liên kết và lợi ích tiềm năng có thể, cuối cùng, siêu hỗn hợp và không quá rõ ràng).

Vì vậy, quay trở lại các ví dụ của tôi, thực hiện các thao tác Viết của tôi bằng cách sử dụng async / await trên MemoryStream sẽ bị ràng buộc CPU (tôi có thể sẽ không được hưởng lợi từ nó), mặc dù tôi chắc chắn sẽ được hưởng lợi từ nó với các tệp và luồng mạng.


1
Đây là một câu trả lời khá hay khi sử dụng theadpool cho công việc bị ràng buộc cpu kém theo nghĩa là các luồng TP nên được sử dụng để giảm tải các hoạt động IO. Tất nhiên, imo làm việc với CPU nên được chặn với sự cẩn thận và không có gì ngăn cản việc sử dụng nhiều luồng.
davidcarr

3

Tóm tắt các câu trả lời khác:

Async / await chủ yếu được tạo cho các tác vụ ràng buộc IO vì bằng cách sử dụng chúng, người ta có thể tránh chặn chuỗi cuộc gọi. Công dụng chính của chúng là với các luồng UI nơi mà luồng không mong muốn bị chặn trên một hoạt động bị ràng buộc IO.

Async không tạo chủ đề riêng của nó. Chuỗi của phương thức gọi được sử dụng để thực thi phương thức async cho đến khi nó tìm thấy một sự chờ đợi. Sau đó, cùng một luồng tiếp tục thực hiện phần còn lại của phương thức gọi ngoài cuộc gọi phương thức async. Trong phương thức async được gọi, sau khi trở về từ chờ đợi, việc tiếp tục có thể được thực thi trên một luồng từ nhóm luồng - nơi duy nhất một luồng riêng biệt xuất hiện trong ảnh.


Tóm tắt tốt, nhưng tôi nghĩ nó nên trả lời thêm 2 câu hỏi nữa để đưa ra bức tranh đầy đủ: 1. Chủ đề nào mà mã chờ đợi được thực thi trên? 2. Ai kiểm soát / cấu hình nhóm luồng đã đề cập - nhà phát triển hoặc môi trường thời gian chạy?
stojke

1. Trong trường hợp này, phần lớn mã được chờ là hoạt động ràng buộc IO sẽ không sử dụng các luồng CPU. Nếu muốn sử dụng chờ đợi cho hoạt động bị ràng buộc của CPU, một Tác vụ riêng biệt có thể được sinh ra. 2. Chuỗi trong nhóm luồng được quản lý bởi bộ lập lịch tác vụ là một phần của khung TPL.
vaibhav kumar

2

Tôi cố gắng giải thích nó từ dưới lên. Có lẽ ai đó thấy nó hữu ích. Tôi đã ở đó, thực hiện điều đó, phát minh lại nó, khi tạo ra các trò chơi đơn giản trong DOS trong Pascal (thời xưa tốt đẹp ...)

Vì vậy, ... Trong mọi ứng dụng hướng sự kiện đều có một vòng lặp sự kiện bên trong đó là một thứ như thế này:

while (getMessage(out message)) // pseudo-code
{
   dispatchMessage(message); // pseudo-code
}

Các khung thường ẩn chi tiết này với bạn nhưng nó ở đó. Hàm getMessage đọc sự kiện tiếp theo từ hàng đợi sự kiện hoặc đợi cho đến khi sự kiện xảy ra: di chuyển chuột, nhấn phím, bấm phím, nhấp chuột, v.v. Sau đó chờ đợi sự kiện tiếp theo và cứ thế cho đến khi một sự kiện thoát ra xuất hiện các vòng lặp và hoàn thành ứng dụng.

Trình xử lý sự kiện nên chạy nhanh để vòng lặp sự kiện có thể thăm dò thêm sự kiện và giao diện người dùng vẫn phản hồi. Điều gì xảy ra nếu một nút bấm kích hoạt một hoạt động đắt tiền như thế này?

void expensiveOperation()
{
    for (int i = 0; i < 1000; i++)
    {
        Thread.Sleep(10);
    }
}

Vâng, UI trở nên không phản hồi cho đến khi hoạt động 10 giây kết thúc khi điều khiển nằm trong chức năng. Để giải quyết vấn đề này, bạn cần chia nhỏ nhiệm vụ thành các phần nhỏ để có thể thực hiện nhanh chóng. Điều này có nghĩa là bạn không thể xử lý toàn bộ trong một sự kiện. Bạn phải làm một phần nhỏ của công việc, sau đó đăng một sự kiện khác vào hàng đợi sự kiện để yêu cầu tiếp tục.

Vì vậy, bạn sẽ thay đổi điều này thành:

void expensiveOperation()
{
    doIteration(0);
}

void doIteration(int i)
{
    if (i >= 1000) return;
    Thread.Sleep(10); // Do a piece of work.
    postFunctionCallMessage(() => {doIteration(i + 1);}); // Pseudo code. 
}

Trong trường hợp này, chỉ lần lặp đầu tiên chạy sau đó nó sẽ gửi một thông báo đến hàng đợi sự kiện để chạy lần lặp tiếp theo và trả về. Đó là ví dụ của chúng tôipostFunctionCallMessageHàm giả đặt một sự kiện "gọi hàm này" vào hàng đợi, vì vậy bộ điều phối sự kiện sẽ gọi nó khi nó đến được nó. Điều này cho phép tất cả các sự kiện GUI khác được xử lý trong khi vẫn chạy liên tục các phần của một hoạt động dài.

Miễn là tác vụ chạy dài này đang chạy, sự kiện tiếp tục của nó luôn nằm trong hàng đợi sự kiện. Vì vậy, về cơ bản bạn đã phát minh ra lịch trình nhiệm vụ của riêng bạn. Trong đó các sự kiện tiếp diễn trong hàng đợi là "các quy trình" đang chạy. Trên thực tế đây là những gì hệ điều hành làm, ngoại trừ việc gửi các sự kiện tiếp diễn và quay lại vòng lập lịch được thực hiện thông qua bộ đếm thời gian của CPU, nơi HĐH đã đăng ký mã chuyển đổi ngữ cảnh, vì vậy bạn không cần quan tâm đến nó. Nhưng ở đây bạn đang viết lịch trình của riêng bạn, do đó bạn cần phải quan tâm đến nó - cho đến nay.

Vì vậy, chúng ta có thể chạy các tác vụ chạy dài trong một luồng song song với GUI bằng cách chia chúng thành các phần nhỏ và gửi các sự kiện tiếp tục. Đây là ý tưởng chung của Tasklớp. Nó đại diện cho một phần công việc và khi bạn gọi .ContinueWithnó, bạn xác định hàm nào sẽ gọi là phần tiếp theo khi phần hiện tại kết thúc (và giá trị trả về của nó được chuyển sang phần tiếp theo). CácTask lớp học sử dụng một hồ bơi thread, nơi có một vòng lặp sự kiện trong mỗi thread chờ đợi để làm mẩu công việc tương tự như muốn tôi đã giới thiệu ngay từ đầu. Bằng cách này, bạn có thể có hàng triệu tác vụ chạy song song, nhưng chỉ có một vài luồng để chạy chúng. Nhưng nó cũng hoạt động tốt với một luồng duy nhất - miễn là các tác vụ của bạn được phân chia thành các phần nhỏ, mỗi phần trong số chúng xuất hiện chạy song song.

Nhưng thực hiện tất cả các chuỗi phân tách công việc này thành các phần nhỏ bằng tay là một công việc rườm rà và hoàn toàn làm rối tung bố cục của logic, bởi vì toàn bộ mã tác vụ nền về cơ bản là một .ContinueWithmớ hỗn độn. Vì vậy, đây là nơi trình biên dịch giúp bạn. Nó thực hiện tất cả các chuỗi này và tiếp tục cho bạn trong nền. Khi bạn nói awaitbạn nói với trình biên dịch rằng "dừng ở đây, thêm phần còn lại của hàm làm tác vụ tiếp tục". Trình biên dịch sẽ giải quyết phần còn lại, vì vậy bạn không cần phải làm vậy.


0

Trên thực tế, async awaitchuỗi là máy trạng thái được tạo bởi trình biên dịch CLR.

async await tuy nhiên không sử dụng các luồng mà TPL đang sử dụng nhóm luồng để thực thi các tác vụ.

Lý do ứng dụng không bị chặn là do máy trạng thái có thể quyết định đồng xử lý nào sẽ thực thi, lặp lại, kiểm tra và quyết định lại.

Đọc thêm:

Async & await tạo ra cái gì?

Async đang chờ và StateMachine được tạo

C # và F # không đồng bộ (III.): Nó hoạt động như thế nào? - Tomas Petricek

Chỉnh sửa :

Được chứ. Có vẻ như công phu của tôi là không chính xác. Tuy nhiên tôi phải chỉ ra rằng máy móc nhà nước là tài sản quan trọng đối với async awaits. Ngay cả khi bạn sử dụng I / O không đồng bộ, bạn vẫn cần một người trợ giúp để kiểm tra xem hoạt động đã hoàn tất chưa, do đó chúng tôi vẫn cần một máy trạng thái và xác định thói quen nào có thể được thực hiện không đồng bộ cùng nhau.


0

Đây không phải là trả lời trực tiếp câu hỏi, nhưng tôi nghĩ đó là một thông tin bổ sung xen kẽ:

Async và await không tự tạo chủ đề mới. NHƯNG tùy thuộc vào nơi bạn sử dụng async await, phần đồng bộ TRƯỚC KHI chờ đợi có thể chạy trên một luồng khác với phần đồng bộ SAU KHI chờ đợi (ví dụ lõi ASP.NET và ASP.NET hoạt động khác nhau).

Trong các ứng dụng dựa trên UI-Thread (WinForms, WPF), bạn sẽ ở cùng một chủ đề trước và sau. Nhưng khi bạn sử dụng async đi trên một chuỗi chủ đề, luồng trước và sau khi chờ có thể không giống nhau.

Một video tuyệt vời về chủ đề này

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.