การบัฟเฟอร์ข้อมูลของ Memory<T> และ Span<T> ใน .NET Core 3 และ C# 8

การบัฟเฟอร์ข้อมูลของ Memory<T> และ Span<T> ใน .NET Core 3 และ C# 8
ในบทความ มีอะไรใหม่ใน .NET Core 3 และ C# 8 : Stackalloc ซ้อนนิพจน์ ได้พูดเรื่อง stackalloc ที่เริ่มตั้งแต่ C#8 และ .NET Core 3.0
ถ้าผลลัพธ์ของนิพจน์ stackalloc มีชนิดข้อมูลเป็นแบบ System.Span<T> หรือ System.ReadOnlySpan<T> เราสามารถใส่นิพจน์ stackalloc ซ้อนไว้ภายในนิพจน์อื่น ๆ ได้
สำหรับบทความนี้จะกล่าวถึง ลักษณะการบัฟเฟอร์ข้อมูลของ Memory<T> และ Span<T> ว่ามีข้อควรพิจารณาในการใช้งานอย่างไร
ทั้ง Memory<T> และ Span<T> ล้วนเป็นบัฟเฟอร์ (buffer หน่วยสำหรับเก็บพักข้อมูล) ของโครงสร้างข้อมูลที่สามารถนำไปใช้ในท่อส่งข้อมูล (pipeline) ได้
เพราะมันถูกออกแบบมาให้สามารถส่งผ่านข้อมูลไปยังหน่วยต่าง ๆ ที่เชื่อมต่อกันด้วยท่อส่งข้อมูลได้อย่างมีประสิทธิภาพ
หน่วยต่าง ๆ อาจประมวลผลหรือเปลี่ยนแปลงแก้ไขบัฟเฟอร์ได้ เนื่องจาก Memory<T> และไทป์อื่น ๆ ที่เกี่ยวข้องสามารถเข้าถึงหน่วยต่าง ๆ หรือเทรดต่าง ๆ หลายอันได้พร้อม ๆ กัน
คนโค้ดจึงจำเป็นต้องปฏิบัติตามข้อแนะนำการใช้งานบัฟเฟอร์เพื่อให้โค้ดมีความคงทน
รูปที่ 1 โค้ดเสมือนแสดงแนวคิด เจ้าของ ผู้ใช้ และช่วงเวลา
เจ้าของ ผู้ใช้ และช่วงเวลา
การใช้งานบัฟเฟอร์มีแนวคิดสำคัญ 3 ประการดังนี้- เจ้าของ: โค้ดที่เป็นเจ้าของบัฟเฟอร์มีหน้าที่จัดการช่วงชีวิตของบัฟเฟอร์
- ผู้ใช้: โค้ดส่วนผู้ใช้ได้รับอนุญาตให้อ่านและเขียนบัฟเฟอร์ได้
- ช่วงเวลา: ระยะเวลาที่หน่วยต่าง ๆ สามารถใช้งานบัฟเฟอร์ได้
ต่อไปนี้เป็นคำอธิบายโค้ด
- บรรทัดที่ 8 นิยาม method WriteInt32ToBuffer ทำหน้าที่เขียนค่าในรูปแบบที่มนุษย์สามารถอ่านเข้าใจได้ไปยังบัฟเฟอร์เอาต์พุต
- บรรทัดที่ 11 นิยาม method DisplayBufferToConsole ทำหน้าที่แสดงสิ่งที่อยู่ภายในบัฟเฟอร์ที่คอนโซล
- บรรทัดที่ 16 สร้างออพเจ็กต์บัฟเฟอร์แบบ Memory<T> โดยมีชนิดข้อมูลเป็น Char
- บรรทัดที่ 19 รับข้อมูลตัวเลขจากแป้นพิมพ์ที่ผู้ใช้ป้อน
- บรรทัดที่ 20 เรียก method WriteInt32ToBuffer เพื่อเขียนค่าไปยังบัฟเฟอร์เอาต์พุต method นี้มีภาวะเป็น “ผู้ใช้” และมี “ช่วงเวลา” ที่ใช้งานบัฟเฟอร์ได้ระหว่างที่ method เริ่มทำงานไปจนออกจาก method
- บรรทัดที่ 21 เรียก method DisplayBufferToConsole เพื่อแสดงสิ่งที่อยู่ภายในบัฟเฟอร์ที่คอนโซล method นี้มีภาวะเป็น “ผู้ใช้” ด้วยเช่นกัน และมี “ช่วงเวลา” ที่ใช้งานบัฟเฟอร์ได้ระหว่างที่ method เริ่มทำงานไปจนออกจาก method เช่นเดียวกัน
- บรรทัดที่ 25 ในโค้ดเสมือนนี้ “เจ้าของ” คือ method Main เพราะมันเป็นผู้สร้างบัฟเฟอร์ มันจึงมีหน้าที่ต้องทำลายบัฟเฟอร์ซึ่งทำได้โดยการเรียก method Destroy()
แบบจำลองของบัฟเฟอร์
อย่างที่บอกในหัวข้อก่อนหน้านี้ว่าบัฟเฟอร์ต้องมีเจ้าของ ในดอตเน็ตคอร์สนับสนุนแบบจำลองแสดงความเป็นเจ้าขอบสองแบบ ได้แก่- เจ้าของเดี่ยว: บัฟเฟอร์มีเจ้าของรายเดียวตลอดชีวิต
- การโอนเจ้าของ: เจ้าของบัฟเฟอร์สามารถโอนความเป็นเจ้าของไปให้หน่วยอื่นได้
รูปที่ 2 คือโค้ดแสดงตัวอย่างการใช้ IMemoryOwner<T> เพื่อกำหนดความเป็นเจ้าของของบัฟเฟอร์
- บรรทัดที่ 10-11 สร้างออพเจ็กต์เจ้าของบัฟเฟอร์ไทป์ Char จากคลาส MemoryPool
- บรรทัดที่ 12 แสดงข้อความบอกให้ผู้ใช้ป้อนตัวเลข
- บรรทัดที่ 15 รับข้อมูลจากผุ้ใช้
- บรรทัดที่ 16 สร้างออพเจ็กต์อ้างอิงบัฟเฟอร์จากเจ้าของบัฟเฟอร์
- บรรทัดที่ 17 บันทึกสิ่งที่ผู้ใช้ป้อนเข้าไปยังบัฟเฟอร์
- บรรทัดที่ 18-19 แสดงสิ่งที่ผู้ใช้ป้อนโดยอ่านจากบัฟเฟอร์
- บรรทัดที่ 21-24 ดักความผิดพลาดกรณีผู้ใช้ป้อนพิมพ์ปนตัวอักษร
- บรรทัดที่ 27-30 ดักความผิดพลาดกรณีผู้ใช้ป้อนจำนวนน้อยหรือมากเกินรับไหว
- บรรทัดที่ 33 ทำลายบัฟเฟอร์หลังการใช้งาน
- บรรทัดที่ 36-42 method ทำหน้าที่บันทึกข้อมูลลงบัฟเฟอร์
- บรรทัดที่ 43,44 method ทำหน้าที่อ่านข้อมูลจากบัฟเฟอร์มาแสดง
Enter a number: 123
Contents of the buffer: '123'
รูปที่ 3 โค้ดตัวอย่างแสดงแบบจำลองการทำงานของบัฟเฟอร์ใช้ using
แบบจำลองของบัฟเฟอร์ใช้ Using
ตัวอย่างโค้ดในรูปที่ 3 ให้ผลลัพธ์การทำงานเหมือนตัวอย่างโค้ดในรูปที่ 2 ทุกประการ แตกต่างกันที่โค้ดเพียงเล็กน้อยโดยโค้ดในรูปที่ 3 ใช้คำสั่ง using (ดูบรรทัดที่ 8) ซึ่งมีข้อดีที่จะทำลายบัฟเฟอร์โดยอัตโนมัติเมื่อโค้ดหลุดออกจากบล็อก (หลังบรรทัดที่ 28)
โปรดสังเกตว่าในโค้ดตัวอย่างทั้งสอง method Main คือผู้เก็บตัวอ้างอิงไปยัง IMemoryOwner<T>
ดังนั้น Main จึงมีภาวะเป็นเจ้าของบัฟเฟอร์ ส่วน method WriteInt32ToBuffer และ method DisplayBufferToConsole รับ Memory<T> มาในลักษณะ API สาธารณะ
นั่นหมายถึง ทั้ง 2 method จึงมีภาวะเป็นผุ้ใช้ และไม่ได้รับพร้อม ๆ กัน แต่รับทีละตัว และเนื่องจาก method DisplayBufferToConsole อ่านบัฟเฟอร์เท่านั้น เราจึงอาจผ่าน API เป็น ReadOnlyMemory<T> แทนก็ได้
รูปที่ 4 โค้ดตัวอย่างแสดงแบบจำลองการทำงานของบัฟเฟอร์แบบไม่ระบุเจ้าของ
บัฟเฟอร์ไม่ระบุเจ้าของ
เราอาจสร้างบัฟเฟอร์ Memory<T> โดยไม่ต้องใช้อินเทอร์เฟส IMemoryOwner<T> ก็ได้ ในกรณีนี้เราจะได้บัฟเฟอร์ที่ไม่ได้แสดงว่าใครเป็นเจ้าของและจะโอนความเป็นเจ้าของไม่ได้ โค้ดตัวอย่างในรูปที่ 4 แสดงการสร้างบัฟเฟอร์ด้วย Memory<T> ที่จะได้บัฟเฟอร์แบบไร้เจ้าของ ต่อไปนี้เป็นคำอธิบายโค้ด- บรรทัด 7 สร้างบัฟเฟอร์แบบ Char เป็นอาร์เรย์ขนาด 65 ไบต์ โดยใช้คลาส Memory<T> ไม่ได้ใช้อินเทอร์เฟส IMemoryOwner<T> ทำให้ตัวเก็บขยะทำหน้าที่เป็นเจ้าของบัฟเฟอร์ method ต่าง ๆสามารถเรียกใช้บัฟเฟอร์ได้ ตอนจบไม่ต้องมีโค้ดทำลายบัฟเฟอร์เพราะตัวเก็บขยะจะจัดการเอง
- บรรทัด 10 รับการป้อนพิมพ์จากผู้ใช้
- บรรทัด 12 บันทึกข้อมูลลงบัฟเฟอร์
- บรรทัด 13 อ่านข้อมูลจากบัฟเฟอร์
ระยะเวลาใช้บัฟเฟอร์
ถ้า method รับค่าเป็นบัฟเฟอร์แบบ Memory<T> และมีค่าส่งกลับเป็น void method นั้นจะต้องไม่ใช้งานบัฟเฟอร์นั้นอีกหลังจาก method จบการทำงานแล้วรูปที่ 5 แสดงตัวอย่างโค้ดที่เรียก method Log ในการวนการทำงานที่มีจำนวนครั้งในการวนขึ้นอยู่กับสิ่งที่ผู้ใช้งานป้อนเข้ามา
ต่อไปนี้เป็นคำอธิบายโค้ด
- บรรทัดที่ 7 โมดิไฟเออร์ extern ทำหน้าระบุว่านิยามของ method Log อยู่ภายนอกแอสเซมบลีนี้ และเราจะเรียกใช้งานมันภายในซอร์สไฟล์นี้
- บรรทัดที่ 12 สร้างตัวอ้างอิงเจ้าของบัฟเฟอร์
- บรรทัดที่ 14 สร้างบัฟเฟอร์ ประกาศตัวแปรเพื่ออ้างอิงบัฟเฟอร์
- บรรทัดที่ 15 ประกาศตัวแปรอ้างอิง span ของบัฟเฟอร์
- บรรทัดที่ 16-23 วนการทำงาน ระหว่างที่วนจะรอรับค่าจากผู้ใช้ จะออกจากลูปเมื่อผู้ใช้ป้อนศูนย์
- บรรทัดที่ 23 เรียก method Log ให้ทำงานร่วมกับบัฟเฟอร์ ถ้า Log เป็น method ที่ทำงานแบบผสานจังหวะโค้ดนี้จะทำงานได้ตามที่คาดไว้ แต่ถ้าไม่ Log อาจใช้งานบัฟเฟอร์อีกหลังจากจบการทำงานแล้วที่จะมีผลทำให้ข้อมูลผิด
method Log แบบต่าง ๆ
ถ้าเขียนนิยาม method Log ไม่ดีจะละเมิดช่วงเวลาของการใช้งานบัฟเฟอร์ที่จะมีผลทำให้ข้อมูลในบัฟเฟอร์เสียรูปที่ 6 แสดงตัวอย่างนิยาม method Log ทั้งแบบที่ใช้ได้และใช้ไม่ได้
ต่อไปนี้เป็นคำอธิบายโค้ด
- บรรทัดที่ 13-20 นิยาม method Log แบบนี้ใช้ไม่ได้เพราะมันวิ่งงานในพื้นหลังและเราไม่ได้หยุดเธรดหลักขณะที่กำลังติดต่อกับอุปกรณ์ IO
- บรรทัดที่ 21-28 นิยาม method Log แบบนี้ใช้ได้ เพราะส่งค่ากลับเป็นทากส์ ไม่ได้ส่งค่ากลับเป็น void เหมือนอันบน
- บรรทัดที่ 29-37 นิยาม method Log แบบนี้ใช้ได้แม้จะส่งค่ากลับเป็น void เพราะเราไม่ได้บันทึกข้อมูลจากบัฟเฟอร์โดยตรงแต่บันทึกจากสำเนา
- บรรทัดที่ 38-45 นิยาม method Log แบบนี้ใช้ได้แม้จะส่งค่ากลับเป็น void เพราะเราไม่ได้บันทึกข้อมูลจากบัฟเฟอร์โดยตรงแต่บันทึกจากสำเนาเหมือนกันกับ method Log ตัวบน แต่ย้ายโค้ดการคัดลอกข้อมูลจากบัฟเฟอร์ไปไว้ภายในอีกทากส์หนึ่ง