Tại sao bạn lại 'chờ đợi' một phương thức, và sau đó thẩm vấn ngay giá trị trả về của nó?


24

Trong bài viết MSDN này , mã ví dụ sau được cung cấp (được chỉnh sửa một chút cho ngắn gọn):

public async Task<ActionResult> Details(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }

    Department department = await db.Departments.FindAsync(id);

    if (department == null)
    {
        return HttpNotFound();
    }

    return View(department);
}

Các FindAsyncphương pháp lấy một Departmentđối tượng bằng ID của nó, và trả về một Task<Department>. Sau đó bộ phận được kiểm tra ngay lập tức để xem nếu nó là null. Theo tôi hiểu, việc yêu cầu giá trị của Nhiệm vụ theo cách này sẽ chặn thực thi mã cho đến khi giá trị từ phương thức chờ được trả về, thực sự biến đây thành một cuộc gọi đồng bộ.

Tại sao bạn sẽ làm điều này? Sẽ không đơn giản hơn nếu chỉ gọi phương thức đồng bộ Find(id), nếu bạn định chặn ngay lập tức?


Nó có thể được thực hiện liên quan. ... else return null;Sau đó, bạn cần kiểm tra xem phương thức đó có thực sự tìm thấy bộ phận bạn yêu cầu không.
Jeremy Kato

Tôi không thấy bất kỳ điều gì trong một asp.net, nhưng trong một ứng dụng định mệnh, bằng cách làm theo cách này, bạn sẽ không đóng băng ui
Rémi

Đây là một liên kết giải thích khái niệm đang chờ đợi từ nhà thiết kế ... msdn.microsoft.com/en-us/magazine/hh456401.aspx
Jon Raynor

Await chỉ đáng suy nghĩ với ASP.NET nếu các công tắc tiếp xúc luồng đang làm bạn chậm lại, hoặc hình thức sử dụng bộ nhớ nhiều ngăn xếp luồng là một vấn đề đối với bạn.
Ian

Câu trả lời:


24

Theo tôi hiểu, việc yêu cầu giá trị của Nhiệm vụ theo cách này sẽ chặn thực thi mã cho đến khi giá trị từ phương thức chờ được trả về, thực sự biến đây thành một cuộc gọi đồng bộ.

Không hẳn.

Khi bạn gọi await db.Departments.FindAsync(id)tác vụ được gửi đi và luồng hiện tại được trả về nhóm để sử dụng cho các hoạt động khác. Luồng thực thi bị chặn (vì nó sẽ không phụ thuộc vào việc sử dụng departmentngay sau đó, nếu tôi hiểu chính xác mọi thứ), nhưng chính luồng này được sử dụng miễn phí bởi những thứ khác trong khi bạn chờ thao tác hoàn tất ngoài máy (và báo hiệu bởi một sự kiện hoặc cổng hoàn thành).

Nếu bạn đã gọi d.Departments.Find(id)thì luồng sẽ ngồi đó và chờ phản hồi, mặc dù hầu hết quá trình xử lý đang được thực hiện trên DB.

Bạn đang giải phóng tài nguyên CPU một cách hiệu quả khi bị ràng buộc đĩa.


2
Nhưng tôi nghĩ tất cả awaitđã làm là ký vào phần còn lại của phương thức dưới dạng tiếp tục trên cùng một luồng (có trường hợp ngoại lệ; một số phương thức không đồng bộ quay lên luồng của chính chúng) hoặc ký vào asyncphương thức dưới dạng tiếp tục trên cùng một luồng và cho phép mã còn lại để thực thi (như bạn có thể thấy, tôi không rõ ràng về cách thức asynchoạt động). Những gì bạn đang mô tả nghe giống như một hình thức tinh vi củaThread.Sleep(untilReturnValueAvailable)
Robert Harvey

1
@RobertHarvey - nó chỉ định nó là phần tiếp theo, nhưng một khi bạn đã gửi nhiệm vụ (và phần tiếp theo của nó) đi để xử lý, không còn gì để chạy. Nó không được đảm bảo trên cùng một chủ đề trừ khi bạn chỉ định nó (thông qua ConfigureAwaitiirc).
Telastyn

1
Xem câu trả lời của tôi ... mặc định tiếp tục được sắp xếp lại theo chủ đề ban đầu.
Michael Brown

2
Tôi nghĩ rằng tôi thấy những gì tôi đang thiếu ở đây. Để điều này mang lại bất kỳ lợi ích nào, khung công tác ASP.NET MVC phải awaitgọi đến public async Task<ActionResult> Details(int? id). Nếu không, cuộc gọi ban đầu sẽ chỉ chặn, chờ department == nullgiải quyết.
Robert Harvey

