Mục đích của việc quay trở lại đang chờ đợi trong C # là gì?


251

Có kịch bản nào mà phương thức viết như thế này không:

public async Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return await DoAnotherThingAsync();
}

thay vì điều này:

public Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return DoAnotherThingAsync();
}

sẽ có ý nghĩa?

Tại sao sử dụng return awaitcấu trúc khi bạn có thể trực tiếp trở về Task<T>từ DoAnotherThingAsync()lời gọi bên trong ?

Tôi thấy mã với return awaitrất nhiều nơi, tôi nghĩ rằng tôi có thể đã bỏ lỡ một cái gì đó. Nhưng theo tôi hiểu, không sử dụng async / await các từ khóa trong trường hợp này và trực tiếp trả về Nhiệm vụ sẽ tương đương về mặt chức năng. Tại sao thêm chi phí bổ sung của awaitlớp bổ sung ?


2
Tôi nghĩ lý do duy nhất khiến bạn thấy điều này là vì mọi người học bằng cách bắt chước và nói chung (nếu họ không cần) họ sử dụng giải pháp đơn giản nhất họ có thể tìm thấy. Vì vậy, mọi người nhìn thấy mã đó, sử dụng mã đó, họ thấy nó hoạt động và từ giờ trở đi, đối với họ, đó là cách đúng đắn để làm điều đó ... Không có gì phải chờ đợi trong trường hợp đó
Fabio Marcolini

7
Có ít nhất một sự khác biệt quan trọng: tuyên truyền ngoại lệ .
chơi

1
Tôi cũng không hiểu nó, không thể hiểu toàn bộ khái niệm này, không có ý nghĩa gì cả. Từ những gì tôi học được nếu một phương thức có kiểu trả về, CNTT PHẢI có từ khóa trả về, đó có phải là quy tắc của ngôn ngữ C # không?
monstro

@monstro câu hỏi của OP có tuyên bố trở lại mặc dù?
David Klempfner

Câu trả lời:


189

Có một trường hợp lén lút khi returntrong phương thức bình thường và return awaittrong asyncphương thức hoạt động khác nhau: khi kết hợp với using(hoặc, nói chung hơn, bất kỳ return awaittrong một trykhối).

Hãy xem xét hai phiên bản của một phương thức:

Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return foo.DoAnotherThingAsync();
    }
}

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

Phương pháp đầu tiên sẽ Dispose()Foođối tượng ngay sau khi DoAnotherThingAsync()trở về phương pháp, đó là khả năng rất lâu trước khi nó thực sự hoàn tất. Điều này có nghĩa là phiên bản đầu tiên có thể có lỗi (vì Foođược xử lý quá sớm), trong khi phiên bản thứ hai sẽ hoạt động tốt.


4
Để đầy đủ, trong trường hợp đầu tiên, bạn nên quay lạifoo.DoAnotherThingAsync().ContinueWith(_ => foo.Dispose());
ghord

7
@ghord Điều đó sẽ không làm việc, Dispose()trở lại void. Bạn sẽ cần một cái gì đó như return foo.DoAnotherThingAsync().ContinueWith(t -> { foo.Dispose(); return t.Result; });. Nhưng tôi không biết tại sao bạn lại làm vậy khi bạn có thể sử dụng tùy chọn thứ hai.
Svick

1
@svick Bạn nói đúng, nó sẽ phù hợp hơn { var task = DoAnotherThingAsync(); task.ContinueWith(_ => foo.Dispose()); return task; }. Trường hợp sử dụng khá đơn giản: nếu bạn đang sử dụng .NET 4.0 (giống như hầu hết), bạn vẫn có thể viết mã async theo cách này sẽ hoạt động tốt được gọi từ 4,5 ứng dụng.
ghord

2
@ghord Nếu bạn đang sử dụng .Net 4.0 và bạn muốn viết mã không đồng bộ, có lẽ bạn nên sử dụng Microsoft.Bcl.Async . Và mã của bạn chỉ xử lý Foosau khi trả về Taskhoàn thành, điều mà tôi không thích, bởi vì nó không cần thiết phải giới thiệu đồng thời.
Svick

1
@svick Mã của bạn chờ cho đến khi nhiệm vụ kết thúc. Ngoài ra, Microsoft.Bcl.Async không thể sử dụng được cho tôi do phụ thuộc vào KB2468871 và xung đột khi sử dụng cơ sở mã hóa async .NET 4.0 với mã async 4.5 phù hợp.
ghord

93

Nếu bạn không cần async(nghĩa là bạn có thể trả lại Tasktrực tiếp), sau đó không sử dụng async.

