วิธีการโทรอย่างปลอดภัยวิธี async ใน C # โดยไม่ต้องรอ


322

ฉันมีasyncวิธีการที่ไม่ส่งคืนข้อมูล:

public async Task MyAsyncMethod()
{
    // do some stuff async, don't return any data
}

ฉันกำลังเรียกสิ่งนี้จากวิธีอื่นซึ่งคืนค่าข้อมูลบางอย่าง

public string GetStringData()
{
    MyAsyncMethod(); // this generates a warning and swallows exceptions
    return "hello world";
}

การโทรMyAsyncMethod()โดยไม่รอมันทำให้เกิด " เพราะการโทรนี้ไม่รอวิธีการปัจจุบันยังคงทำงานก่อนที่การโทรจะเสร็จสิ้น " เตือนใน visual studio ในหน้าคำเตือนนั้นระบุว่า:

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

ฉันแน่ใจว่าฉันไม่ต้องการรอให้การโทรเสร็จสมบูรณ์ ฉันไม่ต้องการหรือมีเวลา แต่การโทรอาจเพิ่มข้อยกเว้น

ฉันพบปัญหานี้สองสามครั้งและฉันแน่ใจว่าเป็นปัญหาทั่วไปที่ต้องมีวิธีแก้ไขปัญหาร่วมกัน

ฉันจะเรียกวิธีการ async อย่างปลอดภัยโดยไม่รอผลลัพธ์ได้อย่างไร

ปรับปรุง:

สำหรับคนที่แนะนำว่าฉันเพิ่งรอผลลัพธ์นี่คือรหัสที่ตอบสนองต่อคำขอทางเว็บในบริการเว็บของเรา (ASP.NET Web API) การรอในบริบท UI ทำให้เธรด UI ว่างอยู่ แต่การรอการร้องขอทางเว็บจะรอให้ภารกิจเสร็จสิ้นก่อนที่จะตอบกลับคำขอซึ่งจะเป็นการเพิ่มเวลาตอบกลับโดยไม่มีเหตุผล


ทำไมไม่เพียงแค่สร้างวิธีการทำให้เสร็จและไม่สนใจมันเลย เพราะถ้ามันทำงานบนเธรดพื้นหลัง จากนั้นโปรแกรมจะไม่หยุดการทำงานของโปรแกรม
Yahya

1
หากคุณไม่ต้องการรอผลลัพธ์ตัวเลือกเดียวคือเพิกเฉย / ไม่แสดงคำเตือน หากคุณไม่ต้องการที่จะรอให้ผล / ข้อยกเว้นแล้วMyAsyncMethod().Wait()
ปีเตอร์ Ritchie

1
เกี่ยวกับการแก้ไขของคุณ: นั่นไม่สมเหตุสมผลสำหรับฉัน บอกว่าการตอบสนองจะถูกส่งไปยังลูกค้า 1 วินาทีหลังจากการร้องขอและ 2 วินาทีในภายหลังวิธีการ async ของคุณจะส่งข้อยกเว้น คุณจะทำอะไรกับข้อยกเว้นนั้น? คุณไม่สามารถส่งให้ลูกค้าได้หากคำตอบของคุณถูกส่งไปแล้ว คุณจะทำอะไรกับมัน

2
@Romoku ยุติธรรมพอ สมมติว่ามีคนดูที่บันทึกอยู่ดี :)

2
การเปลี่ยนแปลงในสถานการณ์ ASP.NET Web API คือWeb API ที่โฮสต์ในตัวเองในกระบวนการมีอายุการใช้งานยาวนาน (เช่นการพูดบริการ Windows) ซึ่งการร้องขอสร้างงานพื้นหลังที่มีความยาวเพื่อทำบางสิ่งที่มีราคาแพง แต่ยังต้องการ รับการตอบกลับอย่างรวดเร็วด้วย HTTP 202 (ยอมรับแล้ว)
David Rubin

คำตอบ:


187

