เหตุใดจึงใช้ Task <T> แทน ValueTask <T> ใน C #


168

ในฐานะของวิธีการซิงค์ C # 7.0 สามารถส่งคืน ValueTask <T> คำอธิบายบอกว่าควรใช้เมื่อเรามีผลแคชหรือจำลอง async ผ่านรหัสซิงโครนัส อย่างไรก็ตามฉันยังคงไม่เข้าใจปัญหาที่เกิดขึ้นกับการใช้ ValueTask เสมอหรือในความเป็นจริงว่าทำไม async / await ไม่ได้ถูกสร้างขึ้นด้วยประเภทค่าตั้งแต่เริ่มต้น ValueTask จะล้มเหลวในการทำงานเมื่อใด


7
ฉันสงสัยว่ามันจะทำอย่างไรกับผลประโยชน์ของValueTask<T>(ในแง่ของการจัดสรร) ไม่เป็นรูปธรรมสำหรับการดำเนินการที่ไม่ตรงกันจริง ๆ (เพราะในกรณีValueTask<T>นั้นจะยังคงต้องการการจัดสรรฮีป) นอกจากนี้ยังมีเรื่องของTask<T>การมีการสนับสนุนอื่น ๆ อีกมากมายภายในห้องสมุด
Jon Skeet

3
@JonSkeet ไลบรารีที่มีอยู่เป็นปัญหา แต่สิ่งนี้ทำให้เกิดคำถามว่างานควรเป็น ValueTask ตั้งแต่เริ่มต้นหรือไม่ อาจไม่มีประโยชน์เมื่อใช้กับสิ่ง async จริง แต่เป็นอันตรายหรือไม่?
Stilgar

8
ดูgithub.com/dotnet/corefx/issues/4708#issuecomment-160658188สติปัญญามากกว่าที่ผมจะสามารถที่จะถ่ายทอด :)
จอนสกีต


3
@JoelMueller ข้นพล็อต :)
Stilgar

คำตอบ:


245

จากเอกสาร API (เน้นการเพิ่ม):

เมธอดอาจส่งคืนอินสแตนซ์ของประเภทค่านี้เมื่อเป็นไปได้ว่าผลลัพธ์ของการดำเนินการของพวกเขาจะพร้อมใช้งานพร้อมกันและเมื่อคาดว่าจะมีการเรียกใช้เมธอดบ่อย ๆ ดังนั้นค่าใช้จ่ายในการจัดสรรใหม่Task<TResult>สำหรับการโทรแต่ละครั้ง

มีความสมดุลกับการใช้เป็นแทนValueTask<TResult> Task<TResult>ตัวอย่างเช่นในขณะที่ a ValueTask<TResult>สามารถช่วยหลีกเลี่ยงการจัดสรรในกรณีที่ผลลัพธ์ที่ประสบความสำเร็จพร้อมใช้งานพร้อมกัน แต่ก็มีสองฟิลด์ในขณะTask<TResult>ที่ประเภทอ้างอิงคือฟิลด์เดียว ซึ่งหมายความว่าการเรียกใช้เมธอดสิ้นสุดการส่งคืนสองฟิลด์ที่มีค่าของข้อมูลแทนหนึ่งซึ่งเป็นข้อมูลเพิ่มเติมที่จะคัดลอก นอกจากนี้ยังหมายความว่าหากวิธีการที่ส่งกลับอย่างใดอย่างหนึ่งเหล่านี้กำลังรออยู่ภายในasyncวิธีการเครื่องรัฐสำหรับasyncวิธีการที่จะมีขนาดใหญ่เนื่องจากจำเป็นต้องจัดเก็บโครงสร้างที่สองเขตแทนการอ้างอิงเดียว

นอกจากนี้สำหรับการใช้งานอื่น ๆ นอกเหนือจากการบริโภคผลมาจากการดำเนินงานที่ไม่ตรงกันผ่านawait, ValueTask<TResult>สามารถนำไปสู่การเขียนโปรแกรมแบบที่ซับซ้อนมากขึ้นซึ่งในสามารถเปิดจริงนำไปสู่การจัดสรรเพิ่มเติม ตัวอย่างเช่นพิจารณาวิธีการที่จะกลับมาอย่างใดอย่างหนึ่งให้กับงานที่เก็บไว้ชั่วคราวเป็นผลร่วมกันหรือTask<TResult> ValueTask<TResult>หากผู้บริโภคของผลลัพธ์ต้องการใช้มันเช่น a Task<TResult>, เพื่อใช้กับในวิธีการเช่นTask.WhenAllและTask.WhenAnyสิ่งValueTask<TResult>แรกนั้นจะต้องถูกแปลงเป็นการTask<TResult>ใช้AsTaskซึ่งจะนำไปสู่การจัดสรรที่จะหลีกเลี่ยงได้หากTask<TResult>มีการใช้แคชในที่แรก.

