Việc sử dụng await khác với việc sử dụng ContinWith như thế nào khi xử lý các tác vụ không đồng bộ?


8

Ý tôi là đây:

public Task<SomeObject> GetSomeObjectByTokenAsync(int id)
    {
        string token = repository.GetTokenById(id);
        if (string.IsNullOrEmpty(token))
        {
            return Task.FromResult(new SomeObject()
            {
                IsAuthorized = false
            });
        }
        else
        {
            return repository.GetSomeObjectByTokenAsync(token).ContinueWith(t =>
            {
                t.Result.IsAuthorized = true;
                return t.Result;
            });
        }
    }

Phương pháp nêu trên có thể được chờ đợi và tôi nghĩ rằng nó rất giống với những gì T hỏi dựa trên Một đồng bộ P attern đề nghị làm gì? (Các mẫu khác mà tôi biết là các mẫu APMEAP .)

Bây giờ, những gì về mã sau đây:

public async Task<SomeObject> GetSomeObjectByToken(int id)
    {
        string token = repository.GetTokenById(id);
        if (string.IsNullOrEmpty(token))
        {
            return new SomeObject()
            {
                IsAuthorized = false
            };
        }
        else
        {
            SomeObject result = await repository.GetSomeObjectByTokenAsync(token);
            result.IsAuthorized = true;
            return result;
        }
    }

Sự khác biệt chính ở đây là phương thức này asyncvà nó sử dụng các awaittừ khóa - vậy điều này có gì thay đổi so với phương pháp được viết trước đó? Tôi biết nó cũng có thể - được chờ đợi. Bất kỳ phương thức nào trả về Nhiệm vụ đều có thể cho vấn đề đó, trừ khi tôi nhầm.

Tôi biết về máy trạng thái được tạo bằng các câu lệnh chuyển đổi đó bất cứ khi nào một phương thức được gắn nhãn là asyncvà tôi biết rằng awaitchính nó không sử dụng luồng nào - nó hoàn toàn không chặn, luồng chỉ đơn giản là thực hiện các thao tác khác, cho đến khi nó được gọi lại để tiếp tục thực thi đoạn mã trên.

Nhưng sự khác biệt cơ bản giữa hai phương pháp là gì, khi chúng ta gọi chúng bằng awaittừ khóa? Có sự khác biệt nào không, và nếu có - cái nào được ưa thích?

EDIT: Tôi cảm thấy như đoạn mã đầu tiên được ưa thích, bởi vì chúng có hiệu quả bõ mẫu âm chót các async / chờ đợi từ khóa, mà không cần bất kỳ hậu quả - chúng tôi trả lại một nhiệm vụ mà sẽ tiếp tục thực hiện của nó đồng bộ, hoặc một nhiệm vụ đã hoàn thành trên con đường nóng (có thể lưu trữ).


3
Rất ít, trong trường hợp này. Trong ví dụ đầu tiên của bạn, result.IsAuthorized = truesẽ được chạy trên nhóm luồng, trong khi trong ví dụ thứ hai, nó có thể được chạy trên cùng một luồng đã gọi GetSomeObjectByToken(nếu nó đã được SynchronizationContextcài đặt trên nó, ví dụ như đó là luồng UI). Hành vi nếu GetSomeObjectByTokenAsyncném một ngoại lệ cũng sẽ hơi khác nhau. Nói chung awaitđược ưa thích hơn ContinueWith, vì nó hầu như luôn luôn dễ đọc hơn.
cant7

1
Trong bài viết này, nó được gọi là "eliding", có vẻ như là một từ khá tốt cho nó. Stephen chắc chắn biết kinh doanh của mình. Kết luận của bài viết về cơ bản là nó không quan trọng lắm, hiệu năng khôn ngoan, nhưng có những cạm bẫy mã hóa nhất định nếu bạn không chờ đợi. Ví dụ thứ hai của bạn là một mô hình an toàn hơn.
John Wu

1
Bạn có thể quan tâm đến cuộc thảo luận twitter này từ Kiến trúc sư phần mềm đối tác tại Microsoft trong nhóm ASP.NET: twitter.com/davidfowl/status/1044847039929028608?lang=vi
Remy

