ความแตกต่างระหว่าง“ await Task.Run (); กลับ;" และ "return Task.Run ()"?


90

มีความแตกต่างทางแนวคิดระหว่างรหัสสองชิ้นต่อไปนี้หรือไม่:

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

และ

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

รหัสที่สร้างขึ้นแตกต่างกันหรือไม่?

แก้ไข:เพื่อหลีกเลี่ยงความสับสนTask.Runกรณีที่คล้ายกัน:

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

และ

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

การอัปเดตล่าสุด:นอกจากคำตอบที่ยอมรับแล้วยังมีความแตกต่างในวิธีLocalCallContextจัดการ: CallContext.LogicalGetData ได้รับการกู้คืนแม้ว่าจะไม่มีอะซิงโครไนซ์ ทำไม?


1
ใช่มันแตกต่างกัน และมันแตกต่างกันมาก มิฉะนั้นจะไม่มีประโยชน์ในการใช้await/ asyncเลย :)
MarcinJuraszek

1
ฉันคิดว่ามีสองคำถามที่นี่ 1.การนำเมธอดไปใช้จริงมีความสำคัญต่อผู้โทรหรือไม่? 2.การนำเสนอที่รวบรวมของทั้งสองวิธีแตกต่างกันหรือไม่?
DavidRR

คำตอบ:


81

ข้อแตกต่างที่สำคัญประการหนึ่งคือการขยายพันธุ์ข้อยกเว้น ข้อยกเว้นโยนภายในasync Taskวิธีการได้รับการจัดเก็บไว้ในที่ส่งคืนTaskวัตถุและยังคงอยู่เฉยๆจนกว่างานที่ได้รับการตั้งข้อสังเกตผ่านawait task, task.Wait(), หรือtask.Result task.GetAwaiter().GetResult()มันแพร่กระจายด้วยวิธีนี้แม้ว่าจะถูกโยนจากส่วนซิงโครนัสของasyncวิธีการก็ตาม

พิจารณารหัสต่อไปนี้โดยที่OneTestAsyncและAnotherTestAsyncทำงานค่อนข้างแตกต่างกัน:

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);
    }
}

ถ้าฉันเรียกDoTestAsync(OneTestAsync, -2)มันจะสร้างผลลัพธ์ต่อไปนี้:

กด Enter เพื่อดำเนินการต่อ
ข้อผิดพลาด: เกิดข้อผิดพลาดอย่างน้อยหนึ่งข้อรอ Task.Delay
ข้อผิดพลาด: 2nd

หมายเหตุผมต้องกดEnterดู

ตอนนี้ถ้าฉันโทรDoTestAsync(AnotherTestAsync, -2)ไปเวิร์กโฟลว์ของโค้ดข้างในDoTestAsyncจะแตกต่างกันมากดังนั้นผลลัพธ์ก็เช่นกัน ครั้งนี้ฉันไม่ได้ขอให้กดEnter:

ข้อผิดพลาด: ค่าต้องเป็น -1 (หมายถึงการหมดเวลาไม่สิ้นสุด), 0 หรือจำนวนเต็มบวก
ชื่อพารามิเตอร์: millisecondsDelayError: 1st

ในทั้งสองกรณีจะTask.Delay(-2)พ่นที่จุดเริ่มต้นในขณะที่ตรวจสอบความถูกต้องของพารามิเตอร์ นี่อาจเป็นสถานการณ์ที่สร้างขึ้น แต่ในทางทฤษฎีTask.Delay(1000)อาจทำให้เกิดปัญหาได้เช่นกันเช่นเมื่อ API ตัวจับเวลาระบบที่อยู่เบื้องหลังล้มเหลว

หมายเหตุด้านข้างตรรกะการเผยแพร่ข้อผิดพลาดยังแตกต่างกันสำหรับasync voidวิธีการ (ซึ่งต่างจากasync Taskวิธีการ) ข้อยกเว้นที่เกิดขึ้นภายในasync voidเมธอดจะถูกโยนซ้ำทันทีบนบริบทการซิงโครไนซ์ของเธรดปัจจุบัน (ผ่านSynchronizationContext.Post) หากเธรดปัจจุบันมีหนึ่งเธรด ( SynchronizationContext.Current != null)มิฉะนั้นจะถูกโยนซ้ำผ่านThreadPool.QueueUserWorkItem) ผู้เรียกไม่มีโอกาสจัดการกับข้อยกเว้นนี้บนสแต็กเฟรมเดียวกัน