เช่นตัวเลือกเริ่มต้นสำหรับวิธีการไม่ตรงกันใด ๆ ควรจะกลับมาหรือTask Task<TResult>เฉพาะในกรณีที่การวิเคราะห์ประสิทธิภาพพิสูจน์ให้เห็นว่ามันคุ้มค่าที่ควรนำมาใช้แทนValueTask<TResult>Task<TResult>


7
@MattThomas: ประหยัดการTaskจัดสรรครั้งเดียว(ซึ่งมีขนาดเล็กและราคาถูกในทุกวันนี้) แต่ด้วยค่าใช้จ่ายในการจัดสรรการโทรที่มีอยู่ให้ใหญ่ขึ้นและเพิ่มขนาดของค่าตอบแทนสองเท่า (ส่งผลกระทบต่อการจัดสรรทะเบียน) ในขณะที่มันเป็นตัวเลือกที่ชัดเจนสำหรับสถานการณ์การอ่านบัฟเฟอร์การใช้เป็นค่าเริ่มต้นกับอินเทอร์เฟซทั้งหมดไม่ใช่สิ่งที่ฉันแนะนำ
Stephen Cleary

1
ขวาไม่ว่าจะเป็นTaskหรือValueTaskสามารถใช้เป็นประเภทส่งคืนแบบซิงโครนัส (พร้อมTask.FromResult) แต่ยังมีคุณค่า (heh) ในValueTaskถ้าคุณมีบางสิ่งที่คุณคาดว่าจะซิงโครนัส ReadByteAsyncเป็นตัวอย่างคลาสสิก ฉันเชื่อว่า ValueTaskถูกสร้างขึ้นเป็นหลักสำหรับ "แชนเนล" ใหม่ (สตรีมไบต์ระดับต่ำ) ซึ่งอาจใช้ในคอร์ ASP.NET ที่ประสิทธิภาพเป็นสิ่งสำคัญ
Stephen Cleary

1
โอ้ฉันรู้ว่าฮ่า ๆ เพียงแค่สงสัยว่าถ้าคุณมีสิ่งที่จะเพิ่มความคิดเห็นเฉพาะที่;)
julealgon

2
ไม่ประชาสัมพันธ์นี้สลับสมดุลมากกว่าที่จะเลือก ValueTask? (อ้างอิง: blog.marcgravell.com/2019/08/ … )
จ็วต

2
@stuartd: ตอนนี้ฉันยังคงแนะนำให้ใช้Task<T>เป็นค่าเริ่มต้น นี่เป็นเพียงเพราะนักพัฒนาส่วนใหญ่ไม่คุ้นเคยกับข้อ จำกัดValueTask<T>(โดยเฉพาะกฎ "กินครั้งเดียวเท่านั้น" และกฎ "ไม่ปิดกั้น") ที่กล่าวว่าหาก devs ทั้งหมดในทีมของคุณดีด้วยValueTask<T>ฉันจะแนะนำแนวทางระดับทีมที่ValueTask<T>ต้องการ
Stephen Cleary

104

อย่างไรก็ตามฉันยังไม่เข้าใจว่าปัญหาคืออะไรกับการใช้ ValueTask เสมอ

ประเภทโครงสร้างไม่ฟรี การคัดลอก struct ที่มีขนาดใหญ่กว่าขนาดของการอ้างอิงอาจช้ากว่าการคัดลอกการอ้างอิง การจัดเก็บโครงสร้างที่มีขนาดใหญ่กว่าการอ้างอิงจะใช้หน่วยความจำมากกว่าการจัดเก็บการอ้างอิง โครงสร้างที่มีขนาดใหญ่กว่า 64 บิตอาจไม่สามารถลงทะเบียนเมื่อการอ้างอิงสามารถลงทะเบียนได้ ประโยชน์ของแรงกดดันในการเก็บสะสมที่ต่ำกว่าอาจไม่เกินต้นทุน

ปัญหาเกี่ยวกับประสิทธิภาพควรได้รับการติดต่อกับสาขาวิชาวิศวกรรม ทำเป้าหมายวัดความคืบหน้าของคุณกับเป้าหมายแล้วตัดสินใจว่าจะแก้ไขโปรแกรมอย่างไรหากไม่บรรลุเป้าหมายวัดไปพร้อมกันเพื่อให้แน่ใจว่าการเปลี่ยนแปลงของคุณนั้นดีขึ้นจริง

ทำไม async / await ไม่ได้ถูกสร้างขึ้นด้วยประเภทค่าตั้งแต่เริ่มต้น

