async / await - เมื่อใดที่จะส่งคืน Task vs void?


502

ภายใต้สถานการณ์ใดที่เราจะต้องการใช้

public async Task AsyncMethod(int num)

แทน

public async void AsyncMethod(int num)

สถานการณ์เดียวที่ฉันนึกออกคือถ้าคุณต้องการให้ภารกิจติดตามความคืบหน้า

นอกจากนี้ในวิธีต่อไปนี้ async และรอคำสำคัญไม่จำเป็นหรือไม่

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

20
หมายเหตุว่าวิธี async ควรเสมอจะมีชื่อ suffixed Async.Example จะกลายเป็นFoo() FooAsync()
Fred

30
@ เฟรดเป็นส่วนใหญ่ แต่ไม่เสมอไป นี่เป็นเพียงการประชุมและข้อยกเว้นได้รับการยอมรับในการประชุมนี้จะมีการเรียนตามเหตุการณ์หรือสัญญาอินเตอร์เฟซที่ดู MSDN ตัวอย่างเช่นคุณไม่ควรเปลี่ยนชื่อตัวจัดการเหตุการณ์ทั่วไปเช่น Button1_Click
Ben

14
เพียงบันทึกที่คุณไม่ควรใช้Thread.Sleepกับงานที่คุณควรทำawait Task.Delay(num)แทน
Bob Vale

45
@ เฟร็ดฉันไม่เห็นด้วยกับสิ่งนี้ IMO เพิ่มส่วนต่อท้าย async ควรจะใช้เฉพาะเมื่อคุณให้อินเทอร์เฟซที่มีตัวเลือกการซิงค์และ async Smurf ตั้งชื่อสิ่งต่าง ๆ ด้วย async เมื่อมีเจตนาเดียวเท่านั้นที่ไม่มีจุดหมาย กรณีในจุดTask.Delayไม่Task.AsyncDelayเป็นเช่นเดียวกับวิธีการทั้งหมดในงานเป็น Async
ไม่ได้รัก

11
ผมมีปัญหาที่น่าสนใจในเช้าวันนี้ด้วยวิธีการควบคุม WebAPI 2 ก็ถูกประกาศเป็นแทนasync void async Taskวิธีการที่ล้มเหลวเพราะมันใช้วัตถุบริบท Entity Framework ประกาศเป็นสมาชิกของตัวควบคุมถูกกำจัดก่อนที่วิธีการเสร็จสิ้นการดำเนินการ เฟรมเวิร์กจะกำจัดคอนโทรลเลอร์ก่อนที่เมธอดจะดำเนินการเสร็จสิ้น ฉันเปลี่ยนวิธีการเป็น async Task และใช้งานได้
costa

คำตอบ:


417

1) Taskโดยปกติคุณต้องการที่จะกลับมาเป็น ข้อยกเว้นหลักควรเป็นเมื่อคุณจำเป็นต้องมีvoidชนิดส่งคืน (สำหรับเหตุการณ์) หากไม่มีเหตุผลที่จะไม่อนุญาตให้โทรหาawaitงานของคุณทำไมไม่อนุญาต

2) asyncวิธีการที่ส่งกลับvoidเป็นพิเศษในอีกแง่มุม: พวกเขาแสดงถึงการดำเนินการ async ระดับบนสุดและมีกฎเพิ่มเติมที่เข้ามาเล่นเมื่องานของคุณส่งคืนข้อยกเว้น วิธีที่ง่ายที่สุดคือการแสดงความแตกต่างด้วยตัวอย่าง:

static async void f()
{
    await h();
}

static async Task g()
{
    await h();
}

static async Task h()
{
    throw new NotImplementedException();
}

private void button1_Click(object sender, EventArgs e)
{
    f();
}

private void button2_Click(object sender, EventArgs e)
{
    g();
}

private void button3_Click(object sender, EventArgs e)
{
    GC.Collect();
}

fข้อยกเว้นของ "เสมอ" สังเกต ข้อยกเว้นที่ปล่อยให้วิธีการแบบอะซิงโครนัสระดับสูงสุดนั้นได้รับการปฏิบัติเช่นเดียวกับข้อยกเว้นอื่น ๆ ที่ไม่สามารถจัดการได้ gไม่เคยสังเกตข้อยกเว้น เมื่อตัวรวบรวมขยะมาเพื่อล้างภารกิจนั้นจะเห็นว่างานส่งผลให้เกิดข้อยกเว้นและไม่มีใครจัดการข้อยกเว้น เมื่อเกิดเหตุการณ์ขึ้นTaskScheduler.UnobservedTaskExceptionตัวจัดการจะทำงาน คุณไม่ควรปล่อยให้เรื่องนี้เกิดขึ้น เพื่อใช้ตัวอย่างของคุณ

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