2
@RobertHarvey Đến lúc await ..."trả lại" FindAsynccuộc gọi đã kết thúc. Đó là những gì đang chờ đợi. Nó được gọi là chờ đợi vì nó làm cho mã của bạn chờ đợi mọi thứ. (Nhưng lưu ý rằng điều đó không giống như làm cho chuỗi hiện tại chờ đợi mọi thứ)
user253751

17

Tôi thực sự ghét rằng không có ví dụ nào cho thấy làm thế nào có thể đợi một vài dòng trước khi chờ đợi nhiệm vụ. Xem xét điều này.

Foo foo = await getFoo();
Bar bar = await getBar();

Console.WriteLine(“Do some other stuff to prepare.”);

doStuff(foo, bar);

Đây là loại mã mà các ví dụ khuyến khích, và bạn đã đúng. Có rất ít ý nghĩa trong việc này. Nó không giải phóng chủ đề chính để làm những việc khác, như phản hồi đầu vào UI, nhưng sức mạnh thực sự của async / await là tôi có thể dễ dàng tiếp tục làm những việc khác trong khi tôi đang chờ một nhiệm vụ có khả năng chạy dài hoàn thành. Đoạn mã trên sẽ chặn khối và chờ đợi để thực thi dòng in cho đến khi chúng tôi nhận được Foo & Bar. Không cần phải chờ đợi mặc dù. Chúng tôi có thể xử lý trong khi chúng tôi chờ đợi.

Task<Foo> foo = getFoo();
Task<Bar> bar = getBar();

Console.WriteLine(“Do some other stuff to prepare.”);

doStuff(await foo, await bar);

Bây giờ, với mã được viết lại, chúng tôi không dừng lại và chờ đợi các giá trị của chúng tôi cho đến khi chúng tôi phải. Tôi luôn đề phòng những loại cơ hội này. Trở nên thông minh khi chúng ta chờ đợi có thể dẫn đến những cải tiến hiệu suất đáng kể. Chúng tôi đã có nhiều lõi ngày nay, cũng có thể sử dụng chúng.


1
Hừm, đó là ít về lõi / luồng và nhiều hơn về cách sử dụng các cuộc gọi không đồng bộ đúng cách.
Ded repeatator

Bạn không sai @Ded repeatator. Đó là lý do tại sao tôi đã trích dẫn khối block trong câu trả lời của tôi. Thật khó để nói chuyện này mà không đề cập đến các chủ đề, nhưng trong khi vẫn thừa nhận có thể có hoặc không có nhiều luồng liên quan.
RubberDuck

Tôi đề nghị bạn nên cẩn thận với việc chờ đợi bên trong một cuộc gọi phương thức khác .. Đôi khi trình biên dịch hiểu nhầm điều này đang chờ đợi và bạn nhận được một ngoại lệ thực sự kỳ lạ. Sau khi tất cả async / await chỉ là cú pháp đường, bạn có thể kiểm tra với sharplab.io xem mã được tạo như thế nào. Tôi đã quan sát điều này trong nhiều lần và bây giờ tôi chỉ chờ một dòng phía trên cuộc gọi khi cần kết quả ... không cần những cơn đau đầu đó.
đuổi

"Có rất ít ý nghĩa trong việc này." - Có rất nhiều ý nghĩa trong việc này. Các trường hợp "tiếp tục làm những việc khác" được áp dụng trong cùng một phương pháp không phải là phổ biến; thay vào đó, nó phổ biến hơn nhiều khi bạn chỉ muốn awaitvà để chủ đề làm những việc hoàn toàn khác nhau thay thế.
Sebastian Redl

Khi tôi nói rằng có một chút ý nghĩa trong điều này @SebastianRedl, tôi có nghĩa là trường hợp bạn rõ ràng có thể chạy cả hai nhiệm vụ song song thay vì chạy, chờ, chạy, chờ. Điều này là phổ biến hơn nhiều so với bạn nghĩ nó là. Tôi đặt cược nếu bạn nhìn xung quanh cơ sở mã của bạn, bạn sẽ tìm thấy cơ hội.
RubberDuck

6

Vì vậy, có nhiều điều xảy ra đằng sau hậu trường ở đây. Async / Await là cú pháp đường. Trước tiên hãy xem chữ ký của chức năng FindAsync. Nó trả về một nhiệm vụ. Bạn đã thấy sự kỳ diệu của từ khóa, nó bỏ hộp Nhiệm vụ đó thành Bộ.