หากคุณต้องการได้รับการยกเว้น "แบบอะซิงโครนัส" คุณสามารถทำได้:

  MyAsyncMethod().
    ContinueWith(t => Console.WriteLine(t.Exception),
        TaskContinuationOptions.OnlyOnFaulted);

สิ่งนี้จะช่วยให้คุณสามารถจัดการกับข้อยกเว้นในเธรดอื่นที่ไม่ใช่เธรด "main" วิธีนี้คุณจะได้ไม่ต้อง "รอ" สำหรับการโทรไปMyAsyncMethod()จากหัวข้อที่โทรMyAsyncMethod; แต่ยังอนุญาตให้คุณทำสิ่งที่มีข้อยกเว้น - แต่เฉพาะในกรณีที่มีข้อยกเว้นเกิดขึ้น

ปรับปรุง:

ในทางเทคนิคคุณสามารถทำสิ่งที่คล้ายกับawait:

try
{
    await MyAsyncMethod().ConfigureAwait(false);
}
catch (Exception ex)
{
    Trace.WriteLine(ex);
}

... ซึ่งจะมีประโยชน์หากคุณจำเป็นต้องใช้try/ catch(หรือusing) โดยเฉพาะแต่ฉันคิดว่าContinueWithจะต้องชัดเจนกว่านี้เล็กน้อยเพราะคุณต้องรู้ว่าConfigureAwait(false)หมายถึงอะไร


7
ฉันเปลี่ยนวิธีการนี้เป็นวิธีการส่วนขยายบนTask: คลาสคงที่สาธารณะ AsyncUtility {โมฆะคงที่แบบสาธารณะ }} การใช้งาน: MyAsyncMethod (). PerformAsyncTaskWithoutAwait (t => log.ErrorFormat ("เกิดข้อผิดพลาดขณะเรียก MyAsyncMethod: \ n {0}", t.Exception));
ทำเครื่องหมาย Avenius

2
downvoter ความคิดเห็น? หากมีสิ่งผิดปกติในคำตอบฉันชอบที่จะรู้และ / หรือแก้ไข
Peter Ritchie

2
เฮ้ - ฉันไม่ได้ลงคะแนน แต่ ... คุณช่วยอธิบายการอัพเดทของคุณได้ไหม? โทรไม่ควรที่จะรอคอยดังนั้นถ้าคุณทำเช่นนั้นแล้วคุณจะทำอย่างไรกับการรอคอยสำหรับการโทร, คุณก็ไม่ได้ดำเนินการต่อในบริบทจับ ...
Bartosz

1
"รอ" ไม่ใช่คำอธิบายที่ถูกต้องในบริบทนี้ บรรทัดหลังจากที่ConfiguratAwait(false)ไม่ทำงานจนกว่างานจะเสร็จสมบูรณ์ แต่เธรดปัจจุบันไม่ได้ "รอ" (เช่นบล็อก) สำหรับสิ่งนั้นบรรทัดถัดไปจะถูกเรียกว่าแบบอะซิงโครนัสกับการรอคอยการร้องขอ หากไม่มีConfigureAwait(false)บรรทัดถัดไปนั้นจะถูกดำเนินการในบริบทคำขอเว็บเดิม ด้วยConfigurateAwait(false)ก็ดำเนินการในลักษณะเดียวกันเป็นวิธี async (การงาน) พ้นเดิมบริบท / ด้ายเพื่อดำเนินการต่อ ...
ปีเตอร์ Ritchie

15
รุ่น ContinueWith นั้นไม่เหมือนกับรุ่น try {await} catch {} ในเวอร์ชั่นแรกทุกอย่างหลังจาก ContinueWith () จะดำเนินการทันที ภารกิจเริ่มต้นถูกไล่ออกและถูกลืม ในเวอร์ชันที่สองทุกอย่างหลังจาก catch {} จะทำงานหลังจากเสร็จสิ้นภารกิจเริ่มต้นเท่านั้น รุ่นที่สองนั้นเทียบเท่ากับ "คอย MyAsyncMethod () ContinueWith (t => Console.WriteLine (t.Exception), TaskContinuationOptionsOnlyOnFaulted) .ConfigureAwait (fals);
Thanasis Ioannidis

