Gói mã đồng bộ vào cuộc gọi không đồng bộ


95

Tôi có một phương thức trong ứng dụng ASP.NET, tiêu tốn khá nhiều thời gian để hoàn thành. Một lệnh gọi đến phương thức này có thể xảy ra tối đa 3 lần trong một yêu cầu của người dùng, tùy thuộc vào trạng thái bộ nhớ cache và các tham số mà người dùng cung cấp. Mỗi cuộc gọi mất khoảng 1-2 giây để hoàn thành. Bản thân phương thức là lời gọi đồng bộ tới dịch vụ và không có khả năng ghi đè việc triển khai.
Vì vậy, cuộc gọi đồng bộ đến dịch vụ trông giống như sau:

public OutputModel Calculate(InputModel input)
{
    // do some stuff
    return Service.LongRunningCall(input);
}

Và cách sử dụng phương thức là (lưu ý, việc gọi phương thức đó có thể xảy ra nhiều lần):

private void MakeRequest()
{
    // a lot of other stuff: preparing requests, sending/processing other requests, etc.
    var myOutput = Calculate(myInput);
    // stuff again
}

Tôi đã cố gắng thay đổi cách triển khai từ phía mình để cung cấp công việc đồng thời của phương pháp này và đây là những gì tôi đã đạt được cho đến nay.

public async Task<OutputModel> CalculateAsync(InputModel input)
{
    return await Task.Run(() =>
    {
        return Calculate(input);
    });
}

Cách sử dụng (một phần của mã "làm việc khác" chạy đồng thời với lệnh gọi đến dịch vụ):

private async Task MakeRequest()
{
    // do some stuff
    var task = CalculateAsync(myInput);
    // do other stuff
    var myOutput = await task;
    // some more stuff
}

Câu hỏi của tôi là như sau. Tôi có sử dụng phương pháp phù hợp để tăng tốc độ thực thi trong ứng dụng ASP.NET hay tôi đang thực hiện công việc không cần thiết khi cố gắng chạy mã đồng bộ không đồng bộ? Bất cứ ai có thể giải thích tại sao cách tiếp cận thứ hai không phải là một tùy chọn trong ASP.NET (nếu nó thực sự không phải)? Ngoài ra, nếu cách tiếp cận như vậy có thể áp dụng, tôi có cần gọi phương thức không đồng bộ như vậy không nếu đó là lệnh gọi duy nhất mà chúng tôi có thể thực hiện vào lúc này (tôi gặp trường hợp như vậy, khi không có việc nào khác phải làm trong khi chờ hoàn thành)?
Hầu hết các bài viết trên mạng về chủ đề này đều đề cập đến việc sử dụng cách async-awaittiếp cận với mã, đã cung cấp awaitablecác phương pháp, nhưng đó không phải là trường hợp của tôi. Đâylà bài viết hay mô tả trường hợp của tôi, không mô tả tình huống của các cuộc gọi song song, từ chối tùy chọn kết thúc cuộc gọi đồng bộ, nhưng theo ý kiến ​​của tôi, tình huống của tôi chính xác là cơ hội để làm điều đó.
Cảm ơn trước cho sự giúp đỡ và lời khuyên.

Câu trả lời:


116

Điều quan trọng là phải phân biệt giữa hai loại đồng thời khác nhau. Đồng thời không đồng bộ là khi bạn có nhiều hoạt động không đồng bộ trong chuyến bay (và vì mỗi hoạt động là không đồng bộ, không ai trong số chúng thực sự đang sử dụng một luồng ). Đồng thời song song là khi bạn có nhiều luồng, mỗi luồng thực hiện một hoạt động riêng biệt.

Điều đầu tiên cần làm là đánh giá lại giả định này:

Bản thân phương thức là lời gọi đồng bộ tới dịch vụ và không có khả năng ghi đè việc triển khai.

Nếu "dịch vụ" của bạn là một dịch vụ web hoặc bất kỳ thứ gì khác bị ràng buộc I / O, thì giải pháp tốt nhất là viết một API không đồng bộ cho nó.

Tôi sẽ tiếp tục với giả định rằng "dịch vụ" của bạn là một hoạt động liên kết với CPU phải thực thi trên cùng một máy với máy chủ web.

Nếu đúng như vậy, thì điều tiếp theo cần đánh giá là một giả định khác:

Tôi cần yêu cầu thực thi nhanh hơn.

Bạn có chắc chắn đó là những gì bạn cần làm không? Có bất kỳ thay đổi giao diện người dùng nào mà bạn có thể thực hiện thay thế - ví dụ: bắt đầu yêu cầu và cho phép người dùng thực hiện công việc khác trong khi xử lý không?

Tôi sẽ tiếp tục với giả định rằng có, bạn thực sự cần làm cho yêu cầu riêng lẻ thực thi nhanh hơn.

Trong trường hợp này, bạn sẽ cần thực thi mã song song trên máy chủ web của mình. Điều này chắc chắn không được khuyến khích nói chung vì mã song song sẽ sử dụng các luồng mà ASP.NET có thể cần để xử lý các yêu cầu khác và bằng cách xóa / thêm các luồng, nó sẽ loại bỏ heuristics của ASP.NET threadpool. Vì vậy, quyết định này có ảnh hưởng đến toàn bộ máy chủ của bạn.

