Bất kỳ sự khác biệt nào giữa “await Task.Run (); trở về;" và “return Task.Run ()”?


90

Có sự khác biệt về khái niệm nào giữa hai đoạn mã sau đây không:

async Task TestAsync() 
{
    await Task.Run(() => DoSomeWork());
}

Task TestAsync() 
{
    return Task.Run(() => DoSomeWork());
}

Mã được tạo có khác nhau không?

CHỈNH SỬA: Để tránh nhầm lẫn với Task.Run, một trường hợp tương tự:

async Task TestAsync() 
{
    await Task.Delay(1000);
}

Task TestAsync() 
{
    return Task.Delay(1000);
}

CẬP NHẬT CUỐI CÙNG: Ngoài câu trả lời được chấp nhận, cũng có sự khác biệt về cách LocalCallContextxử lý: CallContext.LogicalGetData được khôi phục ngay cả khi không có sự đồng bộ. Tại sao?


1
Có, nó khác nhau. Và nó khác rất nhiều. nếu không sẽ không có điểm trong việc sử dụng await/ asyncở tất cả :)
MarcinJuraszek

1
Tôi nghĩ rằng có hai câu hỏi ở đây. 1. Việc triển khai thực tế của phương thức có quan trọng đối với người gọi của nó không? 2. Các biểu diễn biên dịch của hai phương pháp có khác nhau không?
DavidRR

Câu trả lời:


80

Một sự khác biệt chính là trong việc truyền ngoại lệ. Một ngoại lệ, ném ra bên trong một async Taskphương pháp, được lưu trữ trong các trở Taskđối tượng và vẫn im lìm cho đến khi công việc được quan sát qua await task, task.Wait(), task.Resulthoặc task.GetAwaiter().GetResult(). Nó được truyền theo cách này ngay cả khi được ném ra từ phần đồng bộ của asyncphương thức.

Hãy xem xét đoạn mã sau, ở đâu OneTestAsyncAnotherTestAsynchoạt động hoàn toàn khác:

static async Task OneTestAsync(int n)
{
    await Task.Delay(n);
}

static Task AnotherTestAsync(int n)
{
    return Task.Delay(n);
}

// call DoTestAsync with either OneTestAsync or AnotherTestAsync as whatTest
static void DoTestAsync(Func<int, Task> whatTest, int n)
{
    Task task = null;
    try
    {
        // start the task
        task = whatTest(n);

        // do some other stuff, 
        // while the task is pending
        Console.Write("Press enter to continue");
        Console.ReadLine();
        task.Wait();
    }
    catch (Exception ex)
    {
        Console.Write("Error: " + ex.Message);
    }
}

Nếu tôi gọi DoTestAsync(OneTestAsync, -2), nó tạo ra kết quả sau:

Nhấn Enter để tiếp tục
Lỗi: Đã xảy ra một hoặc nhiều lỗi. Xóa Task.Delay
Lỗi: thứ 2

Lưu ý, tôi phải nhấn Enterđể xem nó.

Bây giờ, nếu tôi gọi DoTestAsync(AnotherTestAsync, -2) , quy trình làm việc của mã bên trong DoTestAsynckhá khác, và đầu ra cũng vậy. Lần này, tôi không được yêu cầu nhấn Enter:

Lỗi: Giá trị phải là -1 (biểu thị thời gian chờ vô hạn), 0 hoặc số nguyên dương.
Tên thông số: mili giâyDelayError: 1st

Trong cả hai trường hợp, Task.Delay(-2)ném vào đầu, trong khi xác thực các tham số của nó. Đây có thể là một kịch bản dựng sẵn, nhưng trên lý thuyếtTask.Delay(1000) cũng có thể xảy ra, ví dụ: khi API hẹn giờ hệ thống cơ bản bị lỗi.

Một lưu ý nhỏ, logic truyền lỗi vẫn khác nhau đối với async voidcác phương thức (trái ngược với async Taskcác phương thức). Một ngoại lệ được đưa ra bên trong một async voidphương thức sẽ ngay lập tức được ném lại trên ngữ cảnh đồng bộ hóa của luồng hiện tại (qua SynchronizationContext.Post), nếu luồng hiện tại có một ( SynchronizationContext.Current != null). Nếu không, nó sẽ được ném lại qua ThreadPool.QueueUserWorkItem). Người gọi không có cơ hội xử lý ngoại lệ này trên cùng một khung ngăn xếp.

Tôi đã đăng thêm một số chi tiết về hành vi xử lý ngoại lệ TPL ở đâyở đây .


Hỏi : Có thể bắt chước hành vi lan truyền ngoại lệ của các asyncphương thức đối với các phương thức không Taskdựa trên cơ sở không đồng bộ, để sau này không ném vào cùng một khung ngăn xếp không?

A : Nếu thực sự cần thiết, thì có, có một mẹo cho điều đó:

// async
async Task<int> MethodAsync(int arg)
{
    if (arg < 0)
        throw new ArgumentException("arg");
    // ...
    return 42 + arg;
}

// non-async
Task<int> MethodAsync(int arg)
{
    var task = new Task<int>(() => 
    {
        if (arg < 0)
            throw new ArgumentException("arg");
        // ...
        return 42 + arg;
    });

    task.RunSynchronously(TaskScheduler.Default);
    return task;
}

Tuy nhiên, lưu ý rằng trong một số điều kiện nhất định (như khi nó quá sâu trong ngăn xếp), RunSynchronouslyvẫn có thể thực thi không đồng bộ.


Một điểm khác biệt đáng chú ý là các async/ awaitphiên bản là dễ bị chết khóa trên một bối cảnh đồng bộ hóa không phải mặc định . Ví dụ: phần sau sẽ bị khóa trong ứng dụng WinForms hoặc WPF:

static async Task TestAsync()
{
    await Task.Delay(1000);
}

void Form_Load(object sender, EventArgs e)
{
    TestAsync().Wait(); // dead-lock here
}

Thay đổi nó thành một phiên bản không đồng bộ và nó sẽ không bị khóa:

Task TestAsync() 
{
    return Task.Delay(1000);
}

Bản chất của dead-lock đã được Stephen Cleary giải thích rõ trong blog của mình .


2
Tôi tin rằng có thể tránh được bế tắc trong ví dụ đầu tiên bằng cách thêm .ConfigureAwait (false) vào dòng await, vì nó chỉ xảy ra vì phương thức đang cố gắng quay lại cùng một ngữ cảnh thực thi. Vì vậy, sau đó các dịch tiết là sự khác biệt duy nhất còn lại.
relatively_random

2
@relatively_random, bình luận của bạn là đúng, mặc dù câu trả lời là về sự khác biệt giữa return Task.Run()await Task.Run(); return, hơnawait Task.Run().ConfigureAwait(false); return
noseratio

Nếu bạn thấy chương trình đóng sau khi bạn nhấn Enter, hãy đảm bảo bạn thực hiện ctrl + F5 thay vì F5.
David Klempfner

54

Sự khác biệt giữa

async Task TestAsync() 
{
    await Task.Delay(1000);
}

Task TestAsync() 
{
    return Task.Delay(1000);
}

?

Tôi bối rối bởi câu hỏi này. Hãy để tôi cố gắng làm rõ bằng cách trả lời câu hỏi của bạn bằng một câu hỏi khác. Sự khác biệt giữa là gì?

Func<int> MakeFunction()
{
    Func<int> f = ()=>1;
    return ()=>f();
}

Func<int> MakeFunction()
{
    return ()=>1;
}

?

Dù sự khác biệt giữa hai thứ của tôi là gì, thì sự khác biệt giống nhau giữa hai thứ của bạn.


22
Tất nhiên! Bạn đã mở rộng tầm mắt của tôi :) Trong trường hợp đầu tiên, tôi tạo một nhiệm vụ trình bao bọc, gần với ngữ nghĩa Task.Delay(1000).ContinueWith(() = {}). Trong cái thứ hai, nó chỉ là Task.Delay(1000). Sự khác biệt là một chút tinh tế, nhưng đáng kể.
ava

3
Bạn có thể giải thích một chút sự khác biệt? thực sự thì tôi không..Cảm ơn bạn
zheng yu

4
Do có một sự khác biệt nhỏ với các ngữ cảnh đồng bộ và sự lan truyền ngoại lệ, tôi muốn nói rằng sự khác biệt giữa async / await và các trình bao bọc hàm là không giống nhau.
Cameron MacFarland

1
@CameronMacFarland: Đó là lý do tại sao tôi yêu cầu làm rõ. Câu hỏi đặt ra là có sự khác biệt về khái niệm giữa hai điều này. Tôi không biết. Chắc chắn có nhiều điểm khác biệt; có bất kỳ sự khác biệt nào trong số chúng được coi là khác biệt "khái niệm" không? Trong ví dụ của tôi với các funcs lồng nhau cũng có sự khác biệt trong việc truyền lỗi; nếu các chức năng bị đóng trên trạng thái địa phương thì sẽ có sự khác biệt về thời gian tồn tại của địa phương, v.v. Đây có phải là những khác biệt về "khái niệm"?
Eric Lippert

6
Đây là một câu trả lời cũ, nhưng tôi tin rằng được đưa ra ngày hôm nay, sẽ bị từ chối. Nó không trả lời câu hỏi, cũng không chỉ OP đến một nguồn mà từ đó anh ta có thể học được.
Daniel Dubovski