67

แรกที่คุณควรพิจารณาการทำวิธีและมีมันงานที่กลับมาจากGetStringDataasyncawaitMyAsyncMethod

หากคุณแน่ใจอย่างแน่นอนว่าคุณไม่จำเป็นต้องจัดการกับข้อยกเว้นจากMyAsyncMethod หรือรู้ว่าเมื่อมันเสร็จสมบูรณ์แล้วคุณสามารถทำสิ่งนี้:

public string GetStringData()
{
  var _ = MyAsyncMethod();
  return "hello world";
}

BTW นี่ไม่ใช่ "ปัญหาทั่วไป" มันยากมากที่จะต้องการรันโค้ดบางอย่างและไม่สนใจว่ามันจะเสร็จสมบูรณ์และไม่สนใจว่ามันจะเสร็จสมบูรณ์หรือไม่

ปรับปรุง:

ตั้งแต่คุณอยู่ใน ASP.NET และต้องการที่จะกลับไปเริ่มต้นที่คุณอาจพบฉันบล็อกโพสต์เกี่ยวกับเรื่องที่มีประโยชน์ อย่างไรก็ตาม ASP.NET ไม่ได้ถูกออกแบบมาสำหรับสิ่งนี้และไม่รับประกันว่ารหัสของคุณจะทำงานหลังจากที่ส่งคืนการตอบสนอง ASP.NET จะทำอย่างดีที่สุดเพื่อให้มันรัน แต่ก็ไม่สามารถรับประกันได้

ดังนั้นนี่เป็นวิธีแก้ปัญหาที่ดีสำหรับสิ่งที่เรียบง่ายเช่นการโยนเหตุการณ์ลงในบันทึกซึ่งไม่สำคัญเลยถ้าคุณทำไม่กี่ที่นี่และที่นั่น มันไม่ใช่ทางออกที่ดีสำหรับการดำเนินธุรกิจที่สำคัญใด ๆ ในสถานการณ์เหล่านั้นคุณต้องปรับใช้สถาปัตยกรรมที่ซับซ้อนยิ่งขึ้นด้วยวิธีถาวรเพื่อบันทึกการดำเนินการ (เช่น Azure Queues, MSMQ) และกระบวนการพื้นหลังแยกต่างหาก (เช่น Azure Worker Role, Win32 Service) เพื่อประมวลผล


2
ฉันคิดว่าคุณอาจเข้าใจผิด ฉันทำดูแลถ้ามันจะพ่นข้อยกเว้นและล้มเหลว แต่ฉันไม่ต้องการให้มีการรอคอยวิธีการก่อนที่จะกลับข้อมูลของฉัน ดูการแก้ไขของฉันเกี่ยวกับบริบทที่ฉันกำลังทำอยู่หากมันสร้างความแตกต่าง
George Powell

5
@GeorgePowell: มันอันตรายมากที่มีรหัสที่ทำงานอยู่ในบริบท ASP.NET โดยไม่ต้องมีการร้องขอที่ใช้งานอยู่ ฉันมีโพสต์บล็อกที่อาจช่วยคุณได้แต่ไม่ทราบปัญหาของคุณมากขึ้นฉันไม่สามารถพูดได้ว่าฉันแนะนำวิธีการดังกล่าวหรือไม่
Stephen Cleary

@StephenCleary ฉันมีความต้องการที่คล้ายกัน ในตัวอย่างของฉันฉันมี / ต้องการเครื่องมือประมวลผลแบบชุดเพื่อให้ทำงานในระบบคลาวด์ฉันจะ "ping" จุดสิ้นสุดเพื่อเริ่มการประมวลผลแบบกลุ่ม แต่ฉันต้องการกลับทันที ตั้งแต่ส่งเสียงเริ่มทำงานมันสามารถจัดการทุกอย่างได้จากตรงนั้น หากมีข้อยกเว้นที่ถูกโยนออกไปพวกเขาจะถูกบันทึกไว้ในตาราง "BatchProcessLog / Error" ของฉัน ...
ganders

