Thực hành tốt nhất để gọi ConfigureAwait cho tất cả mã phía máy chủ


561

Khi bạn có mã phía máy chủ (tức là một số ApiController) và các chức năng của bạn không đồng bộ - vì vậy chúng trả về Task<SomeObject>- đó có được coi là cách thực hành tốt nhất mà bất cứ khi nào bạn chờ đợi các chức năng mà bạn gọi ConfigureAwait(false)không?

Tôi đã đọc được rằng nó hiệu quả hơn vì nó không phải chuyển ngữ cảnh luồng trở lại bối cảnh luồng gốc. Tuy nhiên, với ASP.NET Web Api, nếu yêu cầu của bạn xuất hiện trên một luồng và bạn chờ đợi một số chức năng và cuộc gọi ConfigureAwait(false)có khả năng đưa bạn vào một luồng khác khi bạn trả về kết quả cuối cùng của ApiControllerchức năng.

Tôi đã gõ một ví dụ về những gì tôi đang nói dưới đây:

public class CustomerController : ApiController
{
    public async Task<Customer> Get(int id)
    {
        // you are on a particular thread here
        var customer = await SomeAsyncFunctionThatGetsCustomer(id).ConfigureAwait(false);

        // now you are on a different thread!  will that cause problems?
        return customer;
    }
}

Câu trả lời:


628

Cập nhật: ASP.NET Core không có aSynchronizationContext . Nếu bạn đang sử dụng ASP.NET Core, việc bạn có sử dụng ConfigureAwait(false)hay không không quan trọng.

Đối với ASP.NET "Full" hoặc "Classic" hoặc bất cứ điều gì, phần còn lại của câu trả lời này vẫn được áp dụng.

Bài gốc (đối với ASP.NET không phải Core):

Video này của nhóm ASP.NET có thông tin tốt nhất về việc sử dụng asynctrên ASP.NET.

Tôi đã đọc được rằng nó hiệu quả hơn vì nó không phải chuyển ngữ cảnh luồng trở lại bối cảnh luồng gốc.

Điều này đúng với các ứng dụng UI, nơi chỉ có một luồng UI mà bạn phải "đồng bộ hóa" trở lại.

Trong ASP.NET, tình hình phức tạp hơn một chút. Khi một asyncphương thức tiếp tục thực thi, nó lấy một luồng từ nhóm luồng ASP.NET. Nếu bạn vô hiệu hóa việc chụp bối cảnh bằng cách sử dụng ConfigureAwait(false), thì luồng sẽ tiếp tục thực thi phương thức trực tiếp. Nếu bạn không tắt chức năng chụp ngữ cảnh, thì luồng sẽ nhập lại ngữ cảnh yêu cầu và sau đó tiếp tục thực thi phương thức.

Vì vậy, ConfigureAwait(false)không tiết kiệm cho bạn một bước nhảy trong ASP.NET; nó giúp bạn tiết kiệm nhập lại bối cảnh yêu cầu, nhưng điều này thường rất nhanh. ConfigureAwait(false) thể hữu ích nếu bạn đang cố gắng thực hiện một lượng nhỏ xử lý song song một yêu cầu, nhưng thực sự TPL phù hợp hơn với hầu hết các kịch bản đó.

Tuy nhiên, với ASP.NET Web Api, nếu yêu cầu của bạn xuất hiện trên một luồng và bạn đang chờ một số chức năng và gọi ConfigureAwait (false) có khả năng đưa bạn vào một luồng khác khi bạn trả về kết quả cuối cùng của chức năng ApiControll của bạn .

Trên thực tế, chỉ cần làm một awaitcó thể làm điều đó. Khi asyncphương thức của bạn chạm một await, phương thức bị chặn nhưng luồng sẽ quay trở lại nhóm luồng. Khi phương thức đã sẵn sàng để tiếp tục, bất kỳ luồng nào được lấy từ nhóm luồng và được sử dụng để tiếp tục phương thức.

Sự khác biệt duy nhất ConfigureAwaitlàm cho ASP.NET là liệu luồng đó có vào ngữ cảnh yêu cầu khi tiếp tục phương thức hay không.