Nó được gọi là "máy trạng thái", không phải là "máy ảo".
Enigmativity

@Enigmativity Cảm ơn bạn thông thái, tôi quên chỉnh sửa điều đó!
SpiritBob

Câu trả lời:


5

Cơ chế async/ awaitlàm cho trình biên dịch biến mã của bạn thành một máy trạng thái. Mã của bạn sẽ chạy đồng bộ cho đến lần đầu tiênawait đạt đến mức chờ đợi chưa hoàn thành, nếu có.

Trong trình biên dịch Microsoft C #, máy trạng thái này là một loại giá trị, có nghĩa là nó sẽ có chi phí rất nhỏ khi tất cả await được hoàn thành chờ đợi, vì nó sẽ không phân bổ một đối tượng và do đó, nó sẽ không tạo ra rác. Khi bất kỳ sự chờ đợi nào không được hoàn thành, loại giá trị này chắc chắn được đóng hộp.

Lưu ý rằng điều này không tránh phân bổ Tasks nếu đó là loại chờ được sử dụng trong các awaitbiểu thức.

Với ContinueWith, bạn chỉ tránh phân bổ (trừ Task) nếu việc tiếp tục của bạn không có kết thúc và nếu bạn không sử dụng một đối tượng trạng thái hoặc bạn sử dụng lại một đối tượng trạng thái càng nhiều càng tốt (ví dụ từ một nhóm).

Ngoài ra, việc tiếp tục được gọi khi tác vụ hoàn thành, tạo khung stack, nó không được nội tuyến. Khung công tác cố gắng tránh tràn ngăn xếp, nhưng có thể xảy ra trường hợp không tránh được một ngăn, chẳng hạn như khi các mảng lớn được xếp chồng.

Cách nó cố gắng tránh điều này là bằng cách kiểm tra số lượng ngăn xếp còn lại và, nếu bằng một số biện pháp nội bộ, ngăn xếp được coi là đầy đủ, nó lập lịch trình tiếp tục để chạy trong trình lập lịch tác vụ. Nó cố gắng tránh các ngoại lệ chồng chất gây chết người với chi phí hiệu năng.

Đây là một sự khác biệt tinh tế giữa async/ awaitContinueWith:

  • async/ awaitsẽ lên lịch tiếp tục SynchronizationContext.Currentnếu có, nếu không thì trong TaskScheduler.Current 1

  • ContinueWithsẽ lên lịch tiếp tục trong trình lập lịch tác vụ được cung cấp hoặc trong TaskScheduler.Currenttình trạng quá tải mà không có tham số lịch trình tác vụ

Để mô phỏng hành vi mặc định của async/ await:

.ContinueWith(continuationAction,
    SynchronizationContext.Current != null ?
        TaskScheduler.FromCurrentSynchronizationContext() :
        TaskScheduler.Current)

Để mô phỏng hành vi của async/ awaitvới Task's .ConfigureAwait(false):

.ContinueWith(continuationAction,
    TaskScheduler.Default)

Mọi thứ bắt đầu trở nên phức tạp với các vòng lặp và xử lý ngoại lệ. Bên cạnh việc giữ mã của bạn có thể đọc được, async/ awaithoạt động với bất kỳ điều gì đang chờ đợi .

Trường hợp của bạn được xử lý tốt nhất với cách tiếp cận hỗn hợp: phương pháp đồng bộ gọi phương thức không đồng bộ khi cần. Một ví dụ về mã của bạn với cách tiếp cận này:

public Task<SomeObject> GetSomeObjectByTokenAsync(int id)
{
    string token = repository.GetTokenById(id);
    if (string.IsNullOrEmpty(token))
    {
        return Task.FromResult(new SomeObject()
        {
            IsAuthorized = false
        });
    }
    else
    {
        return InternalGetSomeObjectByTokenAsync(repository, token);
    }
}

internal async Task<SomeObject> InternalGetSomeObjectByToken(Repository repository, string token)
{
    SomeObject result = await repository.GetSomeObjectByTokenAsync(token);
    result.IsAuthorized = true;
    return result;
}