8
ใน C # 7 คุณสามารถแทนที่ด้วยvar _ = MyAsyncMethod(); _ = MyAsyncMethod();สิ่งนี้ยังคงหลีกเลี่ยงการเตือน CS4014 แต่ทำให้ชัดเจนยิ่งขึ้นอีกเล็กน้อยว่าคุณไม่ได้ใช้ตัวแปร
Brian

48

คำตอบของ Peter Ritchie คือสิ่งที่ฉันต้องการและบทความของ Stephen Clearyเกี่ยวกับการคืนต้นใน ASP.NET นั้นมีประโยชน์มาก

ในฐานะที่เป็นปัญหาทั่วไปมากขึ้น (ไม่เฉพาะบริบทของ ASP.NET) แอปพลิเคชั่นคอนโซลต่อไปนี้แสดงให้เห็นถึงการใช้งานและพฤติกรรมของคำตอบของ Peter โดยใช้ Task.ContinueWith(...)

static void Main(string[] args)
{
  try
  {
    // output "hello world" as method returns early
    Console.WriteLine(GetStringData());
  }
  catch
  {
    // Exception is NOT caught here
  }
  Console.ReadLine();
}

public static string GetStringData()
{
  MyAsyncMethod().ContinueWith(OnMyAsyncMethodFailed, TaskContinuationOptions.OnlyOnFaulted);
  return "hello world";
}

public static async Task MyAsyncMethod()
{
  await Task.Run(() => { throw new Exception("thrown on background thread"); });
}

public static void OnMyAsyncMethodFailed(Task task)
{
  Exception ex = task.Exception;
  // Deal with exceptions here however you want
}

GetStringData()ผลตอบแทนต้นโดยไม่ต้องรอMyAsyncMethod()และข้อยกเว้นโยนเข้ามาMyAsyncMethod()จะได้รับการจัดการในOnMyAsyncMethodFailed(Task task)และไม่ได้อยู่ในtry/ catchรอบGetStringData()


9
ลบConsole.ReadLine();และเพิ่มการนอนหลับ / ล่าช้าเล็กน้อยMyAsyncMethodและคุณจะไม่เห็นข้อยกเว้น
tymtam

17

ฉันท้ายด้วยโซลูชันนี้:

public async Task MyAsyncMethod()
{
    // do some stuff async, don't return any data
}

public string GetStringData()
{
    // Run async, no warning, exception are catched
    RunAsync(MyAsyncMethod()); 
    return "hello world";
}

private void RunAsync(Task task)
{
    task.ContinueWith(t =>
    {
        ILog log = ServiceLocator.Current.GetInstance<ILog>();
        log.Error("Unexpected Error", t.Exception);

    }, TaskContinuationOptions.OnlyOnFaulted);
}

8

สิ่งนี้เรียกว่าไฟและลืมและมีส่วนเสริมสำหรับสิ่งนั้น

กินงานและไม่ทำอะไรกับมัน มีประโยชน์สำหรับการโทรแบบดับเพลิงและลืมไปยังเมธอด async ภายในเมธอด async

ติดตั้งแพคเกจ nuget

ใช้:

MyAsyncMethod().Forget();

12
มันไม่ได้ทำอะไรเลยมันเป็นเพียงเคล็ดลับในการลบคำเตือน ดูgithub.com/microsoft/vs-threading/blob/master/src/…
Paolo Fulgoni

2

ฉันเดาคำถามที่เกิดขึ้นทำไมคุณต้องทำเช่นนี้? เหตุผลasyncใน C # 5.0 คือเพื่อให้คุณสามารถรอผล วิธีนี้ไม่ได้เป็นแบบอะซิงโครนัส แต่เรียกในเวลาเดียวเพื่อไม่ให้รบกวนเธรดปัจจุบันมากเกินไป

