คุณจะสร้างวิธีแบบอะซิงโครนัสใน C # ได้อย่างไร


196

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

private async void button1_Click(object sender, EventArgs e)
{
    var now = await CountToAsync(1000);
    label1.Text = now.ToString();
}

และฉันเขียนวิธีนี้คือCountToAsync:

private Task<DateTime> CountToAsync(int num = 1000)
{
    return Task.Factory.StartNew(() =>
    {
        for (int i = 0; i < num; i++)
        {
            Console.WriteLine("#{0}", i);
        }
    }).ContinueWith(x => DateTime.Now);
}

นี่คือการใช้Task.Factoryวิธีที่ดีที่สุดในการเขียนวิธีการแบบอะซิงโครนัสหรือฉันควรจะเขียนอีกวิธีนี้หรือไม่?


22
ฉันถามคำถามทั่วไปเกี่ยวกับวิธีจัดโครงสร้างวิธีการ ฉันแค่อยากรู้ว่าจะเริ่มต้นอย่างไรในการเปลี่ยนวิธีการแบบซิงโครนัสของฉันเป็นแบบอะซิงโครนัส
Khalid Abuhakmeh

2
ตกลงดังนั้นวิธีการซิงโครนัสทั่วไปทำอะไรและทำไมคุณต้องการที่จะทำให้มันไม่ตรงกัน ?
Eric Lippert

สมมติว่าฉันต้องแบตช์ไฟล์จำนวนมากและส่งคืนวัตถุผลลัพธ์
Khalid Abuhakmeh

1
ตกลงดังนั้น: (1) การดำเนินการแฝงสูงคืออะไร: การรับไฟล์ - เพราะเครือข่ายอาจช้าหรืออะไรก็ตาม - หรือทำการประมวลผล - เพราะมันใช้งาน CPU มาก และ (2) คุณยังไม่ได้พูดทำไมคุณต้องการให้มันไม่ตรงกันในตอนแรก มีเธรด UI ที่คุณไม่ต้องการบล็อกหรืออะไร
Eric Lippert

21
@EricLippert ตัวอย่างที่ให้โดย op เป็นพื้นฐานมากมันไม่จำเป็นต้องซับซ้อนขนาดนั้น
David B.

คำตอบ:


227

ฉันไม่แนะนำStartNewเว้นแต่คุณจะต้องการความซับซ้อนในระดับนั้น

หากวิธีการซิงค์ของคุณขึ้นอยู่กับวิธีการซิงค์แบบอื่นวิธีที่ง่ายที่สุดคือใช้asyncคำหลัก

private static async Task<DateTime> CountToAsync(int num = 10)
{
  for (int i = 0; i < num; i++)
  {
    await Task.Delay(TimeSpan.FromSeconds(1));
  }

  return DateTime.Now;
}

หากวิธี async ของคุณจะทำ CPU ทำงานคุณควรใช้Task.Run:

private static async Task<DateTime> CountToAsync(int num = 10)
{
  await Task.Run(() => ...);
  return DateTime.Now;
}

คุณอาจพบว่าasync/ awaitบทนำของฉันเป็นประโยชน์


10
@ สตีเฟ่น: "ถ้าวิธีการ async ของคุณเป็น dependanton วิธีการ async อื่น ๆ " - ตกลง แต่จะทำอย่างไรถ้าไม่เป็นเช่นนั้น เกิดอะไรขึ้นถ้าพยายามล้อมโค้ดบางอย่างที่เรียก BeginInvoke ด้วยรหัสโทรกลับบางส่วน
Ricibob

1
@Ricibob: คุณควรใช้ในการห่อTaskFactory.FromAsync BeginInvokeไม่แน่ใจว่าคุณหมายถึงอะไรเกี่ยวกับ "รหัสโทรกลับ"; อย่าลังเลที่จะโพสต์คำถามของคุณเองด้วยรหัส
Stephen Cleary

@ สตีเฟ่น: ขอบคุณ - ใช่ TaskFactory.FromAsync เป็นสิ่งที่ฉันกำลังมองหา
Ricibob