Có một số tình huống return awaithữu ích, như nếu bạn có hai thao tác không đồng bộ để thực hiện:

var intermediate = await FirstAsync();
return await SecondAwait(intermediate);

Để biết thêm về asynchiệu suất, xem bài viếtvideo MSDN của Stephen Toub về chủ đề này.

Cập nhật: Tôi đã viết một bài đăng trên blog chi tiết hơn nhiều.


13
Bạn có thể thêm một lời giải thích về lý do tại sao awaitnó hữu ích trong trường hợp thứ hai? Tại sao không làm return SecondAwait(intermediate);?
Matt Smith

2
Tôi có câu hỏi tương tự như Matt, cũng sẽ không return SecondAwait(intermediate);đạt được mục tiêu trong trường hợp đó chứ? Tôi nghĩ return awaitlà cũng dư thừa ở đây ...
TX_

23
@MattSmith Điều đó sẽ không được biên dịch. Nếu bạn muốn sử dụng awaittrong dòng đầu tiên, bạn cũng phải sử dụng nó trong dòng thứ hai.
Svick

2
@cateyes Tôi không chắc chắn những gì mà Chi phí giới thiệu được giới thiệu bằng cách song song với chúng có nghĩa là, nhưng asyncphiên bản sẽ sử dụng ít tài nguyên (luồng) hơn phiên bản đồng bộ của bạn.
Svick