Tôi có thêm thông tin cơ bản trong bài viết MSDNSynchronizationContext của tôi trênasyncbài đăng blog giới thiệu của tôi .


23
Lưu trữ cục bộ không lưu chuyển bởi bất kỳ bối cảnh. HttpContext.Currentđược lưu chuyển bởi ASP.NET SynchronizationContext, được chảy theo mặc định khi bạn await, nhưng nó không được chảy theo ContinueWith. OTOH, bối cảnh thực thi (bao gồm các hạn chế bảo mật) là bối cảnh được đề cập trong CLR thông qua C # và được truyền bởi cả hai ContinueWithawait(ngay cả khi bạn sử dụng ConfigureAwait(false)).
Stephen Cleary

65
Sẽ không tuyệt sao nếu C # có hỗ trợ ngôn ngữ bản địa cho ConfigureAwait (false)? Một cái gì đó như 'awaitnc' (không chờ đợi ngữ cảnh). Gõ một cuộc gọi phương thức riêng biệt ở khắp mọi nơi là khá khó chịu. :)
NathanAldenSr

19
@NathanAldenSr: Nó đã được thảo luận khá nhiều. Vấn đề với một từ khóa mới là ConfigureAwaitthực sự chỉ có ý nghĩa khi bạn chờ đợi các nhiệm vụ , trong khi awaithành động theo bất kỳ " chờ đợi " nào. Các tùy chọn khác được xem xét là: Có nên loại bỏ hành vi mặc định nếu trong thư viện không? Hoặc có một cài đặt trình biên dịch cho hành vi bối cảnh mặc định? Cả hai đều bị từ chối vì khó đọc mã hơn và cho biết nó làm gì.
Stephen Cleary

10
@AnshulNigam: Đó là lý do tại sao các hành động của bộ điều khiển cần bối cảnh của chúng. Nhưng hầu hết các phương thức mà các hành động gọi là không.
Stephen Cleary

14
@JonathanRoeder: Nói chung, bạn không cần ConfigureAwait(false)phải tránh bế tắc Result/ Waitdựa trên nền tảng vì trên ASP.NET bạn không nên sử dụng Result/ Waitở vị trí đầu tiên.
Stephen Cleary

131

Trả lời ngắn gọn cho câu hỏi của bạn: Không. Bạn không nên gọi ConfigureAwait(false)ở cấp ứng dụng như thế.

Phiên bản TL; DR của câu trả lời dài: Nếu bạn đang viết thư viện nơi bạn không biết người tiêu dùng của mình và không cần bối cảnh đồng bộ hóa (mà bạn không nên có trong thư viện tôi tin), bạn nên luôn luôn sử dụng ConfigureAwait(false). Mặt khác, người tiêu dùng của thư viện của bạn có thể phải đối mặt với những bế tắc bằng cách sử dụng các phương thức không đồng bộ của bạn theo kiểu chặn. Điều này phụ thuộc vào tình hình.

Dưới đây là một lời giải thích chi tiết hơn về tầm quan trọng của ConfigureAwaitphương pháp (trích dẫn từ bài đăng trên blog của tôi):

Khi bạn đang chờ đợi một phương thức với từ khóa đang chờ, trình biên dịch sẽ tạo ra một loạt mã thay cho bạn. Một trong những mục đích của hành động này là xử lý đồng bộ hóa với luồng UI (hoặc chính). Thành phần chính của tính năng này là SynchronizationContext.Currentbối cảnh đồng bộ hóa cho luồng hiện tại. SynchronizationContext.Currentđược điền tùy thuộc vào môi trường bạn đang ở. GetAwaiterPhương thức của Nhiệm vụ tìm kiếm SynchronizationContext.Current. Nếu bối cảnh đồng bộ hóa hiện tại không phải là null, việc tiếp tục được chuyển đến người phục vụ đó sẽ được đăng trở lại bối cảnh đồng bộ hóa đó.