อาจเป็นการดีกว่าถ้าคุณเริ่มต้นเธรดและปล่อยให้เสร็จด้วยตนเอง


2
asyncเป็นมากกว่าเพียงแค่ "รอ" ผลลัพธ์ "await" หมายถึงว่าบรรทัดต่อไปนี้ "await" ถูกดำเนินการแบบอะซิงโครนัสบนเธรดเดียวกันที่เรียกว่า "คอย" สามารถทำได้โดยไม่ต้อง "รอ" แน่นอน แต่คุณต้องมีผู้ได้รับมอบหมายจำนวนมากและเสียรูปลักษณ์และความรู้สึกต่อเนื่องของรหัส (เช่นเดียวกับความสามารถในการใช้usingและtry/catch...
Peter Ritchie

1
@PeterRitchie คุณสามารถมีวิธีที่ทำงานแบบอะซิงโครนัสไม่ใช้awaitคำหลักและไม่ใช้asyncคำหลัก แต่ไม่มีประเด็นใด ๆ ในการใช้asyncคำหลักโดยไม่ต้องใช้awaitคำจำกัดความของวิธีการนั้น
Servy

1
@PeterRitchie จากคำสั่ง: " asyncเป็นมากกว่าเพียงแค่" รอ "ผล" asyncคำหลัก (คุณนัยมันเป็นคำหลักโดยแนบไว้ใน backticks) หมายถึงอะไรมากกว่ารอผล มันเป็นแบบอะซิงโครนัสตามแนวคิดทั่วไปของ CS นั่นหมายถึงมากกว่าแค่รอผล
Servy

1
@Servy asyncสร้างเครื่องสถานะที่จัดการการรอคอยใด ๆ ภายในวิธีการ async หากไม่มีawaits ภายในวิธีการนั้นจะยังคงสร้างเครื่องสถานะนั้น - แต่วิธีการนั้นไม่ตรงกัน และถ้าasyncวิธีการกลับvoidมาไม่มีอะไรต้องรอ ดังนั้นจึงเป็นมากขึ้นกว่าเพียงแค่การรอผล
Peter Ritchie

1
@PeterRitchie ตราบใดที่วิธีคืนค่างานคุณสามารถรอได้ ไม่จำเป็นต้องมีเครื่องรัฐหรือasyncคำหลัก สิ่งที่คุณต้องทำ (และสิ่งที่เกิดขึ้นจริง ๆ ในตอนท้ายด้วยเครื่องรัฐในกรณีพิเศษนั้น) คือวิธีการนั้นถูกเรียกใช้พร้อมกัน ฉันคิดว่าในทางเทคนิคคุณไม่เพียง แต่ลบเครื่องสถานะ Task.FromResultคุณเอาเครื่องของรัฐและการโทรแล้ว ฉันคิดว่าคุณ (และนักเขียนคอมไพเลอร์) สามารถเพิ่มภาคผนวกด้วยตัวคุณเอง
Servy

0

ในเทคโนโลยีที่มีลูปข้อความ (ไม่แน่ใจว่า ASP เป็นหนึ่งในนั้น) คุณสามารถบล็อกลูปและประมวลผลข้อความจนกว่างานจะจบและใช้ ContinueWith เพื่อปลดล็อครหัส:

public void WaitForTask(Task task)
{
    DispatcherFrame frame = new DispatcherFrame();
    task.ContinueWith(t => frame.Continue = false));
    Dispatcher.PushFrame(frame);
}

วิธีนี้คล้ายกับการบล็อกบน ShowDialog และยังคงรักษาการตอบสนองของ UI


0

ฉันไปงานปาร์ตี้ที่นี่ช้า แต่มีห้องสมุดที่ยอดเยี่ยมที่ฉันเคยใช้ซึ่งฉันไม่ได้เห็นอ้างอิงในคำตอบอื่น