ใช่ใช้งานasyncและawaitที่นี่ช่วยให้แน่ใจว่าวิธีการของคุณยังคงทำงานได้อย่างถูกต้องหากมีข้อผิดพลาดเกิดขึ้น

สำหรับข้อมูลเพิ่มเติมโปรดดู: http://msdn.microsoft.com/en-us/magazine/jj991977.aspx


10
ฉันหมายถึงfแทนที่จะgแสดงความคิดเห็นของฉัน ข้อยกเว้นจากการถูกส่งไปยังf จะเพิ่มแต่จะไม่ขัดข้องกระบวนการอีกต่อไปหากไม่ได้จัดการ มีบางสถานการณ์ที่ยอมรับได้ว่ามี "ข้อยกเว้นแบบอะซิงโครนัส" เช่นนี้ซึ่งจะถูกละเว้น SynchronizationContextgUnobservedTaskExceptionUTE
Stephen Cleary

3
หากคุณมีWhenAnyหลายTasks ส่งผลให้มีข้อยกเว้น คุณมักจะต้องจัดการกับคนแรกและคุณมักจะต้องการที่จะไม่สนใจคนอื่น ๆ
Stephen Cleary

1
@StephenCleary ขอบคุณฉันเดาว่าเป็นตัวอย่างที่ดีแม้ว่ามันจะขึ้นอยู่กับเหตุผลที่คุณโทรหาWhenAnyในตอนแรกว่ามันโอเคที่จะเพิกเฉยข้อยกเว้นอื่น ๆ : กรณีการใช้งานหลักที่ฉันมีมันยังคงจบลงรองานที่เหลืออยู่ มีหรือไม่มีข้อยกเว้น

10
ฉันสับสนเล็กน้อยว่าทำไมคุณแนะนำให้คืนภารกิจแทนที่จะเป็นโมฆะ อย่างที่คุณพูด f () จะโยนและยกเว้น แต่ g () จะไม่ ไม่ควรที่จะทราบถึงข้อยกเว้นเธรดเบื้องหลังเหล่านี้หรือไม่
user981225

2
@ user981225 แน่นอน แต่นั่นก็กลายเป็นความรับผิดชอบของผู้โทร g: ทุกวิธีที่โทร g ควรเป็น async และใช้รอเช่นกัน นี่เป็นแนวทางไม่ใช่กฎที่ยากคุณสามารถตัดสินใจได้ว่าในโปรแกรมเฉพาะของคุณมันง่ายกว่าที่จะมี g คืนเป็นโมฆะ

40

ฉันเจอบทความที่มีประโยชน์เกี่ยวกับasyncและvoidเขียนโดยJérôme Laban: https://jaylee.org/archive/2012/07/08/c-sharp-async-tips-and-tricks-part2-jaylee.org และ .html

บรรทัดล่างคือว่าasync+voidสามารถผิดพลาดของระบบและโดยปกติควรจะใช้เฉพาะในตัวจัดการเหตุการณ์ด้าน UI

เหตุผลที่อยู่เบื้องหลังนี้คือบริบทการซิงโครไนซ์ที่ใช้โดย AsyncVoidMethodBuilder ซึ่งไม่มีในตัวอย่างนี้ เมื่อไม่มีบริบทการซิงโครไนซ์ของสภาพแวดล้อมข้อยกเว้นใด ๆ ที่ไม่สามารถจัดการได้โดยเนื้อความของเมธอด async void จะถูกสร้างขึ้นมาใหม่บน ThreadPool ในขณะที่ดูเหมือนว่าไม่มีโลจิคัลสถานที่อื่นที่ข้อยกเว้นชนิดที่ไม่สามารถจัดการได้ถูกโยนทิ้ง แต่ผลลัพธ์ที่โชคร้ายคือกระบวนการกำลังถูกยกเลิกเนื่องจากข้อยกเว้นที่ไม่สามารถจัดการได้บน ThreadPool จะยุติกระบวนการอย่างมีประสิทธิภาพตั้งแต่. NET 2.0 คุณสามารถดักจับข้อยกเว้นที่ไม่ได้จัดการทั้งหมดโดยใช้เหตุการณ์ AppDomain.UnhandledException แต่ไม่มีวิธีกู้คืนกระบวนการจากเหตุการณ์นี้

เมื่อเขียนตัวจัดการเหตุการณ์ UI เมธอดเป็นโมฆะของ async จะไม่เจ็บปวดอย่างใดเพราะข้อยกเว้นจะได้รับการปฏิบัติในลักษณะเดียวกับที่พบในวิธีการที่ไม่ใช้ async พวกเขาถูกโยนลงไปในดิสแพตเชอร์ มีความเป็นไปได้ที่จะกู้คืนจากข้อยกเว้นดังกล่าวโดยมีความถูกต้องมากกว่าสำหรับกรณีส่วนใหญ่ นอกตัวจัดการเหตุการณ์ UI อย่างไรก็ตามวิธีการโมฆะ async เป็นอันตรายอย่างใดที่จะใช้และอาจไม่ง่ายที่จะหา