Theo kinh nghiệm của tôi, tôi đã tìm thấy rất ít vị trí trong mã ứng dụng trong đó việc thêm độ phức tạp như vậy thực sự trả hết thời gian để phát triển, xem xét và thử nghiệm các phương pháp như vậy, trong khi trong mã thư viện, bất kỳ phương pháp nào cũng có thể là một nút cổ chai.

Trường hợp duy nhất mà tôi có xu hướng yêu cầu nhiệm vụ là khi một TaskhoặcTask<T> phương thức trả về chỉ đơn giản trả về kết quả của một phương thức không đồng bộ khác, mà bản thân nó đã không thực hiện bất kỳ I / O hoặc bất kỳ xử lý hậu kỳ nào.

YMMV.


  1. Trừ khi bạn sử dụng ConfigureAwait(false)hoặc chờ đợi trên một số chờ đợi mà sử dụng lập lịch tùy chỉnh

1
Máy trạng thái không phải là một loại giá trị. Hãy xem nguồn được tạo của một chương trình async đơn giản : private sealed class <Main>d__0 : IAsyncStateMachine. Các thể hiện của lớp này có thể được sử dụng lại mặc dù.
Theodor Zoulias

Bạn có nói rằng nếu tôi chỉ can thiệp vào kết quả của một nhiệm vụ, thì có thể sử dụng được ContinueWithkhông?
SpiritBob

3
@TheodorZoulias, mục tiêu nguồn được tạo Phát hành thay vì Debug tạo ra các cấu trúc.
acelent

2
@SpriteBob, nếu bạn thực sự muốn tìm hiểu về kết quả nhiệm vụ, tôi đoán nó ổn, .ContinueWithcó nghĩa là được sử dụng theo chương trình. Đặc biệt là nếu bạn đang xử lý các tác vụ chuyên sâu của CPU thay vì các tác vụ I / O. Nhưng khi bạn đến mức phải xử lý Taskcác thuộc tính của nó, AggregateExceptionvà sự phức tạp của các điều kiện xử lý, chu trình và xử lý ngoại lệ khi các phương thức không đồng bộ tiếp theo được gọi, thì hầu như không có lý do nào để tuân theo nó.
acelent

2
@SpriteBob, xin lỗi, ý tôi là "vòng lặp". Có, bạn có thể tạo trường chỉ đọc tĩnh với a Task.FromResult("..."). Trong mã không đồng bộ, nếu bạn lưu các giá trị theo khóa yêu cầu I / O để lấy, bạn có thể sử dụng từ điển trong đó các giá trị là các tác vụ, ví dụ ConcurrentDictionary<string, Task<T>>thay vì ConcurrentDictionary<string, T>sử dụng GetOrAddlệnh gọi với hàm xuất xưởng và chờ kết quả của nó. Điều này đảm bảo chỉ có một yêu cầu I / O được thực hiện để điền vào khóa của bộ đệm, nó đánh thức người chờ đợi ngay khi nhiệm vụ hoàn thành và phục vụ như một nhiệm vụ hoàn thành sau đó.
acelent

2

Bằng cách sử dụng, ContinueWithbạn đang sử dụng các công cụ có sẵn trước khi giới thiệu async/ awaitchức năng với C # 5 trở lại vào năm 2012. Là một công cụ dài dòng, không dễ kết hợp và yêu cầu công việc bổ sung để mở khóa AggregateExceptionTask<Task<TResult>>trả về giá trị (bạn có được những điều này khi bạn chuyển các đại biểu không đồng bộ làm đối số). Nó cung cấp một vài lợi thế trong lợi nhuận. Bạn có thể cân nhắc sử dụng nó khi bạn muốn đính kèm nhiều phần tiếp theo giống nhau Taskhoặc trong một số trường hợp hiếm hoi mà bạn không thể sử dụng async/ awaitvì một số lý do (như khi bạn đang ở trong một phương thức outtham số ).


Cập nhật: Tôi đã xóa lời khuyên sai lệch rằng ContinueWithnên sử dụng TaskScheduler.Defaultđể bắt chước hành vi mặc định của await. Trên thực tế , awaittheo mặc định lịch trình tiếp tục sử dụng TaskScheduler.Current.

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.