ผมโพสต์รายละเอียดบางอย่างเพิ่มเติมเกี่ยวกับพฤติกรรมการจัดการข้อยกเว้น TPL ที่นี่และที่นี่


ถาม : เป็นไปได้ไหมที่จะเลียนแบบพฤติกรรมการแพร่กระจายข้อยกเว้นของasyncเมธอดสำหรับเมธอดที่ไม่ใช่ async Taskเพื่อไม่ให้ส่วนหลังอยู่บนสแต็กเฟรมเดียวกัน

ตอบ : หากจำเป็นจริงๆใช่มีเคล็ดลับสำหรับสิ่งนั้น:

// 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;
}

อย่างไรก็ตามโปรดทราบว่าภายใต้เงื่อนไขบางประการ (เช่นเมื่อมันอยู่ลึกเกินไปบนสแต็ก) RunSynchronouslyยังสามารถดำเนินการแบบอะซิงโครนัสได้


อีกความแตกต่างที่โดดเด่นก็คือว่า/ รุ่นอื่น ๆ มีแนวโน้มที่จะตายล็อคในบริบทการประสานไม่ใช่ค่าเริ่มต้น เช่นสิ่งต่อไปนี้จะล็อคตายในแอปพลิเคชัน WinForms หรือ WPF:asyncawait

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

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

เปลี่ยนเป็นเวอร์ชันที่ไม่ใช่ async และจะไม่ล็อคตาย:

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

ธรรมชาติของคนตายล็อคจะมีการอธิบายอย่างดีจากสตีเฟ่นเคลียร์ของเขาในบล็อก


2
ฉันเชื่อว่าการหยุดชะงักในตัวอย่างแรกสามารถหลีกเลี่ยงได้โดยการเพิ่ม. ConfigureAwait (false) ในบรรทัดการรอคอยเนื่องจากมันเกิดขึ้นเนื่องจากเมธอดพยายามกลับไปสู่บริบทการดำเนินการเดียวกัน ดังนั้นการกำจัดจึงเป็นความแตกต่างเดียวที่ยังคงอยู่
ค่อนข้าง

2
@relici_random ความคิดเห็นของคุณถูกต้องแม้ว่าคำตอบจะเกี่ยวกับความแตกต่างระหว่างreturn Task.Run()และawait Task.Run(); returnมากกว่าawait Task.Run().ConfigureAwait(false); return
อัตราส่วน

หากคุณพบว่าโปรแกรมปิดหลังจากที่คุณกด Enter ตรวจสอบให้แน่ใจว่าคุณทำ ctrl + F5 แทน F5
David Klempfner

53

อะไรคือความแตกต่างระหว่าง

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

และ

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

เหรอ?

ฉันสับสนกับคำถามนี้ ให้ฉันพยายามชี้แจงโดยตอบคำถามของคุณด้วยคำถามอื่น อะไรคือความแตกต่างระหว่าง?

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

และ

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

เหรอ?

ไม่ว่าความแตกต่างระหว่างสองสิ่งของฉันคืออะไรความแตกต่างที่เหมือนกันคือระหว่างสองสิ่งของคุณ


23
แน่นอน! คุณได้เปิดตาของฉัน :) Task.Delay(1000).ContinueWith(() = {})ในกรณีแรกที่ผมสร้างงานห่อความหมายใกล้เคียงกับ Task.Delay(1000)ในหนึ่งวินาทีก็เพียง ความแตกต่างค่อนข้างละเอียดอ่อน แต่มีนัยสำคัญ
AVO

3
คุณช่วยอธิบายความแตกต่างเล็กน้อยได้ไหม ที่จริงฉันไม่.. ขอบคุณ
เจิ้งหยู่

4
เนื่องจากมีความแตกต่างเล็กน้อยกับบริบทการซิงค์และการเผยแพร่ข้อยกเว้นฉันจะบอกว่าความแตกต่างระหว่าง async / await และ function wrapper นั้นไม่เหมือนกัน
Cameron MacFarland