ถูกต้องไหม ฉันคิดว่าข้อยกเว้นในวิธีการซิงค์จะถูกจับภายในงาน และยัง afaik มีเสมอ SynchronizationContext deafault
NM

30

ฉันได้แนวคิดที่ชัดเจนจากข้อความนี้

  1. วิธีการ Async เป็นโมฆะมีความหมายที่แตกต่างกันในการจัดการข้อผิดพลาด เมื่อมีข้อยกเว้นถูกโยนออกจากงาน async หรือวิธีการ async Task ข้อยกเว้นนั้นจะถูกดักจับและวางไว้บนวัตถุ Task ด้วยเมธอด async เป็นโมฆะไม่มีวัตถุภารกิจดังนั้นข้อยกเว้นใด ๆ ที่ถูกโยนออกมาจากวิธีการ async void จะถูกยกขึ้นโดยตรงบน เริ่มต้น

ข้อยกเว้นจากวิธีการ Async Void ไม่สามารถดักจับได้

private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
    throw;
  }
}

ข้อยกเว้นเหล่านี้สามารถตรวจสอบได้โดยใช้ AppDomain.UnhandledException หรือเหตุการณ์ catch-all ที่คล้ายกันสำหรับแอปพลิเคชัน GUI / ASP.NET แต่การใช้เหตุการณ์เหล่านั้นสำหรับการจัดการข้อยกเว้นปกติเป็นสูตรสำหรับความไม่แน่นอน (มันขัดข้องแอปพลิเคชัน)

  1. วิธีการ Async เป็นโมฆะมีความหมายที่แตกต่างกันในการเขียน วิธีการ Async ที่คืนค่าภารกิจหรืองานสามารถประกอบได้อย่างง่ายดายโดยใช้ await, Task.WhenAny, Task.WhenAll และอื่น ๆ วิธีการ Async ที่ส่งคืนเป็นโมฆะไม่ได้ให้วิธีที่ง่ายในการแจ้งรหัสการโทรที่พวกเขาได้เสร็จสิ้นแล้ว มันง่ายที่จะเริ่มวิธีการ async เป็นโมฆะหลายวิธี แต่มันไม่ง่ายที่จะตัดสินว่าเมื่อมันเสร็จสิ้นแล้ว วิธีการโมฆะ Async จะแจ้งให้ทราบ SynchronizationContext ของพวกเขาเมื่อพวกเขาเริ่มต้นและเสร็จสิ้น แต่ SynchronizationContext ที่กำหนดเองเป็นโซลูชั่นที่ซับซ้อนสำหรับรหัสแอปพลิเคชันปกติ

  2. วิธี Async Void มีประโยชน์เมื่อใช้การซิงโครไนซ์ตัวจัดการเหตุการณ์เนื่องจากพวกเขายกข้อยกเว้นของพวกเขาโดยตรงบน SynchronizationContext

สำหรับรายละเอียดเพิ่มเติมตรวจสอบลิงค์นี้ https://msdn.microsoft.com/en-us/magazine/jj991977.aspx


21