Chức năng gọi không chặn. Điều gì xảy ra là việc gán cho bộ phận và mọi thứ theo từ khóa đang chờ được đóng vào một bao đóng và cho tất cả các ý định và mục đích được truyền cho phương thức Task.CContueWith (chức năng FindAsync được tự động thực hiện trên một luồng khác).

Tất nhiên, có nhiều điều xảy ra đằng sau hậu trường vì thao tác được sắp xếp lại theo chủ đề ban đầu (vì vậy bạn không còn phải lo lắng về việc đồng bộ hóa với giao diện người dùng khi thực hiện thao tác nền) và trong trường hợp chức năng gọi là Async ( và được gọi là không đồng bộ) điều tương tự xảy ra với ngăn xếp.

Vì vậy, những gì xảy ra là bạn có được sự kỳ diệu của các hoạt động Async, mà không có những cạm bẫy.


1

Không, nó không trở lại ngay lập tức. Việc chờ đợi làm cho phương thức gọi không đồng bộ. Khi FindAsync được gọi, phương thức Chi tiết trả về với một tác vụ chưa hoàn thành. Khi FindAsync kết thúc, nó sẽ trả kết quả của nó vào biến bộ phận và tiếp tục phần còn lại của phương thức Chi tiết.


Không có không có chặn ở tất cả. Nó sẽ không bao giờ được chuyển đến bộ phận == null cho đến khi phương thức async kết thúc.
Steve

1
async awaitnói chung không tạo ra các luồng mới và ngay cả khi nó đã hoạt động, bạn vẫn cần đợi giá trị bộ phận đó để tìm hiểu xem nó có rỗng không.
Robert Harvey

2
Vì vậy, về cơ bản những gì bạn đang nói là, để điều này có ích lợi gì cả, cuộc gọi đến public async Task<ActionResult> cũng phải được chỉnh sửa await.
Robert Harvey

2
@RobertHarvey Vâng, bạn đã có ý tưởng. Async / Await về cơ bản là virus. Nếu bạn "chờ" một chức năng, bạn cũng nên chờ bất kỳ chức năng nào gọi nó. awaitkhông nên trộn lẫn với .Wait()hoặc .Result, vì điều này có thể gây ra bế tắc. Chuỗi async / await thường kết thúc tại một hàm có async voidchữ ký, phần lớn được sử dụng cho các trình xử lý sự kiện hoặc cho các hàm được gọi trực tiếp bởi các thành phần UI.
KChaloux

1
@Steve - " ... vì vậy nó sẽ nằm trên luồng của chính nó. " Không, nhiệm vụ đọc vẫn không phải là luồng và async không song song . Điều này bao gồm đầu ra thực tế chứng minh rằng một luồng mới không được tạo.
ToolmakerSteve

1

Tôi thích nghĩ về "async" như một hợp đồng, một hợp đồng có nội dung "tôi có thể thực hiện điều này một cách không đồng bộ nếu bạn cần, nhưng bạn cũng có thể gọi tôi như bất kỳ chức năng đồng bộ nào khác".

Có nghĩa là, một nhà phát triển đã thực hiện các chức năng và một số quyết định thiết kế đã khiến họ thực hiện / đánh dấu một loạt các chức năng là "không đồng bộ". Người gọi / người tiêu dùng của các chức năng có quyền tự do sử dụng chúng khi họ quyết định. Như bạn nói, bạn có thể gọi await ngay trước khi gọi hàm và đợi nó, theo cách này bạn đã coi nó như một hàm đồng bộ, nhưng nếu bạn muốn, bạn có thể gọi nó mà không cần chờ

Task<Department> deptTask = db.Departments.FindAsync(id);

và sau đó, giả sử, 10 dòng xuống chức năng bạn gọi

Department d = await deptTask;

do đó coi nó như là một hàm không đồng bộ.

Tuỳ bạn.


1
Nó giống như "Tôi bảo lưu quyền xử lý tin nhắn không đồng bộ, sử dụng điều này để nhận kết quả".
Ded repeatator

-1

"nếu bạn định chặn ngay lập tức" thì anwser là "Có". Chỉ khi bạn cần phản hồi nhanh, await / async mới có ý nghĩa. Ví dụ, luồng UI đến một phương thức không đồng bộ thực sự, luồng UI sẽ quay trở lại và tiếp tục lắng nghe nhấp vào nút, trong khi mã bên dưới "await" sẽ bị loại bỏ bởi luồng khác và cuối cùng nhận được kết quả.


1
Câu trả lời này cung cấp những gì mà các câu trả lời khác không 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.