Khi sử dụng chính xác Task.Run và khi chỉ async-await


317

Tôi muốn hỏi bạn về ý kiến ​​của bạn về kiến ​​trúc chính xác khi sử dụng Task.Run. Tôi đang gặp phải giao diện người dùng bị lag trong ứng dụng WPF .NET 4.5 của chúng tôi (với khung Caliburn Micro).

Về cơ bản tôi đang làm (đoạn mã rất đơn giản):

public class PageViewModel : IHandle<SomeMessage>
{
   ...

   public async void Handle(SomeMessage message)
   {
      ShowLoadingAnimation();

      // Makes UI very laggy, but still not dead
      await this.contentLoader.LoadContentAsync();

      HideLoadingAnimation();
   }
}

public class ContentLoader
{
    public async Task LoadContentAsync()
    {
        await DoCpuBoundWorkAsync();
        await DoIoBoundWorkAsync();
        await DoCpuBoundWorkAsync();

        // I am not really sure what all I can consider as CPU bound as slowing down the UI
        await DoSomeOtherWorkAsync();
    }
}

Từ các bài viết / video tôi đã đọc / xem, tôi biết rằng await asynckhông nhất thiết phải chạy trên một chủ đề nền và để bắt đầu công việc trong nền bạn cần phải chờ nó Task.Run(async () => ... ). Việc sử dụng async awaitkhông chặn UI, nhưng nó vẫn đang chạy trên luồng UI, vì vậy nó làm cho nó bị lag.

Đâu là nơi tốt nhất để đặt Task.Run?

Tôi có nên chỉ

  1. Kết thúc cuộc gọi bên ngoài vì đây là công việc phân luồng ít hơn cho .NET

  2. hoặc tôi chỉ nên bọc các phương thức gắn với CPU bên trong khi chạy với Task.Runđiều này làm cho nó có thể sử dụng lại cho những nơi khác? Tôi không chắc chắn ở đây nếu bắt đầu làm việc trên các chủ đề nền sâu trong cốt lõi là một ý tưởng tốt.

Quảng cáo (1), giải pháp đầu tiên sẽ như thế này:

public async void Handle(SomeMessage message)
{
    ShowLoadingAnimation();
    await Task.Run(async () => await this.contentLoader.LoadContentAsync());
    HideLoadingAnimation();
}

// Other methods do not use Task.Run as everything regardless
// if I/O or CPU bound would now run in the background.

Quảng cáo (2), giải pháp thứ hai sẽ như thế này:

public async Task DoCpuBoundWorkAsync()
{
    await Task.Run(() => {
        // Do lot of work here
    });
}

public async Task DoSomeOtherWorkAsync(
{
    // I am not sure how to handle this methods -
    // probably need to test one by one, if it is slowing down UI
}

BTW, dòng trong (1) await Task.Run(async () => await this.contentLoader.LoadContentAsync());nên đơn giản là await Task.Run( () => this.contentLoader.LoadContentAsync() );. AFAIK bạn không đạt được bất cứ điều gì bằng cách thêm một giây awaitasyncbên trong Task.Run. Và vì bạn không truyền tham số, điều đó đơn giản hóa hơn một chút await Task.Run( this.contentLoader.LoadContentAsync );.
ToolmakerSteve

thực sự có một chút khác biệt nếu bạn có sự chờ đợi thứ hai bên trong. Xem bài viết này . Tôi thấy nó rất hữu ích, chỉ với điểm đặc biệt này, tôi không đồng ý và thích quay lại trực tiếp Nhiệm vụ thay vì chờ đợi. (như bạn đề xuất trong bình luận của bạn)
Lukas K

Câu trả lời:


365

Lưu ý các nguyên tắc để thực hiện công việc trên một luồng UI , được thu thập trên blog của tôi:

  • Không chặn luồng UI trong hơn 50ms một lần.
  • Bạn có thể lên lịch ~ 100 phần tiếp theo trên luồng UI mỗi giây; 1000 là quá nhiều.

Có hai kỹ thuật bạn nên sử dụng:

1) Sử dụng ConfigureAwait(false)khi bạn có thể.

Ví dụ, await MyAsync().ConfigureAwait(false);thay vì await MyAsync();.

ConfigureAwait(false)nói awaitrằng bạn không cần phải tiếp tục trong bối cảnh hiện tại (trong trường hợp này, "trên bối cảnh hiện tại" có nghĩa là "trên luồng UI"). Tuy nhiên, đối với phần còn lại của asyncphương thức đó (sau ConfigureAwait), bạn không thể làm bất cứ điều gì giả định rằng bạn đang ở trong bối cảnh hiện tại (ví dụ: cập nhật các thành phần UI).

Để biết thêm thông tin, hãy xem bài viết MSDN của tôi Thực tiễn tốt nhất trong lập trình không đồng bộ .

2) Sử dụng Task.Runđể gọi các phương thức giới hạn CPU.

