คุณต้องวาง Task.Run ลงในเมธอดเพื่อทำให้เป็น async หรือไม่?


304

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

ตัวอย่างที่ 1

private async Task DoWork1Async()
{
    int result = 1 + 2;
}

ตัวอย่างที่ 2

private async Task DoWork2Async()
{
    Task.Run( () =>
    {
        int result = 1 + 2;
    });
}

หากฉันรอDoWork1Async()รหัสจะทำงานพร้อมกันหรือไม่ตรงกัน?

ฉันจำเป็นต้องรวมโค้ดการซิงค์ไว้ด้วยTask.Runเพื่อให้วิธีนี้ไม่สามารถรอได้และแบบอะซิงโครนัสเพื่อไม่ให้บล็อกเธรด UI หรือไม่

ฉันพยายามที่จะคิดออกว่าวิธีการของฉันเป็นTaskหรือTask<T>ฉันจะต้องห่อรหัสด้วยTask.Runเพื่อให้มันไม่ตรงกัน

คำถามโง่ฉันแน่ใจ แต่ผมดูตัวอย่างในสุทธิที่ผู้คนกำลังรอรหัสที่มีอะไร async ภายในและไม่ได้อยู่ในห่อหรือTask.RunStartNew


30
ข้อมูลโค้ดแรกของคุณไม่ใช่คำเตือนใด ๆ ใช่หรือไม่
svick

คำตอบ:


586

อันดับแรกเรามาทำความเข้าใจกับคำศัพท์บางคำ: "asynchronous" ( async) หมายความว่ามันอาจให้การควบคุมกลับไปที่เธรดการโทรก่อนที่จะเริ่ม ในasyncวิธีการเหล่านั้นคะแนน "ผลผลิต" เป็นawaitนิพจน์

สิ่งนี้แตกต่างอย่างมากจากคำว่า "asynchronous" ซึ่งเป็น (mis) ที่ใช้โดยเอกสารคู่มือ MSDN เป็นเวลาหลายปีเพื่อหมายถึง "ดำเนินการบนเธรดพื้นหลัง"

เพื่อทำให้สับสนมากขึ้นปัญหาasyncแตกต่างจาก "รอ" มีบางasyncวิธีการที่มีผลตอบแทนประเภทไม่ได้ awaitable และวิธีการมากมายที่จะกลับประเภท awaitable asyncที่ไม่ได้

พอเกี่ยวกับสิ่งที่พวกเขาไม่ ; นี่คือสิ่งที่พวกเขาเป็น :

  • asyncคำหลักที่ช่วยให้วิธีการที่ไม่ตรงกัน (นั่นคือมันจะช่วยให้awaitการแสดงออก) asyncวิธีการอาจจะกลับมาTask, Task<T>หรือ void(ถ้าคุณต้อง)
  • รูปแบบใดก็ตามที่เป็นไปตามรูปแบบที่แน่นอนสามารถรอได้ ที่พบมากที่สุดประเภท awaitable มีและTaskTask<T>

ดังนั้นหากเราเปลี่ยนรูปแบบคำถามของคุณเป็น "ฉันจะเรียกใช้การดำเนินการบนเธรดพื้นหลังในลักษณะที่เป็นไปไม่ได้" คำตอบคือใช้Task.Run:

private Task<int> DoWorkAsync() // No async because the method does not need await
{
  return Task.Run(() =>
  {
    return 1 + 2;
  });
}

(แต่รูปแบบนี้เป็นวิธีที่ไม่ดีดูด้านล่าง)

แต่หากคำถามของคุณคือ "ฉันจะสร้างasyncวิธีการที่สามารถให้ผลตอบแทนกลับไปที่ผู้โทรแทนที่จะบล็อก" คำตอบคือประกาศวิธีการasyncและใช้awaitสำหรับคะแนน "ยอม" ของมัน:

private async Task<int> GetWebPageHtmlSizeAsync()
{
  var client = new HttpClient();
  var html = await client.GetAsync("http://www.example.com/");
  return html.Length;
}

ดังนั้นรูปแบบพื้นฐานของสิ่งต่าง ๆ คือการมีasyncโค้ดขึ้นอยู่กับ "awaitables" ในการawaitแสดงออก "awaitables" เหล่านี้อาจเป็นasyncวิธีการอื่นหรือเพียงแค่วิธีปกติที่กลับมารอ วิธีการปกติกลับมาTask/ Task<T> สามารถใช้Task.Runในการดำเนินการโค้ดบนด้ายพื้นหลังหรือ (มากกว่าปกติ) พวกเขาสามารถใช้TaskCompletionSource<T>หรือหนึ่งในทางลัดมัน ( TaskFactory.FromAsync, Task.FromResultฯลฯ ) ฉันไม่แนะนำให้ห่อทั้งวิธีในTask.Run; วิธีการแบบซิงโครนัสควรมีลายเซ็นแบบซิงโครนัสและควรถูกทิ้งไว้กับผู้บริโภคว่าควรห่อในTask.Run:

private int DoWork()
{
  return 1 + 2;
}

private void MoreSynchronousProcessing()
{
  // Execute it directly (synchronously), since we are also a synchronous method.
  var result = DoWork();
  ...
}

private async Task DoVariousThingsFromTheUIThreadAsync()
{
  // I have a bunch of async work to do, and I am executed on the UI thread.
  var result = await Task.Run(() => DoWork());
  ...
}

ฉันมีasync/ awaitแนะนำในบล็อกของฉัน; ในตอนท้ายเป็นแหล่งข้อมูลการติดตามที่ดี เอกสาร MSDN สำหรับasyncนั้นก็ดีเหมือนกัน


