การบัฟเฟอร์ข้อมูลของ 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> ใน .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 ประการดังนี้
 

  • เจ้าของ: โค้ดที่เป็นเจ้าของบัฟเฟอร์มีหน้าที่จัดการช่วงชีวิตของบัฟเฟอร์
  • ผู้ใช้: โค้ดส่วนผู้ใช้ได้รับอนุญาตให้อ่านและเขียนบัฟเฟอร์ได้
  • ช่วงเวลา:  ระยะเวลาที่หน่วยต่าง ๆ สามารถใช้งานบัฟเฟอร์ได้

รูปที่ 1 เป็นโค้ดเสมือน (pseudo-code) สาธิตแนวคิดสำคัญ 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 โค้ดตัวอย่างแสดงแบบจำลองการทำงานของบัฟเฟอร์

โค้ดตัวอย่างแสดงแบบจำลองการทำงานของบัฟเฟอร์

แบบจำลองของบัฟเฟอร์

อย่างที่บอกในหัวข้อก่อนหน้านี้ว่าบัฟเฟอร์ต้องมีเจ้าของ ในดอตเน็ตคอร์สนับสนุนแบบจำลองแสดงความเป็นเจ้าขอบสองแบบ ได้แก่
 

  • เจ้าของเดี่ยว: บัฟเฟอร์มีเจ้าของรายเดียวตลอดชีวิต
  • การโอนเจ้าของ: เจ้าของบัฟเฟอร์สามารถโอนความเป็นเจ้าของไปให้หน่วยอื่นได้

เราอาจใช้อินเทอร์เฟส System.Buffers.IMemoryOwner<T> เพื่อจัดการความเป็นเจ้าของของบัฟเฟอร์ได้ โดย IMemoryOwner<T> สนับสนุนแบบจำลองทั้งสองแบบ
รูปที่ 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

แบบจำลองของบัฟเฟอร์ใช้ 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 อ่านข้อมูลจากบัฟเฟอร์

รูปที่ 5 แสดงตัวอย่างโค้ดระยะเวลาการใช้งานบัฟเฟอร์

แสดงตัวอย่างโค้ดระยะเวลาการใช้งานบัฟเฟอร์

ระยะเวลาใช้บัฟเฟอร์

ถ้า 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 อาจใช้งานบัฟเฟอร์อีกหลังจากจบการทำงานแล้วที่จะมีผลทำให้ข้อมูลผิด

รูปที่ 6 โค้ดแสดง method  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 ตัวบน แต่ย้ายโค้ดการคัดลอกข้อมูลจากบัฟเฟอร์ไปไว้ภายในอีกทากส์หนึ่ง

โดยสรุปบทความนี้ได้อธิบายเรื่องเจ้าของ ผู้ใช้ และช่วงเวลา, แบบจำลองของบัฟเฟอร์ , แบบจำลองของบัฟเฟอร์ใช้ Using บัฟเฟอร์ไม่ระบุเจ้าของ , ระยะเวลาใช้บัฟเฟอร์ และ method  Log แบบต่าง ๆ ซึ่งแสดงข้อได้เปรียบหรือจุดเด่นของภาษา C# ที่ไม่พบในภาษาอื่น ๆ บางภาษา