Bạn nên sử dụng Task.Run, nhưng không phải trong bất kỳ mã nào bạn muốn có thể sử dụng lại (ví dụ: mã thư viện). Vì vậy, bạn sử dụng Task.Runđể gọi phương thức, không phải là một phần của việc thực hiện phương thức.

Vì vậy, công việc hoàn toàn bị ràng buộc bởi CPU sẽ như thế này:

// Documentation: This method is CPU-bound.
void DoWork();

Mà bạn sẽ gọi bằng cách sử dụng Task.Run:

await Task.Run(() => DoWork());

Các phương thức là hỗn hợp của ràng buộc CPU và ràng buộc I / O phải có Asyncchữ ký với tài liệu chỉ ra bản chất ràng buộc CPU của chúng:

// Documentation: This method is CPU-bound.
Task DoWorkAsync();

Mà bạn cũng sẽ gọi bằng cách sử dụng Task.Run(vì nó bị ràng buộc một phần CPU):

await Task.Run(() => DoWorkAsync());

4
Cảm ơn sự tôn trọng nhanh chóng của bạn! Tôi biết liên kết bạn đã đăng và xem các video được tham chiếu trong blog của bạn. Trên thực tế đó là lý do tại sao tôi đăng câu hỏi này - trong video được nói (giống như trong phản hồi của bạn), bạn không nên sử dụng Task.Run trong mã lõi. Nhưng vấn đề của tôi là, tôi cần phải bọc phương thức đó mỗi lần tôi sử dụng nó để không làm chậm phản hồi (xin lưu ý tất cả mã của tôi là không đồng bộ và không bị chặn, nhưng không có Thread.Run thì nó chỉ bị lag). Tôi cũng bối rối không biết liệu có phải là cách tiếp cận tốt hơn để chỉ bọc các phương thức bị ràng buộc của CPU (nhiều lệnh gọi của Task.Run) hoặc bọc hoàn toàn mọi thứ trong một Nhiệm vụ.Run không?
Lukas K

12
Tất cả các phương pháp thư viện của bạn nên được sử dụng ConfigureAwait(false). Nếu bạn làm điều đó trước, thì bạn có thể thấy điều đó Task.Runlà hoàn toàn không cần thiết. Nếu bạn làm vẫn cần Task.Run, sau đó nó không làm cho nhiều sự khác biệt với thời gian chạy trong trường hợp này cho dù bạn gọi nó là một lần hoặc nhiều lần, vì vậy chỉ cần làm những gì tự nhiên nhất cho mã của bạn.
Stephen Cleary

2
Tôi không hiểu kỹ thuật đầu tiên sẽ giúp anh ta như thế nào. Ngay cả khi bạn sử dụng ConfigureAwait(false)phương thức giới hạn cpu của mình, thì đó vẫn là luồng UI sẽ thực hiện phương thức ràng buộc cpu và chỉ mọi thứ sau đó mới có thể được thực hiện trên luồng TP. Hay tôi đã hiểu nhầm điều gì?
Darius

4
@ user4205580: Không, Task.Runhiểu chữ ký không đồng bộ, vì vậy nó sẽ không hoàn thành cho đến khi DoWorkAsynchoàn tất. Việc thêm async/ awaitlà không cần thiết. Tôi giải thích thêm về "tại sao" trong loạt bài viết về Task.Runnghi thức xã giao của tôi .
Stephen Cleary

3
@ user4205580: Không. Phần lớn các phương thức không đồng bộ "lõi" không sử dụng nội bộ. Cách thông thường để thực hiện phương pháp không đồng bộ "cốt lõi" là sử dụng TaskCompletionSource<T>hoặc một trong các ký hiệu viết tắt của nó như FromAsync. Tôi có một bài đăng blog đi sâu vào chi tiết hơn tại sao các phương thức async không yêu cầu chủ đề .
Stephen Cleary

12

Một vấn đề với ContentLoader của bạn là bên trong nó hoạt động tuần tự. Một mô hình tốt hơn là song song hóa công việc và sau đó đồng bộ hóa ở cuối, để chúng tôi nhận được

public class PageViewModel : IHandle<SomeMessage>
{
   ...

   public async void Handle(SomeMessage message)
   {
      ShowLoadingAnimation();

      // makes UI very laggy, but still not dead
      await this.contentLoader.LoadContentAsync(); 

      HideLoadingAnimation();   
   }
}

public class ContentLoader 
{
    public async Task LoadContentAsync()
    {
        var tasks = new List<Task>();
        tasks.Add(DoCpuBoundWorkAsync());
        tasks.Add(DoIoBoundWorkAsync());
        tasks.Add(DoCpuBoundWorkAsync());
        tasks.Add(DoSomeOtherWorkAsync());

        await Task.WhenAll(tasks).ConfigureAwait(false);
    }
}

Rõ ràng, điều này không hoạt động nếu bất kỳ tác vụ nào yêu cầu dữ liệu từ các tác vụ khác trước đó, nhưng sẽ cung cấp cho bạn thông lượng tổng thể tốt hơn cho hầu hết các kịch 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.