1
สตีเฟ่นใน for for วนซ้ำถัดไปเรียกว่าทันทีหรือหลังTask.Delayผลตอบแทน?
jp2code

3
@ jp2code: awaitเป็นการรอแบบอะซิงโครนัสดังนั้นจึงไม่ดำเนินการซ้ำต่อไปจนกว่าจะTask.Delayเสร็จสิ้นภารกิจที่ส่งคืนโดยสมบูรณ์
สตีเฟ่นเคลียร์

12

หากคุณไม่ต้องการใช้ async / await ในวิธีการของคุณ แต่ยังคง "ตกแต่ง" เพื่อให้สามารถใช้คำหลักที่รอคอยจากภายนอกTaskCompletionSource.cs :

public static Task<T> RunAsync<T>(Func<T> function)
{ 
    if (function == null) throw new ArgumentNullException(“function”); 
    var tcs = new TaskCompletionSource<T>(); 
    ThreadPool.QueueUserWorkItem(_ =>          
    { 
        try 
        {  
           T result = function(); 
           tcs.SetResult(result);  
        } 
        catch(Exception exc) { tcs.SetException(exc); } 
   }); 
   return tcs.Task; 
}

จากที่นี่และที่นี่

เพื่อสนับสนุนกระบวนทัศน์ดังกล่าวกับ Tasks เราจำเป็นต้องมีวิธีในการรักษาส่วนหน้าของงานและความสามารถในการอ้างถึงการดำเนินการแบบอะซิงโครนัสโดยพลการในฐานะงาน แต่เพื่อควบคุมอายุการใช้งานของงานนั้นตามกฎของ อะซิงโครนัสและต้องทำในลักษณะที่ไม่เสียค่าใช้จ่ายมากนัก นี่คือจุดประสงค์ของ TaskCompletionSource

ฉันเห็นยังใช้ใน. NET แหล่งเช่น WebClient.cs :

    [HostProtection(ExternalThreading = true)]
    [ComVisible(false)]
    public Task<string> UploadStringTaskAsync(Uri address, string method, string data)
    {
        // Create the task to be returned
        var tcs = new TaskCompletionSource<string>(address);

        // Setup the callback event handler
        UploadStringCompletedEventHandler handler = null;
        handler = (sender, e) => HandleCompletion(tcs, e, (args) => args.Result, handler, (webClient, completion) => webClient.UploadStringCompleted -= completion);
        this.UploadStringCompleted += handler;

        // Start the async operation.
        try { this.UploadStringAsync(address, method, data, tcs); }
        catch
        {
            this.UploadStringCompleted -= handler;
            throw;
        }

        // Return the task that represents the async operation
        return tcs.Task;
    }

ในที่สุดฉันก็พบว่ามีประโยชน์เช่นกัน:

ฉันถูกถามคำถามนี้ตลอดเวลา ความหมายคือต้องมีเธรดบางแห่งที่บล็อกในการโทร I / O ไปยังทรัพยากรภายนอก ดังนั้นรหัสแบบอะซิงโครนัสทำให้เธรดร้องขอเพิ่มขึ้น แต่มีค่าใช้จ่ายของเธรดอื่นที่อื่นในระบบใช่ไหม ไม่เลย. เพื่อให้เข้าใจว่าทำไมขนาดคำขอแบบอะซิงโครนัสฉันจะติดตามตัวอย่าง (ตัวย่อ) ของการโทรแบบ I / O แบบอะซิงโครนัส สมมติว่าคำขอต้องเขียนไปยังไฟล์ เธรดคำร้องขอเรียกวิธีการเขียนแบบอะซิงโครนัส WriteAsync ดำเนินการโดย Base Class Library (BCL) และใช้พอร์ตที่สมบูรณ์สำหรับ I / O แบบอะซิงโครนัส ดังนั้นการเรียกใช้ WriteAsync จึงถูกส่งผ่านไปยัง OS เพื่อเขียนไฟล์อะซิงโครนัส จากนั้นระบบปฏิบัติการจะสื่อสารกับไดรเวอร์สแต็คส่งผ่านข้อมูลเพื่อเขียนใน I / O Request packet (IRP) นี่คือสิ่งที่น่าสนใจ: หากไดรเวอร์อุปกรณ์ไม่สามารถจัดการ IRP ได้ทันทีจะต้องจัดการแบบอะซิงโครนัส ดังนั้นไดรเวอร์บอกให้ดิสก์เริ่มเขียนและส่งกลับการตอบสนอง“ รอดำเนินการ” ไปยังระบบปฏิบัติการ ระบบปฏิบัติการผ่านการตอบสนอง“ ที่รอดำเนินการ” ไปยัง BCL และ BCL ส่งคืนงานที่ไม่สมบูรณ์ไปยังรหัสการจัดการคำขอ รหัสการจัดการการร้องขอกำลังรองานซึ่งจะส่งคืนงานที่ไม่สมบูรณ์จากวิธีการนั้นเป็นต้น ในที่สุดรหัสการจัดการการร้องขอจะจบลงด้วยการส่งคืนงานที่ไม่สมบูรณ์ไปยัง ASP.NET และเธรดการร้องขอนั้นจะถูกปล่อยให้กลับไปยังพูลเธรด รหัสการจัดการการร้องขอกำลังรองานซึ่งจะส่งคืนงานที่ไม่สมบูรณ์จากวิธีการนั้นเป็นต้น ในที่สุดรหัสการจัดการการร้องขอจะจบลงด้วยการส่งคืนงานที่ไม่สมบูรณ์ไปยัง ASP.NET และเธรดการร้องขอนั้นจะถูกปล่อยให้กลับไปยังพูลเธรด รหัสการจัดการการร้องขอกำลังรองานซึ่งจะส่งคืนงานที่ไม่สมบูรณ์จากวิธีการนั้นเป็นต้น ในที่สุดรหัสการจัดการการร้องขอจะจบลงด้วยการส่งคืนงานที่ไม่สมบูรณ์ไปยัง ASP.NET และเธรดการร้องขอนั้นจะถูกปล่อยให้กลับไปยังพูลเธรด

บทนำสู่ Async / Await บน ASP.NET

หากเป้าหมายคือการปรับปรุงความสามารถในการขยาย (มากกว่าการตอบสนอง) เป้าหมายทั้งหมดนั้นขึ้นอยู่กับการมีอยู่ของ I / O ภายนอกที่ให้โอกาสในการทำเช่นนั้น


1
หากคุณจะเห็นความคิดเห็นด้านบนการใช้งาน Task.Runเช่นQueues งานที่ระบุเพื่อทำงานบน ThreadPool และส่งคืนตัวจัดการงานสำหรับงานนั้น คุณทำเช่นเดียวกัน ฉันแน่ใจว่า Task.Run จะดีกว่าทำให้จัดการการยกเลิกภายในและทำให้การเพิ่มประสิทธิภาพบางอย่างด้วยการตั้งเวลาและตัวเลือกการเรียกใช้
unsafePtr

ดังนั้นสิ่งที่คุณทำกับ ThreadPool.QueueUserWorkItem สามารถทำได้ด้วย Task.Run ใช่ไหม
Razvan

-1

วิธีหนึ่งที่ง่ายมากในการสร้างวิธีการแบบอะซิงโครนัสคือการใช้วิธีการ Task.Yield () ตามที่ MSDN ระบุ:

คุณสามารถใช้งานที่รอ Task.Yield (); ในวิธีการแบบอะซิงโครนัสเพื่อบังคับให้วิธีการแบบอะซิงโครนัสสมบูรณ์

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

private async Task<DateTime> CountToAsync(int num = 1000)
{
    await Task.Yield();
    for (int i = 0; i < num; i++)
    {
        Console.WriteLine("#{0}", i);
    }
    return DateTime.Now;
}

ในแอปพลิเคชัน WinForms วิธีการที่เหลือจะเสร็จสิ้นในเธรดเดียวกัน (เธรด UI) Task.Yield()จะเป็นประโยชน์แน่นอนสำหรับกรณีที่คุณต้องการเพื่อให้แน่ใจว่ากลับมาTaskจะไม่สมบูรณ์ทันทีที่สร้าง
Theodor Zoulias
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.