11
  1. Phương thức đầu tiên thậm chí không biên dịch.

    Vì ' Program.TestAsync()' là một phương thức không đồng bộ trả về ' Task', một từ khóa trả về không được theo sau bởi một biểu thức đối tượng. Bạn có định quay lại ' Task<T>' không?

    Nó phải được

    async Task TestAsync()
    {
        await Task.Run(() => DoSomeWork());
    }
    
  2. Có sự khác biệt lớn về khái niệm giữa hai điều này. Cái đầu tiên là không đồng bộ, cái thứ hai thì không. Đọc Hiệu suất không đồng bộ: Hiểu chi phí của không đồng bộ và chờ đợi để hiểu thêm một chút về nội bộ của async/ await.

  3. Họ tạo ra các mã khác nhau.

    .method private hidebysig 
        instance class [mscorlib]System.Threading.Tasks.Task TestAsync () cil managed 
    {
        .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = (
            01 00 25 53 4f 54 65 73 74 50 72 6f 6a 65 63 74
            2e 50 72 6f 67 72 61 6d 2b 3c 54 65 73 74 41 73
            79 6e 63 3e 64 5f 5f 31 00 00
        )
        .custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
            01 00 00 00
        )
        // Method begins at RVA 0x216c
        // Code size 62 (0x3e)
        .maxstack 2
        .locals init (
            [0] valuetype SOTestProject.Program/'<TestAsync>d__1',
            [1] class [mscorlib]System.Threading.Tasks.Task,
            [2] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder
        )
    
        IL_0000: ldloca.s 0
        IL_0002: ldarg.0
        IL_0003: stfld class SOTestProject.Program SOTestProject.Program/'<TestAsync>d__1'::'<>4__this'
        IL_0008: ldloca.s 0
        IL_000a: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Create()
        IL_000f: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
        IL_0014: ldloca.s 0
        IL_0016: ldc.i4.m1
        IL_0017: stfld int32 SOTestProject.Program/'<TestAsync>d__1'::'<>1__state'
        IL_001c: ldloca.s 0
        IL_001e: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
        IL_0023: stloc.2
        IL_0024: ldloca.s 2
        IL_0026: ldloca.s 0
        IL_0028: call instance void [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::Start<valuetype SOTestProject.Program/'<TestAsync>d__1'>(!!0&)
        IL_002d: ldloca.s 0
        IL_002f: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder SOTestProject.Program/'<TestAsync>d__1'::'<>t__builder'
        IL_0034: call instance class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder::get_Task()
        IL_0039: stloc.1
        IL_003a: br.s IL_003c
    
        IL_003c: ldloc.1
        IL_003d: ret
    } // end of method Program::TestAsync
    

    .method private hidebysig 
        instance class [mscorlib]System.Threading.Tasks.Task TestAsync2 () cil managed 
    {
        // Method begins at RVA 0x21d8
        // Code size 23 (0x17)
        .maxstack 2
        .locals init (
            [0] class [mscorlib]System.Threading.Tasks.Task CS$1$0000
        )
    
        IL_0000: nop
        IL_0001: ldarg.0
        IL_0002: ldftn instance class [mscorlib]System.Threading.Tasks.Task SOTestProject.Program::'<TestAsync2>b__4'()
        IL_0008: newobj instance void class [mscorlib]System.Func`1<class [mscorlib]System.Threading.Tasks.Task>::.ctor(object, native int)
        IL_000d: call class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Threading.Tasks.Task::Run(class [mscorlib]System.Func`1<class [mscorlib]System.Threading.Tasks.Task>)
        IL_0012: stloc.0
        IL_0013: br.s IL_0015
    
        IL_0015: ldloc.0
        IL_0016: ret
    } // end of method Program::TestAsync2
    

@MarcinJuraszek, thực sự là nó không biên dịch. Đó là một lỗi đánh máy, tôi chắc chắn bạn đã hiểu đúng. Nếu không, một câu trả lời tuyệt vời, cảm ơn! Tôi nghĩ C # có thể đủ thông minh để tránh tạo ra một lớp máy trạng thái trong trường hợp đầu tiên.
avg

9

Hai ví dụ làm khác nhau. Khi một phương thức được đánh dấu bằngasync từ khóa, trình biên dịch sẽ tạo ra một máy trạng thái phía sau hậu trường. Đây là những gì chịu trách nhiệm cho việc nối lại các liên tục sau khi một lỗi có thể được chờ đợi.

Ngược lại, khi một phương thức không được đánh dấu, asyncbạn sẽ mất khả năng chờ awaitđợi. (Nghĩa là, bên trong chính phương thức; phương thức vẫn có thể được người gọi của nó chờ đợi.) Tuy nhiên, bằng cách tránhasync từ khóa, bạn không còn tạo ra máy trạng thái, có thể thêm một chút chi phí hợp lý (nâng các cục bộ lên các trường của máy trạng thái, các đối tượng bổ sung cho GC).

Trong các ví dụ như thế này, nếu bạn có thể tránh async-await và trả về một cách trực tiếp có thể chờ đợi, thì điều đó nên được thực hiện để cải thiện hiệu quả của phương pháp.

Xem câu hỏi nàycâu trả lời này rất giống với câu hỏi của bạn và câu trả lời này.

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.