มีอะไรใหม่ใน .NET Core 2 และ C# 7 : Generalized Async Return Types
มีอะไรใหม่ใน .NET Core 2 และ C# 7 : Generalized Async Return Types
Async ส่งค่ากลับได้หลากหลาย
คุณสมบัติ “การทำให้ค่าส่งกลับของ Async กว้างขึ้น” (Generalized Async Return Types ย่อ GART)เป็นคุณสมบัติใหม่ของภาษา C# 7.0 ที่ช่วยให้การส่งค่ากลับจาก Method แบบ Async ไม่จำเป็นต้องมีชนิดข้อมูลเป็น Object อย่างแต่ก่อน
เดิมทีการส่งค่ากลับจาก method แบบ Async เป็นได้แค่ task, task<T> หรือไม่ก็ void ซึ่งไม่ดี
เพราะการเป็น task เป็น Reference Type การใช้งานมันจะเกิดการจองที่หน่วยความจำสำหรับ object หรือที่เรียกว่า boxing ที่เราต้องการหลีกเลี่ยงเพราะจะทำให้เกิดปัญหาคอขวด
ในกรณีที่เราประกาศ Method โดยใส่ modifier async แล้วส่งค่ากลับเป็นสิ่งที่อยู่ใน cache หรือเป็นสิ่งที่ทำงานตรงจังหวะกันอย่างสมบูรณ์
จะมีผลให้เวลาที่ใช้ในการจองหน่วยความจำมีผลกระทบต่อประสิทธิภาพโดยเฉพาะอย่างยิ่งถ้าโค้ดนั้นอยู่ภายในลูปที่วนทำงานมากรอบเหรือเร็ว
ใน C# 7.0 ค่าส่งกลับไม่จำเป็นต้องเป็นแค่ task, task<T> และ void แต่อาจจะเป็น type อะไรก็ได้ ตราบที่ไปกันได้กับรูปแบบของ Async
นั่นคือจะต้องสามารถเรียกหา method GetAwaiter ได้ มี type ใหม่ถูกเพิ่มเข้าใน.Net Framework ชื่อ ValueTask เพื่อใช้ประโยชน์จากคุณสมบัติใหม่ของภาษานี้
รูปที่ 1
จาก รูปที่ 1 เป็นโค้ดแสดงตัวอย่าง method แบบ Async ที่ส่งค่ากลับเป็นชนิดข้อมูลแบบ Task<T>
บรรทัด 22-33 คือ นิยาม method GetLeisureHours() ที่เป็น method แบบ Async ซึ่งต้องการส่งค่ากลับเป็นเลขจำนวนเต็ม เราจึงต้องใช้คำสั่ง Task<int>
บรรทัด 15-21 คือนิยาม method ShowTodaysInfo() ซึ่งเป็น method แบบ Async ที่ต้องการส่งค่ากลับเป็น string เราจึงใช้คำสั่ง Task<string> ผลลัพธ์การทำงาน คือ บรรทัด 37-39
นี่เป็นการเขียนโค้ดแบบเก่าที่ไม่ได้ใช้คุณสมบัติ GART ให้เป็นประโยชน์
โปรดสังเกตว่าการส่งค่ากลับทั้งแบบ Task<int> และ Task<string> ล้วนแล้วแต่อยู่ในรูปของ object หรือ Reference Type ทั้งตอนส่งและตอนรับ
จะเกิดการ boxingและ unboxing ที่ทำให้เกิดค่าโสหุ้ยในแง่ของเวลาและความสิ้นเปลืองหน่วยความจำ
รูปที่ 2
บรรทัด 22-33 คือ นิยาม method GetLeisureHours() ที่เป็น method แบบ Async ซึ่งต้องการส่งค่ากลับเป็นเลขจำนวนเต็ม เราจึงต้องใช้คำสั่ง Task<int>
บรรทัด 15-21 คือนิยาม method ShowTodaysInfo() ซึ่งเป็น method แบบ Async ที่ต้องการส่งค่ากลับเป็น string เราจึงใช้คำสั่ง Task<string> ผลลัพธ์การทำงาน คือ บรรทัด 37-39
นี่เป็นการเขียนโค้ดแบบเก่าที่ไม่ได้ใช้คุณสมบัติ GART ให้เป็นประโยชน์
โปรดสังเกตว่าการส่งค่ากลับทั้งแบบ Task<int> และ Task<string> ล้วนแล้วแต่อยู่ในรูปของ object หรือ Reference Type ทั้งตอนส่งและตอนรับ
จะเกิดการ boxingและ unboxing ที่ทำให้เกิดค่าโสหุ้ยในแง่ของเวลาและความสิ้นเปลืองหน่วยความจำ
รูปที่ 2
เมื่อเรานิยาม method แบบ Async ที่ไม่มีค่าส่งกลับ เราต้องใส่คำว่า Task เป็น return type เหมือนการใส่ void
เมื่อเรานิยาม method แบบธรรมดาที่ไม่มีค่าส่งกลับ เมื่อเรามี method แบบนี้ และ ต้องการเรียกใช้มัน และ เราต้องใส่ตัวกระทำ await ถ้าอยากให้โค้ดที่เรียกคอยจนกว่ามันจะทำงานเสร็จ
Method WaitAndApologize() ใน รูปที่ 2 Method แบบ Async ที่เราใส่คำว่า Task เป็น return type ไว้แต่ไม่มีค่าส่งกลับในส่วนไส้ไม่มีคำสั่ง return เราเรียก method นี้จากใน method DisplayCurrentInfo() ที่เป็น method แบบ Async ด้วยเหมือนกัน
โดยเราต้องการให้ WaitAndApologize() ทำงานเสร็จก่อน จึงใส่ตัวกระทำ await ไว้หน้าการเรียก (ดูบรรทัดที่ 10)
เราเรียก method DisplayCurrentInfo() จากใน method Main และ เราต้องการมันทำงานจบก่อนจึงค่อยทำคำสั่งต่อไป
ในกรณีนี้เราจะใส่ตัวกระทำ await ไว้หน้าการเรียกไม่ได้ เพราะ method Main ไม่ได้เป็น method แบบ Async จึงต้องใช้การเรียก method Wait() แทน
เมื่อ runโปรแกรมจะเห็นผลลัพธ์เหมือนบรรทัดที่ 32 ถึง 37
รูปที่ 3
เมื่อเรานิยาม method แบบธรรมดาที่ไม่มีค่าส่งกลับ เมื่อเรามี method แบบนี้ และ ต้องการเรียกใช้มัน และ เราต้องใส่ตัวกระทำ await ถ้าอยากให้โค้ดที่เรียกคอยจนกว่ามันจะทำงานเสร็จ
Method WaitAndApologize() ใน รูปที่ 2 Method แบบ Async ที่เราใส่คำว่า Task เป็น return type ไว้แต่ไม่มีค่าส่งกลับในส่วนไส้ไม่มีคำสั่ง return เราเรียก method นี้จากใน method DisplayCurrentInfo() ที่เป็น method แบบ Async ด้วยเหมือนกัน
โดยเราต้องการให้ WaitAndApologize() ทำงานเสร็จก่อน จึงใส่ตัวกระทำ await ไว้หน้าการเรียก (ดูบรรทัดที่ 10)
เราเรียก method DisplayCurrentInfo() จากใน method Main และ เราต้องการมันทำงานจบก่อนจึงค่อยทำคำสั่งต่อไป
ในกรณีนี้เราจะใส่ตัวกระทำ await ไว้หน้าการเรียกไม่ได้ เพราะ method Main ไม่ได้เป็น method แบบ Async จึงต้องใช้การเรียก method Wait() แทน
เมื่อ runโปรแกรมจะเห็นผลลัพธ์เหมือนบรรทัดที่ 32 ถึง 37
รูปที่ 3
รูปที่ 4
การนิยาม method แบบ Async ที่ไม่มีค่าส่งกลับ เราจะใช้คำว่า void แทนคำว่า Task เป็น return type ก็ได้
สิ่งที่แตกต่างกัน คือ หากใช้ void โค้ดที่เรียกจะใช้คำสั่ง await เพื่อรอให้ทำงานเสร็จก่อนไม่ได้
การนิยาม method แบบ Async ที่ไม่มีค่าส่งกลับ เราควรจะใช้คำว่า Task เป็น Return type เสมอ
ยกเว้นกรณีที่ Method แบบ Async นั้นเป็นevent handler ซึ่งไม่มีค่าส่งกลับให้ใช้ void ได้
รูปที่ 3 คือตัวอย่างการใช้คำว่า void แทนคำว่า Task เป็น return type กับ method แบบ Async ที่เป็น event handler Class Counter ทำหน้าที่เป็นตัวนับที่จะส่ง event thresholdReachedEvent เมื่อการนับถึงจุดแบ่ง ดูคำว่า void บรรทัดที่ 34
รูปที่ 4 คือโค้ดแสดงตัวอย่างนำ class นี้มาใช้งาน
ผลลัพธ์การทำงานจะเหมือนบรรทัดที่ 55 ถึง 57
รูปที่ 5
สิ่งที่แตกต่างกัน คือ หากใช้ void โค้ดที่เรียกจะใช้คำสั่ง await เพื่อรอให้ทำงานเสร็จก่อนไม่ได้
การนิยาม method แบบ Async ที่ไม่มีค่าส่งกลับ เราควรจะใช้คำว่า Task เป็น Return type เสมอ
ยกเว้นกรณีที่ Method แบบ Async นั้นเป็นevent handler ซึ่งไม่มีค่าส่งกลับให้ใช้ void ได้
รูปที่ 3 คือตัวอย่างการใช้คำว่า void แทนคำว่า Task เป็น return type กับ method แบบ Async ที่เป็น event handler Class Counter ทำหน้าที่เป็นตัวนับที่จะส่ง event thresholdReachedEvent เมื่อการนับถึงจุดแบ่ง ดูคำว่า void บรรทัดที่ 34
รูปที่ 4 คือโค้ดแสดงตัวอย่างนำ class นี้มาใช้งาน
ผลลัพธ์การทำงานจะเหมือนบรรทัดที่ 55 ถึง 57
รูปที่ 5
การใช้ ValueTask<TResult>
คำว่า Task ที่เราใช้เป็น return typeในโค้ดตัวอย่างที่ผ่านมาคือ “class” (รวมถึง Task<TResult> ด้วย)ดังนั้นสิ่งที่มัน return จึงเป็น Referencetype ที่กินทรัพยากรมากกว่า Valuetype
ด้วยเหตุนี้ใน C# version 7 ขึ้นไปจึงเสนอตัวเลือกใหม่ที่เป็น Value type เป็น structure ชื่อ ValueTask ที่อยู่ใน Name Space System.Threading.Tasks.ValueTask<TResult>
ก่อนจะใช้งานได้จะต้องติดตั้ง Package ชื่อ System.Threading.Tasks.Extensions ผ่าน nuget เสียก่อน
เนื่องจาก ValueTask<TResult> เป็น Value Type จึงกินทรัพยากรน้อยกว่า Task<TResult> ที่เป็น Reference Type
รูปที่ 5 แสดงการนิยาม method ShowTodaysInfo() ที่เป็น method แบบ Async ที่ใช้ ValueTask เป็นค่าส่งกลับ ซึ่งเรียก method GetLeisureHours() ซึ่งก็เป็น method แบบ Async ที่ใช้ ValueTask เป็นค่าส่งกลับด้วยเช่นกัน
โค้ดที่เรียกให้สอง method นี้ทำงาน คือ บรรทัดที่ 32 ผลลัพธ์การทำงานเหมือนบรรทัดที่ 39 ถึง 41
คำว่า Generalized Async Return Types หมายถึง เราสามารถใช้ type อะไรก็ได้เป็นค่าส่งกลับของ method แบบ Async เพราะ ValueTask<TResult> เป็น generic
ดังนั้นเราจึงอาจนิยาม type ของค่าส่งกลับขึ้นเองก็ได้เช่นกัน
รูปที่ 6
การใช้ Async กับ Main
C# 7 มีคุณสมบัติอันหนึ่งที่นักพัฒนารอคอยมาอย่างยาวนาน คือ การมี method Main() ที่สามารถใช้งานร่วมกับตัวกระทำ async ได้ถ้าเรากำลังทำโปรแกรมที่จะถูกนำไปใช้เป็น Lybrary เราไม่ต้องนิยาม method Main()
แต่ถ้าเรากำลังทำโปรแกรมอย่างใดอย่างหนึ่งต่อไปนี้ Windows Forms App, UWP App, Console App, WPF App, ASP.NET และ ASP.NET Core App และ Xamarian App
เราจำเป็นจะต้องใส่นิยาม method Main() ที่ตอนนี้ใช้ async ได้แล้ว
รูปที่ 6 บรรทัด 2-5 แสดงให้เห็นว่าแต่เดิม method Main() มี signature อยู่ 4 แบบ ทุกแบบเป็น Static
สาเหตุที่ต้องเป็น Static เพื่อให้ตัว run time สามารถเรียกให้มันทำงานได้โดยตรง ไม่ต้องนำ class ที่มีนิยามของมันไปสร้าง object เสียก่อน
อันแรกเป็นแบบไม่มีพารามิเตอร์ ไม่ส่งค่าอะไรกลับ
อันที่สอง int เพื่อบอกว่าทำงานสำเร็จอย่างไร
อันที่ 3 และ 4 มีพารามิเตอร์ เป็น string array เพื่อให้ผู้เรียกส่ง argument ต่าง ๆ ไปให้ได้
ภาษา C# สนับสนุนการทำงานแบบ Multi Taking และการทำงานร่วมกับ Web Server ด้วยรูปแบบ async/await มาตั้งแต่ version 5 (สมัย .Net framework version 4.5) ที่ช่วยให้การทำงานแบบไม่ผสานจังหวะ (asynchronous operation) ราบรื่นขึ้น
แต่ถ้าเราต้องการใช้งานรูปแบบ async/await โดยเขียนโค้ดอย่างบรรทัด 7-10 จะ error จึงต้องเลี่ยงไปเขียนอย่างบรรทัด 12-15 หรืออย่างบรรทัด 17-20 ซึ่งแม้จะทำงานอย่างที่ต้องการได้แต่ก็อ่านเข้าใจยาก
C# 7.1 จึงเพิ่ม signature ของ method Main() อีก 4 แบบ ให้สามารถใช้งานร่วมกับตัวกระทำ async ได้เป็นแบบ Task ที่สามารถใช้ร่วมกับ async ได้ และ ภายใน Main() ก็ใช้คำสั่ง await ได้โดยตรง
รูปที่ 7
รูปที่ 7 แสดงตัวอย่างโค้ดเปรียบเทียบระหว่างการทำ async/await ก่อน และ หลัง C# 7.1
ในกรณีนี้เราเรียก method Delay() ของ class Task ที่จะหยุดการทำงานของโปรแกรมตามระยะเวลาที่กำหนด
ใน C#ก่อน 7.1 เราจำเป็นจะต้องเรียกหา method GetAwaiter() และ GetResult() เพื่อให้หยุดรอ (ดูบรรทัด 13)
แต่ใน C# 7.1 เป็นต้นไปเราสามารถใส่ async ไว้ที่นิยาม method Main() ได้ และใส่คำสั่ง await ไว้หน้า method Delay() ได้ ซึ่งทำให้โค้ดอ่านง่ายขึ้น
จากบทความ ท่านผู้อ่านจะเห็นว่า คุณสมบัติใหม่ของภาษา C# 7.0 จะเห็นว่า Async ส่งค่ากลับได้หลากหลายขึ้น
ทำให้เราแก้ไขปัญหาคอขวดจากการ boxing ได้ เพราะการส่งค่ากลับจาก Method แบบ Async ไม่จำเป็นต้องมีชนิดข้อมูลเป็น Object เหมือนอย่าง Version ก่อนหน้าครับ
สำหรับการใช้ Async นั้น ลองดูตัวอย่างการเขียนเพื่อให้ App ไม่ค้างตอนอ่านไฟล์ใหญ่ ได้ที่ บทความ มีอะไรใหม่ใน .NET Core 2 และ C# 7 : App ไม่ค้างตอนอ่านไฟล์ใหญ่ ได้เพิ่มเติมนะครับ
ในกรณีนี้เราเรียก method Delay() ของ class Task ที่จะหยุดการทำงานของโปรแกรมตามระยะเวลาที่กำหนด
ใน C#ก่อน 7.1 เราจำเป็นจะต้องเรียกหา method GetAwaiter() และ GetResult() เพื่อให้หยุดรอ (ดูบรรทัด 13)
แต่ใน C# 7.1 เป็นต้นไปเราสามารถใส่ async ไว้ที่นิยาม method Main() ได้ และใส่คำสั่ง await ไว้หน้า method Delay() ได้ ซึ่งทำให้โค้ดอ่านง่ายขึ้น
จากบทความ ท่านผู้อ่านจะเห็นว่า คุณสมบัติใหม่ของภาษา C# 7.0 จะเห็นว่า Async ส่งค่ากลับได้หลากหลายขึ้น
ทำให้เราแก้ไขปัญหาคอขวดจากการ boxing ได้ เพราะการส่งค่ากลับจาก Method แบบ Async ไม่จำเป็นต้องมีชนิดข้อมูลเป็น Object เหมือนอย่าง Version ก่อนหน้าครับ
สำหรับการใช้ Async นั้น ลองดูตัวอย่างการเขียนเพื่อให้ App ไม่ค้างตอนอ่านไฟล์ใหญ่ ได้ที่ บทความ มีอะไรใหม่ใน .NET Core 2 และ C# 7 : App ไม่ค้างตอนอ่านไฟล์ใหญ่ ได้เพิ่มเติมนะครับ