1
@CameronMacFarland: นั่นคือเหตุผลที่ฉันขอคำชี้แจง คำถามที่ถามคือมีความแตกต่างทางแนวคิดระหว่างทั้งสอง ดีฉันไม่รู้ มีความแตกต่างมากมายอย่างแน่นอน พวกเขาถือเป็นความแตกต่างทาง "แนวคิด" หรือไม่ ในตัวอย่างของฉันกับ funcs ที่ซ้อนกันยังมีความแตกต่างในการเผยแพร่ข้อผิดพลาด ถ้าฟังก์ชันถูกปิดในสถานะท้องถิ่นจะมีความแตกต่างในช่วงอายุการใช้งานในท้องถิ่นและอื่น ๆ "แนวความคิด" เหล่านี้แตกต่างกันหรือไม่?
Eric Lippert

8
นี่เป็นคำตอบเก่า แต่ฉันเชื่อว่าในวันนี้จะถูกลดลง ไม่ตอบคำถามและไม่ชี้ OP ไปยังแหล่งที่เขาสามารถเรียนรู้ได้
Daniel Dubovski

11
  1. วิธีแรกไม่ได้รวบรวม

    เนื่องจาก ' Program.TestAsync()' เป็นเมธอด async ที่ส่งกลับ ' Task' จึงต้องไม่ตามด้วยคำหลัก return ด้วยนิพจน์อ็อบเจกต์ คุณตั้งใจจะกลับมา ' Task<T>'?

    มันจะต้องมี

    async Task TestAsync()
    {
        await Task.Run(() => DoSomeWork());
    }
    
  2. มีความแตกต่างทางแนวคิดที่สำคัญระหว่างสองสิ่งนี้ อันแรกเป็นแบบอะซิงโครนัสอันที่สองไม่ใช่ อ่าน Async ประสิทธิภาพการทำงาน: การทำความเข้าใจกับค่าใช้จ่ายของ Async และรอคอยที่จะได้รับเพียงเล็กน้อยเพิ่มเติมเกี่ยวกับ internals ของ/asyncawait

  3. พวกเขาสร้างรหัสที่แตกต่างกัน

    .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 แน่นอนมันไม่ได้รวบรวม นั่นเป็นการพิมพ์ผิดฉันแน่ใจว่าคุณเข้าใจถูกแล้ว มิฉะนั้นคำตอบที่ดีขอบคุณ! ฉันคิดว่า C # อาจฉลาดพอที่จะหลีกเลี่ยงการสร้างคลาสเครื่องของรัฐในกรณีแรก
AVO

9

สองตัวอย่างไม่แตกต่างกัน เมื่อเมธอดถูกทำเครื่องหมายด้วยasyncคีย์เวิร์ดคอมไพเลอร์จะสร้าง state-machine ที่อยู่เบื้องหลัง นี่คือสิ่งที่รับผิดชอบในการดำเนินการต่อเมื่อมีการรอคอยที่รอคอย

ในทางตรงกันข้ามเมื่อไม่มีการทำเครื่องหมายวิธีการasyncคุณจะสูญเสียความสามารถในการawaitรอคอย (นั่นคือภายในเมธอดเองผู้เรียกยังสามารถรอเมธอดได้) อย่างไรก็ตามการหลีกเลี่ยงasyncคีย์เวิร์ดจะทำให้คุณไม่ได้สร้าง state-machine อีกต่อไปซึ่งสามารถเพิ่มค่าใช้จ่ายได้เล็กน้อย (ยกชาวบ้านไปยังฟิลด์ ของ state-machine อ็อบเจ็กต์เพิ่มเติมของ GC)

ในตัวอย่างเช่นนี้หากคุณสามารถหลีกเลี่ยงasync-awaitและส่งคืนสิ่งที่รอคอยได้โดยตรงควรทำเพื่อปรับปรุงประสิทธิภาพของวิธีการนี้

ดูคำถามนี้และคำตอบนี้ซึ่งคล้ายกับคำถามของคุณและคำตอบนี้มาก

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.