https://github.com/brminnick/AsyncAwaitBestPractices

หากคุณต้องการ "Fire And Forget" คุณจะต้องเรียกใช้วิธีการต่อพ่วงกับงานนั้น ๆ

การส่งต่อการดำเนินการยกเว้นไปยังการโทรช่วยให้แน่ใจว่าคุณได้รับสิ่งที่ดีที่สุดทั้งสองโลก - ไม่จำเป็นต้องรอการดำเนินการและทำให้ผู้ใช้ของคุณช้าลงในขณะที่ยังคงความสามารถในการจัดการข้อยกเว้น

ในตัวอย่างของคุณคุณจะใช้มันเช่นนี้:

   public string GetStringData()
    {
        MyAsyncMethod().SafeFireAndForget(onException: (exception) =>
                    {
                      //DO STUFF WITH THE EXCEPTION                    
                    }); 
        return "hello world";
    }

นอกจากนี้ยังช่วยให้ AsyncCommands ที่รอคอยการใช้ ICommand ออกจากกล่องซึ่งเหมาะสำหรับ MVVM Xamarin ของฉัน


-1

การแก้ปัญหาคือการเริ่มต้น HttpClient เป็นงานการดำเนินการอื่นโดยไม่มีบริบท sincronization:

var submit = httpClient.PostAsync(uri, new StringContent(body, Encoding.UTF8,"application/json"));
var t = Task.Run(() => submit.ConfigureAwait(false));
await t.ConfigureAwait(false);

-1

หากคุณต้องการทำสิ่งนี้จริงๆ เพียงเพื่อที่อยู่ "เรียกวิธีการ async ใน C # โดยไม่ต้องรอคอย" คุณสามารถดำเนินการวิธี async Task.Runภายใน วิธีนี้จะรอจนกว่าจะMyAsyncMethodเสร็จ

public string GetStringData()
{
    Task.Run(()=> MyAsyncMethod()).Result;
    return "hello world";
}

awaitอะซิงโครนัสปลดงานResultของคุณในขณะที่เพียงใช้ผลลัพธ์จะบล็อกจนกว่างานจะเสร็จสมบูรณ์


-1

โดยทั่วไปเมธอด async จะส่งคืนคลาส Task หากคุณใช้Wait()เมธอดหรือResultคุณสมบัติและโค้ดโยนข้อยกเว้น - ชนิดของข้อยกเว้นจะถูกรวมเข้าด้วยกันAggregateException- คุณต้องค้นหาException.InnerExceptionเพื่อค้นหาข้อยกเว้นที่ถูกต้อง

แต่ก็เป็นไปได้ที่จะใช้.GetAwaiter().GetResult()แทน - มันจะรองาน async แต่จะไม่ตัดข้อยกเว้น

ดังนั้นนี่คือตัวอย่างสั้น ๆ :

public async Task MyMethodAsync()
{
}

public string GetStringData()
{
    MyMethodAsync().GetAwaiter().GetResult();
    return "test";
}

คุณอาจต้องการส่งคืนพารามิเตอร์บางส่วนจากฟังก์ชัน async ซึ่งสามารถทำได้โดยการเพิ่มพิเศษAction<return type>ในฟังก์ชัน async ตัวอย่างเช่นนี้:

public string GetStringData()
{
    return MyMethodWithReturnParameterAsync().GetAwaiter().GetResult();
}

public async Task<String> MyMethodWithReturnParameterAsync()
{
    return "test";
}

โปรดทราบว่าโดยทั่วไปวิธีการ async จะมีASyncการตั้งชื่อต่อท้ายเพียงเพื่อให้สามารถหลีกเลี่ยงการชนกันระหว่างฟังก์ชั่นการซิงค์ด้วยชื่อเดียวกัน (เช่นFileStream.ReadAsync) - ฉันได้อัปเดตชื่อฟังก์ชันเพื่อทำตามคำแนะนำนี้

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