Khi sử dụng một phương thức sử dụng các tính năng ngôn ngữ không đồng bộ mới, theo kiểu chặn, bạn sẽ kết thúc với một bế tắc nếu bạn có Đồng bộ hóa có sẵn. Khi bạn đang sử dụng các phương thức như vậy theo kiểu chặn (chờ trên phương thức Nhiệm vụ với Chờ hoặc lấy kết quả trực tiếp từ thuộc tính Kết quả của Tác vụ), bạn sẽ chặn luồng chính cùng một lúc. Cuối cùng, khi Nhiệm vụ hoàn thành bên trong phương thức đó trong luồng, nó sẽ gọi tiếp tục để gửi lại cho luồng chính vì SynchronizationContext.Currentcó sẵn và bị bắt. Nhưng có một vấn đề ở đây: luồng UI bị chặn và bạn gặp bế tắc!

Ngoài ra, đây là hai bài viết tuyệt vời cho bạn chính xác cho câu hỏi của bạn:

Cuối cùng, có một đoạn video ngắn tuyệt vời từ Lucian Wischik chính xác về chủ đề này: Các phương pháp thư viện Async nên xem xét sử dụng Task.ConfigureAwait (false) .

Hi vọng điêu nay co ich.


2
"Phương thức tác vụ GetAwaiter tìm kiếm SyncizationContext.C hiện tại. Nếu bối cảnh đồng bộ hóa hiện tại không phải là null, việc tiếp tục được chuyển đến người phục vụ đó sẽ được đưa trở lại bối cảnh đồng bộ hóa đó." - Tôi đang có ấn tượng rằng bạn đang cố gắng nói rằng Taskđi bộ ngăn xếp để có được SynchronizationContext, đó là sai. Cái SynchronizationContextđược lấy trước khi cuộc gọi đến Taskvà sau đó phần còn lại của mã được tiếp tục trên SynchronizationContextif nếu SynchronizationContext.Currentkhông phải là null.
casperOne

1
@casperOne Tôi có ý định nói như vậy.
tugberk

8
Không phải là trách nhiệm của người gọi để đảm bảo rằng SynchronizationContext.Currentrõ ràng / hoặc rằng thư viện được gọi trong một Task.Run()thay vì phải viết .ConfigureAwait(false)tất cả các thư viện lớp?
b Liệu

1
@binki - mặt khác: (1) có lẽ một thư viện được sử dụng trong nhiều ứng dụng, do đó, nỗ lực một lần trong thư viện để làm cho ứng dụng dễ dàng hơn là hiệu quả về chi phí; (2) có lẽ tác giả thư viện biết rằng anh ta đã viết mã mà không có lý do gì để yêu cầu tiếp tục trên bối cảnh ban đầu, mà anh ta thể hiện bởi những người .ConfigureAwait(false)đó. Có lẽ sẽ dễ dàng hơn cho các tác giả thư viện nếu đó là hành vi mặc định, nhưng tôi cho rằng việc viết thư viện một cách khó khăn hơn một chút sẽ tốt hơn là viết một ứng dụng chính xác hơn một chút.
ToolmakerSteve

4
Tại sao tác giả của một thư viện mã hóa người tiêu dùng? Nếu người tiêu dùng muốn bế tắc, tại sao tôi phải ngăn chặn họ?
Quarkly

25

Hạn chế lớn nhất mà tôi đã tìm thấy khi sử dụng ConfigureAwait (false) là văn hóa luồng được hoàn nguyên về mặc định của hệ thống. Nếu bạn đã cấu hình một nền văn hóa, ví dụ ...

<system.web>
    <globalization culture="en-AU" uiCulture="en-AU" />    
    ...

và bạn đang lưu trữ trên một máy chủ có văn hóa được đặt thành en-US, sau đó bạn sẽ tìm thấy trước khi ConfigureAwait (false) được gọi là CultureInfo.CiverseCARM sẽ trả lại en-AU và sau khi bạn sẽ nhận được en-US. I E

// CultureInfo.CurrentCulture ~ {en-AU}
await xxxx.ConfigureAwait(false);
// CultureInfo.CurrentCulture ~ {en-US}

Nếu ứng dụng của bạn đang làm bất cứ điều gì đòi hỏi định dạng dữ liệu cụ thể về văn hóa, thì bạn sẽ cần lưu ý điều này khi sử dụng ConfigureAwait (false).


27
Các phiên bản hiện đại của .NET (tôi nghĩ từ 4.6?) Sẽ truyền bá văn hóa qua các luồng, ngay cả khi ConfigureAwait(false)được sử dụng.
Stephen Cleary

