Sử dụng hiệu quả async / await với ASP.NET Web API


114

Tôi đang cố gắng sử dụng async/awaittính năng của ASP.NET trong dự án API Web của mình. Tôi không chắc liệu nó có tạo ra sự khác biệt nào về hiệu suất của dịch vụ Web API của tôi hay không. Vui lòng tìm bên dưới quy trình làm việc và mã mẫu từ ứng dụng của tôi.

Luồng công việc:

Ứng dụng giao diện người dùng → Điểm cuối API web (bộ điều khiển) → Phương thức gọi trong lớp dịch vụ API web → Gọi một dịch vụ web bên ngoài khác. (Ở đây chúng tôi có các tương tác DB, v.v.)

Bộ điều khiển:

public async Task<IHttpActionResult> GetCountries()
{
    var allCountrys = await CountryDataService.ReturnAllCountries();

    if (allCountrys.Success)
    {
        return Ok(allCountrys.Domain);
    }

    return InternalServerError();
}

Lớp dịch vụ:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    var response = _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");

    return Task.FromResult(response);
}

Tôi đã thử nghiệm đoạn mã trên và đang hoạt động. Nhưng tôi không chắc liệu đó có phải là cách sử dụng chính xác của async/await. Hãy chia sẻ suy nghĩ của bạn.


Câu trả lời:


202

Tôi không chắc liệu nó có tạo ra sự khác biệt nào về hiệu suất của API của tôi hay không.

Hãy nhớ rằng lợi ích chính của mã không đồng bộ ở phía máy chủ là khả năng mở rộng . Nó sẽ không làm cho các yêu cầu của bạn chạy nhanh hơn một cách kỳ diệu. Tôi đề cập đến một số asynccân nhắc " tôi có nên sử dụng " trong bài viết của tôi trên asyncASP.NET .

Tôi nghĩ rằng trường hợp sử dụng của bạn (gọi các API khác) rất phù hợp cho mã không đồng bộ, chỉ cần lưu ý rằng "không đồng bộ" không có nghĩa là "nhanh hơn". Cách tiếp cận tốt nhất là trước tiên làm cho giao diện người dùng của bạn đáp ứng và không đồng bộ; điều này sẽ làm cho ứng dụng của bạn cảm thấy nhanh hơn ngay cả khi nó hơi chậm.

Theo như mã đi, đây không phải là không đồng bộ:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
  var response = _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
  return Task.FromResult(response);
}

Bạn cần một triển khai không đồng bộ thực sự để nhận được các lợi ích về khả năng mở rộng của async:

public async Task<BackOfficeResponse<List<Country>>> ReturnAllCountriesAsync()
{
  return await _service.ProcessAsync<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
}

Hoặc (nếu logic của bạn trong phương pháp này thực sự chỉ là một sự chuyển giao):

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountriesAsync()
{
  return _service.ProcessAsync<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
}

Lưu ý rằng nó dễ dàng hơn để làm việc từ "trong ra ngoài" hơn là "từ ngoài vào trong" như thế này. Nói cách khác, không bắt đầu với hành động của bộ điều khiển không đồng bộ và sau đó buộc các phương thức hạ lưu trở thành không đồng bộ. Thay vào đó, hãy xác định các hoạt động không đồng bộ tự nhiên (gọi các API bên ngoài, truy vấn cơ sở dữ liệu, v.v.) và làm cho các hoạt động đó không đồng bộ ở mức thấp nhất trước ( Service.ProcessAsync). Sau đó, để asyncnhỏ giọt, làm cho các hành động của bộ điều khiển của bạn không đồng bộ như bước cuối cùng.

Và bạn không nên sử dụng Task.Runtrong trường hợp này trong trường hợp nào .


4
Cảm ơn Stephen vì những bình luận quý giá của bạn. Tôi đã thay đổi tất cả các phương thức lớp dịch vụ của mình thành không đồng bộ và bây giờ tôi gọi lệnh gọi REST bên ngoài của mình bằng cách sử dụng phương thức ExecuteTaskAsync và đang hoạt động như mong đợi. Cũng cảm ơn các bài đăng trên blog của bạn về async và Tasks. Nó thực sự đã giúp tôi rất nhiều để có được những hiểu biết ban đầu.
arp

5
Bạn có thể giải thích lý do tại sao bạn không sử dụng Task.Run ở đây?
Maarten