8
@sgnsajgon: ใช่ asyncวิธีการจะต้องกลับTask, หรือTask<T> และรอคอย; ไม่ใช่. voidTaskTask<T>void
Stephen Cleary

3
ที่จริงแล้วasync voidลายเซ็นวิธีการจะรวบรวมเป็นเพียงความคิดที่น่ากลัวมากในขณะที่คุณหลวมตัวชี้ไปที่งาน async ของคุณ
IEatBagels

4
@TopinFrassi: ใช่พวกเขาจะรวบรวม แต่voidไม่รอ
Stephen Cleary

4
@ohadinho: ไม่สิ่งที่ฉันกำลังพูดถึงในโพสต์บล็อกคือเมื่อวิธีทั้งหมดเป็นเพียงการโทรTask.Run(เช่นDoWorkAsyncในคำตอบนี้) การใช้Task.Runเพื่อเรียกเมธอดจากบริบท UI นั้นเหมาะสม (เช่นDoVariousThingsFromTheUIThreadAsync)
สตีเฟ่นเคลียร์

2
ใช่แล้ว มันสามารถใช้Task.Runเพื่อเรียกใช้เมธอดได้ แต่หากมีTask.Runโค้ดของเมธอดทั้งหมด (หรือเกือบทั้งหมด) นั่นก็เป็นรูปแบบการต่อต้าน - ให้เมธอดนั้นซิงโครนัสและเลื่อนTask.Runระดับขึ้นไป
Stephen Cleary

22

หนึ่งในสิ่งที่สำคัญที่สุดที่จะจำได้ว่าเมื่อตกแต่งวิธีการที่มีasyncคือว่าอย่างน้อยมีหนึ่ง รอคอยผู้ประกอบการภายในวิธีการ ในตัวอย่างของฉันจะแปลเป็นภาษาที่แสดงด้านล่างใช้TaskCompletionSource

private Task<int> DoWorkAsync()
{
    //create a task completion source
    //the type of the result value must be the same
    //as the type in the returning Task
    TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
    Task.Run(() =>
    {
        int result = 1 + 2;
        //set the result to TaskCompletionSource
        tcs.SetResult(result);
    });
    //return the Task
    return tcs.Task;
}

private async void DoWork()
{
    int result = await DoWorkAsync();
}

26
เหตุใดคุณจึงใช้ TaskCompletionSource แทนที่จะส่งคืนภารกิจที่ส่งคืนโดยเมธอด Task.Run () (และเปลี่ยนเนื้อความเพื่อส่งคืนผลลัพธ์)
แดกดัน

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

12

เมื่อคุณใช้ Task.Run เพื่อเรียกใช้เมธอด Task ได้รับเธรดจาก threadpool เพื่อเรียกใช้เมธอดนั้น ดังนั้นจากมุมมองของเธรด UI จึงเป็น "แบบอะซิงโครนัส" เนื่องจากไม่ได้บล็อกเธรด UI นี่เป็นเรื่องปกติสำหรับแอปพลิเคชันเดสก์ท็อปเนื่องจากคุณไม่จำเป็นต้องใช้หลายเธรดเพื่อดูแลการโต้ตอบของผู้ใช้

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

True Async ไม่จำเป็นต้องเกี่ยวข้องกับการใช้เธรดสำหรับการดำเนินการของ I / O เช่นการเข้าถึงไฟล์ / ฐานข้อมูลเป็นต้นคุณสามารถอ่านสิ่งนี้เพื่อเข้าใจว่าทำไมการดำเนินการ I / O จึงไม่ต้องการเธรด http://blog.stephencleary.com/2013/11/there-is-no-thread.html

ในตัวอย่างง่าย ๆ ของคุณมันเป็นการคำนวณแบบ CPU-bound ที่บริสุทธิ์ดังนั้นการใช้งาน Task.Run นั้นใช้ได้


ดังนั้นถ้าฉันต้องใช้ api ภายนอกแบบซิงโครนัสภายในตัวควบคุมเว็บ api ฉันไม่ควรตัดการโทรแบบซิงโครนัสใน Task.Run ()? อย่างที่คุณพูดการทำเช่นนั้นจะทำให้เธรดการร้องขอเริ่มต้นถูกบล็อก แต่จะใช้เธรดพูลอื่นเพื่อเรียก API ภายนอก ในความเป็นจริงฉันคิดว่ามันยังคงเป็นความคิดที่ดีเพราะการทำเช่นนี้ในทางทฤษฎีสามารถใช้เธรดพูลสองอันเพื่อประมวลผลคำขอจำนวนมากเช่นหนึ่งเธรดสามารถประมวลผลคำร้องขอขาเข้าจำนวนมากและอีกอันสามารถเรียก API ภายนอกสำหรับคำขอทั้งหมดเหล่านี้
stt106

ฉันเห็นด้วยฉันไม่ได้กำลังบอกว่าคุณไม่ควรปิดการโทรแบบซิงโครนัสทั้งหมดภายใน Task.Run () ฉันแค่ชี้ให้เห็นถึงปัญหาที่อาจเกิดขึ้น
zheng yu

1
@ stt106 I should NOT wrap the synchronous call in Task.Run()ถูกต้อง หากคุณเป็นเช่นนั้นคุณเพียงแค่สลับเธรด เช่นคุณกำลังยกเลิกการบล็อกเธรดเริ่มต้น แต่คุณใช้เธรดอื่นจากเธรดพูลซึ่งอาจถูกใช้เพื่อประมวลผลคำขออื่น ผลลัพธ์เดียวคือค่าใช้จ่ายในการสลับบริบทเมื่อการโทรเสร็จสิ้นเพื่อให้ได้รับเป็นศูนย์อย่างสมบูรณ์
Saeb Amini
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.