awaitถูกเพิ่มใน C # นานหลังจากที่มีTask<T>ประเภทอยู่ มันจะค่อนข้างผิดปกติในการคิดค้นรูปแบบใหม่เมื่อมีอยู่แล้ว และawaitผ่านการทำซ้ำการออกแบบที่ยอดเยี่ยมมากมายก่อนที่จะทำการส่งมอบในปี 2012 ความสมบูรณ์แบบคือศัตรูของความดี ดีกว่าที่จะจัดส่งโซลูชันที่ทำงานได้ดีกับโครงสร้างพื้นฐานที่มีอยู่แล้วหากมีความต้องการของผู้ใช้ให้ปรับปรุงในภายหลัง

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

บางคนสามารถอธิบายได้ว่า ValueTask ทำงานล้มเหลวเมื่อใด

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


23

ValueTask<T>ไม่ได้เป็นส่วนหนึ่งของTask<T>มันเป็นซูเปอร์

ValueTask<T>เป็นสหภาพที่แยกออกจากกันของ T และ a Task<T>ทำให้ปราศจากการจัดสรรสำหรับReadAsync<T>การคืนค่า T ที่มีให้พร้อมกัน (ตรงกันข้ามกับการใช้Task.FromResult<T>ซึ่งจำเป็นต้องจัดสรรTask<T>อินสแตนซ์) ValueTask<T>เป็น awaitable Task<T>เพื่อการบริโภคส่วนใหญ่ของกรณีจะแตกต่างจากที่มี

ValueTask เป็นโครงสร้างช่วยให้สามารถเขียนวิธีการ async ที่ไม่จัดสรรหน่วยความจำเมื่อพวกเขาทำงานพร้อมกันโดยไม่สูญเสียความสอดคล้องของ API ลองนึกภาพว่ามีส่วนต่อประสานกับวิธีการส่งคืนงาน แต่ละคลาสที่ใช้อินเทอร์เฟซนี้จะต้องส่งคืนภารกิจแม้ว่าจะเกิดการเรียกใช้งานพร้อมกัน (หวังว่าจะใช้ Task.FromResult) แน่นอนคุณสามารถมี 2 วิธีที่แตกต่างกันบนอินเทอร์เฟซหนึ่งแบบซิงโครนัสและแบบอะซิงโครนัส แต่สิ่งนี้ต้องการการใช้งานที่แตกต่างกัน 2 แบบเพื่อหลีกเลี่ยง "ซิงค์ผ่าน async" และ "

ดังนั้นให้คุณเขียนวิธีหนึ่งที่ไม่ตรงกันหรือแบบซิงโครนัสแทนที่จะเขียนวิธีใดวิธีหนึ่งที่เหมือนกันสำหรับแต่ละวิธี คุณสามารถใช้งานได้ทุกที่Task<T>แต่มักจะไม่เพิ่มอะไรเลย

มันเพิ่มสิ่งหนึ่ง: เพิ่มสัญญาโดยนัยแก่ผู้เรียกว่าวิธีการใช้ฟังก์ชันการทำงานเพิ่มเติมที่ValueTask<T>ให้ไว้จริง ๆ ฉันเองชอบเลือกพารามิเตอร์และประเภทกลับที่บอกผู้โทรให้มากที่สุด อย่าส่งคืนIList<T>หากการแจงนับไม่สามารถนับได้ อย่าส่งคืนIEnumerable<T>ถ้าทำได้ ผู้บริโภคของคุณไม่จำเป็นต้องค้นหาเอกสารใด ๆ เพื่อทราบว่าวิธีการใดของคุณที่สามารถเรียกได้ว่าเหมาะสมและไม่สามารถทำได้

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

นี่คือสิ่งที่สำคัญสำหรับการพิมพ์ที่แข็งแกร่ง

หากโปรแกรมเมอร์บางคนออกแบบวิธีการซิงค์ในร้านค้าของคุณไม่สามารถตัดสินใจอย่างชาญฉลาดอาจเป็นประโยชน์ในการกำหนดพี่เลี้ยงอาวุโสให้กับโปรแกรมเมอร์ที่มีประสบการณ์น้อยแต่ละคนและมีการทบทวนรหัสประจำสัปดาห์ หากพวกเขาเดาผิดให้อธิบายว่าทำไมมันควรทำแตกต่างกัน มันเป็นค่าใช้จ่ายสำหรับคนอาวุโส แต่มันจะทำให้จูเนียร์เร็วขึ้นเร็วกว่าการโยนพวกเขาลงไปในก้นบึ้งและให้กฎเกณฑ์แก่พวกเขาตามอำเภอใจ