Khi bạn sử dụng mã song song trên ASP.NET, bạn đang đưa ra quyết định thực sự hạn chế khả năng mở rộng của ứng dụng web của mình. Bạn cũng có thể thấy một số lượng đáng kể chuỗi bị gián đoạn, đặc biệt nếu các yêu cầu của bạn quá khó. Tôi khuyên bạn chỉ nên sử dụng mã song song trên ASP.NET nếu bạn biết rằng số lượng người dùng đồng thời sẽ khá thấp (tức là không phải là máy chủ công cộng).

Vì vậy, nếu bạn đạt được điều này và bạn chắc chắn muốn thực hiện xử lý song song trên ASP.NET, thì bạn có một vài lựa chọn.

Một trong những phương pháp dễ dàng hơn là sử dụng Task.Run, rất giống với mã hiện có của bạn. Tuy nhiên, tôi không khuyên bạn nên triển khai một CalculateAsyncphương thức vì điều đó có nghĩa là quá trình xử lý là không đồng bộ (nó không phải vậy). Thay vào đó, hãy sử dụng Task.Runtại điểm của cuộc gọi:

private async Task MakeRequest()
{
  // do some stuff
  var task = Task.Run(() => Calculate(myInput));
  // do other stuff
  var myOutput = await task;
  // some more stuff
}

Ngoài ra, nếu nó hoạt động tốt với mã của bạn, bạn có thể sử dụng các Parallelloại, ví dụ Parallel.For, Parallel.ForEachhoặc Parallel.Invoke. Ưu điểm của Parallelmã là luồng yêu cầu được sử dụng như một trong các luồng song song và sau đó tiếp tục thực thi trong ngữ cảnh luồng (có ít chuyển đổi ngữ cảnh hơn asyncví dụ):

private void MakeRequest()
{
  Parallel.Invoke(() => Calculate(myInput1),
      () => Calculate(myInput2),
      () => Calculate(myInput3));
}

Tôi không khuyến khích sử dụng LINQ song song (PLINQ) trên ASP.NET cả.


1
Cảm ơn rất nhiều cho một câu trả lời chi tiết. Một điều nữa tôi muốn làm rõ. Khi tôi bắt đầu thực thi bằng cách sử dụng Task.Run, nội dung được chạy trong một luồng khác, được lấy từ nhóm luồng. Sau đó, không cần thiết phải gói các cuộc gọi chặn (chẳng hạn như lệnh gọi đến dịch vụ của tôi) thành Runs, bởi vì chúng sẽ luôn sử dụng mỗi luồng một, sẽ bị chặn trong khi thực thi phương thức. Trong tình huống như vậy, lợi ích duy nhất còn lại async-awaittrong trường hợp của tôi là thực hiện một số hành động cùng một lúc. Vui long sửa cho tôi nêu tôi sai.
Eadel

2
Có, lợi ích duy nhất bạn nhận được từ Run(hoặc Parallel) là tính đồng thời. Các hoạt động vẫn là mỗi chặn một luồng. Vì bạn nói rằng dịch vụ này là một dịch vụ web, thì tôi không khuyên bạn nên sử dụng Runhoặc Parallel; thay vào đó, hãy viết một API không đồng bộ cho dịch vụ.
Stephen Cleary

3
@StephenCleary. ý bạn là gì bởi "... và vì mỗi hoạt động là không đồng bộ, không ai trong số chúng thực sự đang sử dụng một chuỗi ..." cảm ơn!
alltej

3
@alltej: Đối với mã không đồng bộ thực sự, không có luồng .
Stephen Cleary

4
@JoshuaFrank: Không trực tiếp. Theo định nghĩa, mã đằng sau một API đồng bộ phải chặn chuỗi gọi. Bạn có thể sử dụng một API không đồng bộ như BeginGetResponsevà bắt đầu một vài trong số đó, sau đó chặn (đồng bộ) cho đến khi tất cả chúng hoàn tất, nhưng kiểu tiếp cận này khá phức tạp - xem blog.stephencleary.com/2012/07/dont-block-on -async-code.htmlmsdn.microsoft.com/en-us/magazine/mt238404.aspx . Thường sẽ dễ dàng và sạch sẽ hơn để áp dụng asyncmọi cách, nếu có thể.
Stephen Cleary

1

Tôi nhận thấy rằng đoạn mã sau có thể chuyển đổi một Tác vụ thành luôn chạy không đồng bộ

private static async Task<T> ForceAsync<T>(Func<Task<T>> func)
{
    await Task.Yield();
    return await func();
}

và tôi đã sử dụng nó theo cách sau

await ForceAsync(() => AsyncTaskWithNoAwaits())

Thao tác này sẽ thực thi bất kỳ Tác vụ nào một cách không đồng bộ để bạn có thể kết hợp chúng trong các kịch bản WhenAll, WhenAny và các mục đích sử dụng khác.

Bạn cũng có thể chỉ cần thêm Task.Yield () làm dòng đầu tiên của mã được gọi của bạn.

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.