1
@Maarten: Tôi giải thích nó (ngắn gọn) trong bài báo tôi đã liên kết . Tôi đi vào chi tiết hơn trên blog của tôi .
Stephen Cleary

Tôi đã đọc bài viết của bạn Stephen và có vẻ như tránh đề cập đến điều gì đó. Khi một yêu cầu ASP.NET đến và bắt đầu, nó đang chạy trên một chuỗi nhóm luồng, điều đó tốt. Nhưng nếu nó trở nên không đồng bộ thì có nó sẽ bắt đầu xử lý không đồng bộ và luồng đó trở lại nhóm ngay lập tức. Nhưng công việc được thực hiện trong phương thức không đồng bộ sẽ tự thực thi trên một luồng nhóm luồng!
Hugh


12

Nó đúng, nhưng có lẽ không hữu ích.

Vì không có gì phải chờ - không có lệnh gọi đến các API chặn có thể hoạt động không đồng bộ - thì bạn đang thiết lập cấu trúc để theo dõi hoạt động không đồng bộ (có chi phí cao) nhưng sau đó không sử dụng khả năng đó.

Ví dụ: nếu lớp dịch vụ đang thực hiện các hoạt động DB với Entity Framework hỗ trợ các cuộc gọi không đồng bộ:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    using (db = myDBContext.Get()) {
      var list = await db.Countries.Where(condition).ToListAsync();

       return list;
    }
}

Bạn sẽ cho phép luồng công nhân làm việc khác trong khi db được truy vấn (và do đó có thể xử lý một yêu cầu khác).

Await có xu hướng là một cái gì đó cần phải đi xuống: rất khó để phù hợp trở lại với một hệ thống hiện có.


-1

Bạn không tận dụng hiệu quả async / await vì luồng yêu cầu sẽ bị chặn trong khi thực thi phương thức đồng bộReturnAllCountries()

Chuỗi được chỉ định để xử lý một yêu cầu sẽ ReturnAllCountries()không hoạt động trong khi nó hoạt động.

Nếu bạn có thể triển khai ReturnAllCountries()không đồng bộ, thì bạn sẽ thấy lợi ích về khả năng mở rộng. Điều này là do luồng có thể được phát hành trở lại nhóm luồng .NET để xử lý một yêu cầu khác trong khi ReturnAllCountries()đang thực thi. Điều này sẽ cho phép dịch vụ của bạn có thông lượng cao hơn, bằng cách sử dụng các luồng hiệu quả hơn.


Đây không phải là sự thật. Ổ cắm được kết nối nhưng luồng xử lý yêu cầu có thể làm một việc khác, chẳng hạn như xử lý một yêu cầu khác, trong khi chờ cuộc gọi dịch vụ.
Garr Godfrey

-8

Tôi sẽ thay đổi lớp dịch vụ của bạn thành:

public Task<BackOfficeResponse<List<Country>>> ReturnAllCountries()
{
    return Task.Run(() =>
    {
        return _service.Process<List<Country>>(BackOfficeEndpoint.CountryEndpoint, "returnCountries");
    }      
}

khi bạn có nó, bạn vẫn đang chạy _service.Process cuộc gọi đồng bộ và thu được rất ít hoặc không thu được lợi ích nào từ việc chờ đợi nó.

Với cách tiếp cận này, bạn đang kết thúc cuộc gọi có khả năng chậm trong một Task, khởi động nó và trả lại cuộc gọi đang chờ. Bây giờ bạn nhận được lợi ích của việc chờ đợi Task.


3
Theo liên kết blog , tôi có thể thấy rằng ngay cả khi chúng tôi sử dụng Task.Run, phương pháp vẫn chạy đồng bộ?
arp

Hừ! Có rất nhiều sự khác biệt giữa đoạn mã trên và liên kết đó. Của bạn Servicekhông phải là một phương thức không đồng bộ và không được chờ đợi trong Servicechính cuộc gọi. Tôi có thể hiểu quan điểm của anh ấy, nhưng tôi không tin rằng nó áp dụng ở đây.
Jonesopolis

8
Task.Runnên tránh trên ASP.NET. Cách tiếp cận này sẽ loại bỏ tất cả các lợi ích từ async/ awaitvà thực sự hoạt động kém hơn khi tải hơn là chỉ một cuộc gọi đồng bộ.
Stephen Cleary
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.