ถ้าคนที่เขียนวิธีการนั้นไม่รู้ว่าสามารถเรียกมันแบบซิงโครนัสได้ใครในโลกบ้าง!

หากคุณมีโปรแกรมเมอร์ที่ไม่มีประสบการณ์หลายคนที่เขียนวิธีการแบบอะซิงก์คนเหล่านั้นก็เรียกพวกเขาด้วยเช่นกัน? พวกเขามีคุณสมบัติที่จะคิดออกเองว่าตัวไหนปลอดภัยที่จะเรียก async หรือพวกเขาจะเริ่มใช้กฎเกณฑ์ที่คล้ายคลึงกันกับวิธีที่พวกเขาเรียกสิ่งเหล่านี้หรือไม่?

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


4
ถ้าฉันเห็นวิธีการส่งคืนValueTask<T>ฉันคิดว่าคนที่เขียนวิธีการทำเช่นนั้นเพราะวิธีการใช้ฟังก์ชั่นที่ValueTask<T>เพิ่ม ฉันไม่เข้าใจว่าทำไมคุณคิดว่ามันเป็นที่พึงปรารถนาสำหรับวิธีการทั้งหมดของคุณที่จะมีผลตอบแทนประเภทเดียวกัน เป้าหมายคืออะไร
15ee8f99-57ff-4f92-890c-b56153

2
เป้าหมาย 1 คืออนุญาตให้มีการเปลี่ยนแปลงการใช้งาน จะทำอย่างไรถ้าฉันไม่ได้แคชผลลัพธ์ตอนนี้ แต่เพิ่มรหัสแคชในภายหลัง เป้าหมาย 2 จะเป็นแนวทางที่ชัดเจนสำหรับทีมที่สมาชิกทุกคนไม่เข้าใจเมื่อ ValueTask มีประโยชน์ เพียงแค่ทำมันเสมอหากไม่มีอันตรายในการใช้งาน
Stilgar

6
ในมุมมองของฉันที่เกือบจะเหมือนมีวิธีการของคุณกลับมาobjectเพราะบางทีสักวันหนึ่งคุณจะต้องการให้พวกเขากลับมาแทนint string
15ee8f99-57ff-4f92-890c-b56153

3
อาจ แต่ถ้าคุณถามคำถาม "ทำไมคุณถึงส่งคืนสตริงแทนวัตถุ" ฉันสามารถตอบได้อย่างง่ายดายโดยชี้ไปที่การขาดความปลอดภัยประเภท เหตุผลที่ไม่ใช้ ValueTask ทุกที่ดูเหมือนจะละเอียดกว่า
Stilgar

3
@Stilgar สิ่งหนึ่งที่ฉันจะพูด: ปัญหาที่ลึกซึ้งควรกังวลคุณมากกว่าที่เห็นได้ชัด
15ee8f99-57ff-4f92-890c-b56153

20

มีบางคือการเปลี่ยนแปลงในสุทธิ 2.1 เริ่มต้นจาก. net core 2.1 ValueTask ไม่เพียง แต่แสดงถึงการกระทำที่เสร็จสมบูรณ์แบบซิงโครนัสเท่านั้น นอกจากนี้เราได้รับไม่ใช่ทั่วไปValueTaskประเภท

ฉันจะแสดงความคิดเห็น Stephen Toub ที่เกี่ยวข้องกับคำถามของคุณ:

เรายังคงต้องทำตามแนวทางที่เป็นทางการ แต่ฉันหวังว่ามันจะเป็นอย่างนี้สำหรับพื้นที่ผิวสาธารณะ API:

  • งานให้การใช้งานมากที่สุด

  • ValueTask มีตัวเลือกมากที่สุดสำหรับการเพิ่มประสิทธิภาพ

  • หากคุณกำลังเขียนวิธีการอินเตอร์เฟซ / เสมือนที่คนอื่นจะแทนที่ ValueTask เป็นตัวเลือกเริ่มต้นที่ถูกต้อง

  • หากคุณคาดว่าจะใช้ API บนเส้นทางที่ร้อนแรงซึ่งการจัดสรรจะมีความสำคัญ ValueTask เป็นตัวเลือกที่ดี

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

จากมุมมองการนำไปใช้งานอินสแตนซ์ ValueTask ที่ส่งคืนจำนวนมากจะยังคงได้รับการสนับสนุนจากงาน

คุณสมบัตินี้สามารถใช้ได้ไม่เพียง แต่ใน. net core 2.1 คุณจะสามารถใช้กับแพ็คเกจSystem.Threading.Tasks.Extensions


1
เพิ่มเติมจากสตีเฟ่นวันนี้: blogs.msdn.microsoft.com/dotnet/2018/11/07/…
Mike Cheel
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.