ปัญหาในการโทรเป็นโมฆะ async คือคุณไม่ได้รับงานคืนคุณไม่มีทางรู้ว่าเมื่องานของฟังก์ชั่นเสร็จสมบูรณ์ (ดูhttps://blogs.msdn.microsoft.com/oldnewthing/20170720-00/ ? p = 96655 )

นี่คือสามวิธีในการเรียกใช้ฟังก์ชัน async:

async Task<T> SomethingAsync() { ... return t; }
async Task SomethingAsync() { ... }
async void SomethingAsync() { ... }

ในทุกกรณีฟังก์ชั่นจะถูกเปลี่ยนเป็นห่วงโซ่ของงาน ความแตกต่างคือสิ่งที่ฟังก์ชั่นส่งคืน

ในกรณีแรกฟังก์ชันส่งคืนงานที่สร้าง t ในที่สุด

ในกรณีที่สองฟังก์ชั่นส่งคืนงานที่ไม่มีผลิตภัณฑ์ แต่คุณยังสามารถรอที่จะรู้ว่าเมื่อมันทำงานให้เสร็จ

กรณีที่สามเป็นคดีที่น่ารังเกียจ กรณีที่สามเป็นเหมือนกรณีที่สองยกเว้นว่าคุณไม่ได้รับงานคืน คุณไม่มีทางรู้ว่าเมื่องานของฟังก์ชั่นเสร็จสมบูรณ์

กรณีโมฆะ async เป็น "ไฟและลืม": คุณเริ่มต้นห่วงโซ่งาน แต่คุณไม่สนใจเมื่อมันเสร็จ เมื่อฟังก์ชั่นกลับมาสิ่งที่คุณรู้คือทุกอย่างที่รอจนถึงครั้งแรกได้ดำเนินการแล้ว ทุกอย่างหลังจากการรอครั้งแรกจะทำงานในบางจุดที่ไม่ระบุในอนาคตที่คุณไม่สามารถเข้าถึงได้


6

ฉันคิดว่าคุณสามารถใช้async voidเพื่อเริ่มการทำงานของแบ็คกราวน์ได้เช่นกันตราบใดที่คุณระมัดระวังในการตรวจจับยกเว้น คิด?

class Program {

    static bool isFinished = false;

    static void Main(string[] args) {

        // Kick off the background operation and don't care about when it completes
        BackgroundWork();

        Console.WriteLine("Press enter when you're ready to stop the background operation.");
        Console.ReadLine();
        isFinished = true;
    }

    // Using async void to kickoff a background operation that nobody wants to be notified about when it completes.
    static async void BackgroundWork() {
        // It's important to catch exceptions so we don't crash the appliation.
        try {
            // This operation will end after ten interations or when the app closes. Whichever happens first.
            for (var count = 1; count <= 10 && !isFinished; count++) {
                await Task.Delay(1000);
                Console.WriteLine($"{count} seconds of work elapsed.");
            }
            Console.WriteLine("Background operation came to an end.");
        } catch (Exception x) {
            Console.WriteLine("Caught exception:");
            Console.WriteLine(x.ToString());
        }
    }
}

1
ปัญหาในการโทรเป็นโมฆะ async คือคุณไม่ได้รับงานคืนคุณไม่มีทางรู้ว่าเมื่องานของฟังก์ชั่นเสร็จสมบูรณ์
user8128167

วิธีนี้เหมาะสมสำหรับการดำเนินการพื้นหลัง (หายาก) ที่คุณไม่สนใจเกี่ยวกับผลลัพธ์และคุณไม่ต้องการให้มีข้อยกเว้นที่ส่งผลกระทบต่อการดำเนินการอื่น กรณีการใช้งานทั่วไปจะส่งเหตุการณ์บันทึกไปยังเซิร์ฟเวอร์บันทึก คุณต้องการให้สิ่งนี้เกิดขึ้นในเบื้องหลังและคุณไม่ต้องการให้ตัวจัดการบริการ / คำขอของคุณล้มเหลวหากเซิร์ฟเวอร์บันทึกไม่ทำงาน แน่นอนว่าคุณต้องตรวจจับข้อยกเว้นทั้งหมดไม่เช่นนั้นกระบวนการของคุณจะถูกยกเลิก หรือมีวิธีที่ดีกว่าเพื่อให้บรรลุนี้
Florian Winter

ข้อยกเว้นจากวิธีการ Async Void ไม่สามารถดักจับได้ msdn.microsoft.com/en-us/magazine/jj991977.aspx
Si Zi

3
@SiZi the catch เป็น INSIDE วิธีการโมฆะ async ดังที่แสดงในตัวอย่างและมันถูกจับ
bboyle1234

สิ่งที่เกี่ยวกับเมื่อความต้องการของคุณเปลี่ยนเป็นต้องไม่ทำงานถ้าคุณไม่สามารถเข้าสู่ระบบ - แล้วคุณต้องเปลี่ยนลายเซ็นใน API ทั้งหมดของคุณแทนการเปลี่ยนตรรกะซึ่งขณะนี้มี // ละเว้นการยกเว้น
Milney

0

คำตอบของฉันง่าย ๆ ที่คุณไม่สามารถรอวิธีการโมฆะ

Error   CS4008  Cannot await 'void' TestAsync   e:\test\TestAsync\TestAsyncProgram.cs

ดังนั้นหากวิธีการนั้นเป็นแบบอะซิงจะเป็นการดีกว่าที่จะต้องรอเพราะคุณสามารถปล่อยข้อได้เปรียบแบบอะซิงก์ได้


-2

ตามเอกสารของ Microsoftไม่ควรใช้async void

อย่าทำสิ่งนี้: ตัวอย่างต่อไปนี้ใช้async voidซึ่งทำให้การร้องขอ HTTP เสร็จสมบูรณ์เมื่อถึงการรอครั้งแรก:

  • ซึ่งเป็นแนวปฏิบัติที่ไม่ดีในแอป ASP.NET Core เสมอ

  • เข้าถึง HttpResponse หลังจากการร้องขอ HTTP เสร็จสมบูรณ์

  • เกิดปัญหากระบวนการ

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