1
Cảm ơn bạn về thông tin. Chúng tôi thực sự đang sử dụng .net 4.5.2
Mick

11

Tôi có một số suy nghĩ chung về việc thực hiện Task:

  1. Nhiệm vụ là dùng một lần nhưng chúng tôi không được phép sử dụng using.
  2. ConfigureAwaitđã được giới thiệu trong 4.5. Taskđược giới thiệu vào 4.0.
  3. Chủ đề .NET luôn được sử dụng để chuyển ngữ cảnh (xem C # qua sách CLR) nhưng trong quá trình triển khai mặc định, Task.ContinueWithchúng không nhận ra rằng chuyển đổi ngữ cảnh rất tốn kém và nó bị tắt theo mặc định.
  4. Vấn đề là một nhà phát triển thư viện không nên quan tâm liệu khách hàng của mình có cần luồng ngữ cảnh hay không do đó không nên quyết định có chảy luồng ngữ cảnh hay không.
  5. [Đã thêm sau] Thực tế là không có câu trả lời chính đáng và tài liệu tham khảo phù hợp và chúng tôi tiếp tục đấu tranh vì điều này có nghĩa là ai đó đã không thực hiện đúng công việc của họ.

Tôi đã có một vài bài viết về chủ đề này nhưng tôi thực hiện - ngoài câu trả lời hay của Tugberk - là bạn nên biến tất cả các API không đồng bộ và lý tưởng hóa luồng ngữ cảnh. Vì bạn đang thực hiện async, bạn chỉ có thể sử dụng các phần tiếp theo thay vì chờ đợi vì vậy sẽ không có bế tắc vì không có sự chờ đợi nào được thực hiện trong thư viện và bạn giữ dòng chảy để bối cảnh được giữ nguyên (chẳng hạn như httpContext).

Vấn đề là khi thư viện trưng ra API đồng bộ nhưng sử dụng API không đồng bộ khác - do đó bạn cần sử dụng Wait()/ Resulttrong mã của mình.


6
1) Bạn có thể gọi Task.Disposenếu bạn muốn; bạn không cần phần lớn thời gian. 2) Taskđược giới thiệu trong .NET 4.0 như một phần của TPL, không cần thiết ConfigureAwait; Khi asyncđược thêm vào, họ đã sử dụng lại Taskloại hiện có thay vì phát minh ra loại mới Future.
Stephen Cleary

6
3) Bạn đang nhầm lẫn hai loại "bối cảnh" khác nhau. "Bối cảnh" được đề cập trong C # thông qua CLR luôn được truyền đi, ngay cả trong Tasks; "bối cảnh" được kiểm soát bởi ContinueWithlà một SynchronizationContexthoặc TaskScheduler. Những bối cảnh khác nhau được giải thích chi tiết trên blog của Stephen Toub .
Stephen Cleary

21
4) Tác giả thư viện không cần quan tâm liệu người gọi của nó có cần luồng ngữ cảnh hay không, bởi vì mỗi phương thức không đồng bộ tiếp tục độc lập. Vì vậy, nếu người gọi cần dòng chảy ngữ cảnh, họ có thể lưu nó, bất kể tác giả thư viện có chảy nó hay không.
Stephen Cleary

1
Lúc đầu, bạn dường như đang phàn nàn thay vì trả lời câu hỏi. Và sau đó, bạn đang nói về các bối cảnh, trong đó, ngoại trừ có một số loại bối cảnh trong .Net và bạn thực sự không rõ bạn đang nói về cái nào (hoặc cái nào?). Và ngay cả khi bạn không tự nhầm lẫn (nhưng tôi nghĩ là bạn, tôi tin rằng không có ngữ cảnh nào được sử dụng để chạy với Threads, nhưng không còn nữa với ContinueWith()), điều này làm cho câu trả lời của bạn trở nên khó hiểu.
Svick

1
@StephenCleary vâng, lib dev không cần biết, nó tùy thuộc vào khách hàng. Tôi nghĩ rằng tôi đã làm cho nó rõ ràng, nhưng phrasing của tôi không rõ ràng.
Aliostad
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.