4
@TomLint Nó thực sự không biên dịch. Giả sử kiểu trả về SecondAwaitlà `chuỗi, thông báo lỗi là:" CS4016: Vì đây là phương thức không đồng bộ, nên biểu thức trả về phải là loại 'chuỗi' thay vì 'Nhiệm vụ <chuỗi>' ".
Svick

23

Lý do duy nhất bạn muốn làm là nếu có một số mã khác awaittrong mã trước đó hoặc nếu bạn bằng cách nào đó thao túng kết quả trước khi trả lại. Một cách khác trong đó có thể xảy ra là thông qua try/catchthay đổi cách xử lý ngoại lệ. Nếu bạn không làm bất cứ điều gì thì bạn đúng, không có lý do gì để thêm chi phí cho việc thực hiện phương pháp async.


4
Như với câu trả lời của Stephen, tôi không hiểu tại sao lại return awaitcần thiết (thay vì chỉ trả lại nhiệm vụ gọi trẻ em) ngay cả khi có một số điều khác đang chờ trong mã trước đó . Bạn có thể vui lòng cung cấp giải thích?
TX_

10
@TX_ Nếu bạn muốn xóa asyncthì bạn sẽ chờ nhiệm vụ đầu tiên như thế nào? Bạn cần đánh dấu phương thức như asyncthể bạn muốn sử dụng bất kỳ sự chờ đợi nào . Nếu phương thức được đánh dấu là asyncvà bạn có awaitmã sớm hơn, thì bạn cần awaitthao tác async thứ hai để nó thuộc loại thích hợp. Nếu bạn vừa xóa awaitthì nó sẽ không biên dịch vì giá trị trả về sẽ không phải là loại thích hợp. Vì phương thức là asynckết quả luôn được bọc trong một tác vụ.
Phục vụ

11
@Noseratio Hãy thử hai. Các biên dịch đầu tiên. Cái thứ hai thì không. Thông báo lỗi sẽ cho bạn biết vấn đề. Bạn sẽ không trở lại đúng loại. Khi trong một asyncphương thức bạn không trả về một tác vụ, bạn trả về kết quả của nhiệm vụ sẽ được gói.
Phục vụ

3
@Servy, tất nhiên - bạn nói đúng. Trong trường hợp sau, chúng ta sẽ trả về Task<Type>một cách rõ ràng, trong khi asyncra lệnh trả về Type(mà trình biên dịch sẽ biến thành Task<Type>).
chơi

3
@Itsik Chắc chắn, asyncchỉ là cú pháp đường cho việc nối dây một cách rõ ràng. Bạn không cần async phải làm bất cứ điều gì, nhưng khi làm chỉ là về bất kỳ hoạt động không đồng bộ không tầm thường đó là đáng kể dễ dàng hơn để làm việc với. Ví dụ, mã bạn cung cấp không thực sự truyền lỗi như bạn muốn và việc thực hiện đúng trong các tình huống thậm chí phức tạp hơn bắt đầu trở nên khó khăn hơn rất nhiều. Mặc dù bạn không bao giờ cần async , các tình huống tôi mô tả là nơi nó tăng thêm giá trị để sử dụng nó.
Phục vụ

17

Một trường hợp khác bạn có thể cần chờ kết quả là trường hợp này:

async Task<IFoo> GetIFooAsync()
{
    return await GetFooAsync();
}

async Task<Foo> GetFooAsync()
{
    var foo = await CreateFooAsync();
    await foo.InitializeAsync();
    return foo;
}

Trong trường hợp này, GetIFooAsync()phải chờ kết quả GetFooAsyncvì loại Tkhác nhau giữa hai phương thức và Task<Foo>không được gán trực tiếp Task<IFoo>. Nhưng nếu bạn chờ đợi kết quả, nó sẽ trở thành Foothứ được gán trực tiếp IFoo. Sau đó, phương thức async chỉ đóng gói lại kết quả bên trong Task<IFoo>và bạn đi.


1
Đồng ý, điều này thực sự gây phiền nhiễu - tôi tin rằng nguyên nhân sâu xa Task<>là bất biến.
StuartLC

7

Làm cho phương thức "thunk" đơn giản khác không đồng bộ sẽ tạo ra một máy trạng thái không đồng bộ trong bộ nhớ trong khi phương thức không đồng bộ hóa thì không. Mặc dù điều đó thường có thể khiến mọi người sử dụng phiên bản không đồng bộ vì nó hiệu quả hơn (điều đó đúng) nhưng điều đó cũng có nghĩa là trong trường hợp bị treo, bạn không có bằng chứng nào cho thấy phương pháp đó có liên quan đến "ngăn xếp trả lại / tiếp tục" mà đôi khi làm cho nó khó khăn hơn để hiểu hang.

Vì vậy, vâng, khi perf không quan trọng (và thường là không) Tôi sẽ ném async vào tất cả các phương thức này để tôi có máy trạng thái async để giúp tôi chẩn đoán bị treo sau đó và cũng để đảm bảo rằng nếu chúng không phương pháp thunk từng phát triển theo thời gian, họ sẽ chắc chắn trả lại các nhiệm vụ bị lỗi thay vì ném.


6

Nếu bạn không sử dụng trả lại, chờ đợi, bạn có thể phá hỏng dấu vết ngăn xếp của mình trong khi gỡ lỗi hoặc khi nó được in trong nhật ký ngoại lệ.

Khi bạn trả lại tác vụ, phương thức đã hoàn thành mục đích của nó và nó nằm ngoài ngăn xếp cuộc gọi. Khi bạn sử dụng, return awaitbạn sẽ để nó trong ngăn xếp cuộc gọi.

Ví dụ:

Ngăn xếp cuộc gọi khi sử dụng await: A đang chờ tác vụ từ B => B đang chờ tác vụ từ C

Ngăn xếp cuộc gọi khi không sử dụng await: A đang chờ tác vụ từ C, mà B đã trả về.


2

Điều này cũng làm tôi bối rối và tôi cảm thấy rằng các câu trả lời trước đó đã bỏ qua câu hỏi thực tế của bạn:

Tại sao sử dụng cấu trúc trả về đang chờ khi bạn có thể trực tiếp trả về Tác vụ từ lệnh gọi DoAnotherT BreathAsync () bên trong?

Vâng đôi khi bạn thực sự muốn một Task<SomeType>, nhưng hầu hết thời gian bạn thực sự muốn một ví dụ SomeType, đó là, kết quả từ nhiệm vụ.

Từ mã của bạn:

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

Một người không quen thuộc với cú pháp (ví dụ như tôi) có thể nghĩ rằng phương thức này sẽ trả về một Task<SomeResult>, nhưng vì nó được đánh dấu async, nên có nghĩa là kiểu trả về thực tế của nó là SomeResult. Nếu bạn chỉ sử dụng return foo.DoAnotherThingAsync(), bạn sẽ trả về một Tác vụ, sẽ không biên dịch. Cách chính xác là trả về kết quả của nhiệm vụ, vì vậy return await.


1
"loại lợi nhuận thực tế". Hở? async / await không thay đổi loại trả lại. Trong ví dụ của bạn var task = DoSomethingAsync();sẽ cho bạn một nhiệm vụ, không phảiT
Giày

2
@Shoe Tôi không chắc là tôi hiểu rõ async/awaitđiều đó. Theo hiểu biết của tôi Task task = DoSomethingAsync(), trong khi Something something = await DoSomethingAsync()cả hai làm việc. Cái đầu tiên cung cấp cho bạn nhiệm vụ phù hợp, trong khi cái thứ hai, do awaittừ khóa, cung cấp cho bạn kết quả từ nhiệm vụ sau khi nó hoàn thành. Tôi có thể, ví dụ, có Task task = DoSomethingAsync(); Something something = await task;.
heltonbiker
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.