ValueTask และการหลีกเลี่ยงการจัดสรรหน่วยความจำ
ใช้ ValueTask เพื่อลดการจัดสรรบน heap ในเส้นทางการทำงานที่ใช้บ่อย ทำความเข้าใจข้อจำกัด และหลีกเลี่ยงการใช้งานผิดรูปแบบที่พบบ่อย
ValueTask และการหลีกเลี่ยงการจัดสรรหน่วยความจำ เป็นบทเรียน C# Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน C# Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส C# Academy มีบทเรียนทั้งหมด 4 บทเรียน
ปัญหาการจัดสรรหน่วยความจำของ Task
Task<T> เป็นชนิดอ้างอิง — เมธอด async ทุกเมธอดจะจัดสรร Task บนฮีป แม้ผลลัพธ์จะพร้อมใช้งานแบบพร้อมกันก็ตาม (เช่น ผลลัพธ์จากแคช) ในเส้นทางการทำงานที่ถูกเรียกบ่อย สิ่งนี้อาจเพิ่มภาระให้ GC อย่างมาก ValueTask<T> ช่วยแก้ปัญหานี้
ValueTask คืออะไร
ValueTask<T> เป็น struct ที่แทนได้ทั้งผลลัพธ์ที่พร้อมใช้งานแบบพร้อมกันและการดำเนินการแบบอะซิงโครนัสที่ยังรออยู่ เมื่อผลลัพธ์พร้อมใช้งานแบบพร้อมกัน ก็ไม่จำเป็นต้องจัดสรรหน่วยความจำบนฮีป
// Task<T>: always allocates, even when result is cached
public async Task<string> GetCached_Task() =>
_cache.TryGetValue("key", out var v) ? v : await FetchAsync();
// ValueTask<T>: zero allocation when value is cached
public ValueTask<string> GetCached_ValueTask() =>
_cache.TryGetValue("key", out var v)
? ValueTask.FromResult(v) // no allocation
: new ValueTask<string>(FetchAsync()); // allocation only when neededการส่งคืน ValueTask จากอินเทอร์เฟซ
ใช้ ValueTask<T> ในเมธอดของอินเทอร์เฟซที่ถูกเรียกบ่อยและมักทำงานเสร็จแบบพร้อมกัน ผู้ใช้สามารถใช้งานได้เหมือนกับ Task<T> ทุกประการ
public interface IProductRepository
{
// ValueTask: often completes synchronously from cache
ValueTask<Product?> GetByIdAsync(int id);
// Task: always async (DB query)
Task<List<Product>> SearchAsync(string query);
}
// Consumption is identical to Task:
var product = await _repo.GetByIdAsync(42);
var results = await _repo.SearchAsync("keyboard");ValueTask กับ IValueTaskSource
หากต้องการประสิทธิภาพสูงสุด ให้ใช้งาน IValueTaskSource<T> เพื่อจัดเก็บอ็อบเจ็กต์สถานะเบื้องหลังไว้ในพูล วิธีนี้หลีกเลี่ยงการจัดสรรหน่วยความจำทั้งหมดได้ แม้การดำเนินการแบบอะซิงโครนัสจะเสร็จสิ้น โดย .NET ใช้แนวทางนี้ภายในโค้ดซ็อกเก็ตและการรับส่งข้อมูล
// In most cases, use the simple ValueTask approach.
// IValueTaskSource is for library authors needing zero-alloc async at extreme scale.
// Example: System.IO.Pipelines uses this internally
var result = await pipe.Reader.ReadAsync(ct);
// ReadAsync returns ValueTask<ReadResult> backed by IValueTaskSource
// -- zero allocation for every read even under high throughputข้อจำกัดของ ValueTask: รอได้เพียงครั้งเดียว
กฎสำคัญ: ต้องรอ ValueTask<T> ได้ไม่เกินหนึ่งครั้ง และห้ามเรียก .Result ก่อนที่การดำเนินการจะเสร็จสิ้น การฝ่าฝืนกฎนี้ทำให้เกิดพฤติกรรมที่ไม่ได้กำหนด
var vt = repo.GetByIdAsync(42);
// BAD: await multiple times
var a = await vt; // OK
var b = await vt; // UNDEFINED BEHAVIOR — don't do this!
// BAD: two concurrent awaits
var t1 = vt.AsTask(); // converts to Task (safe to share)
var t2 = vt.AsTask(); // second AsTask() on same ValueTask — WRONG
// GOOD: if you need multiple awaits, convert to Task
var task = repo.GetByIdAsync(42).AsTask();
var r1 = await task;
var r2 = await task; // OK — Task can be awaited multiple timesแนวทางเลือก ValueTask<T> เทียบกับ Task<T>
ใช้ ValueTask<T> เฉพาะเมื่อการวัดประสิทธิภาพแสดงให้เห็นว่าเมธอดแบบอะซิงโครนัสสร้างแรงกดดันจากการจัดสรรหน่วยความจำ อย่าใช้กับทุกกรณี เพราะจะเพิ่มความซับซ้อนโดยไม่เกิดประโยชน์กับโค้ดที่ทำงานไม่บ่อย
// USE ValueTask when:
// 1. The method often completes synchronously (cache hit)
// 2. The method is on a hot path called millions of times/sec
// 3. You're writing a library with high-throughput I/O
// USE Task when:
// 1. The result is always async
// 2. The method might be awaited multiple times
// 3. You use Task.WhenAll / Task.WhenAny
// 4. You store the task in a variable and share itValueTask ที่ไม่ใช่แบบทั่วไป
ValueTask (แบบไม่ทั่วไป) ใช้กับเมธอดที่ส่งคืน Task (อะซิงโครนัสที่ไม่มีค่าผลลัพธ์) ใช้กับเมธอดที่ถูกเรียกบ่อยและมักทำงานเสร็จแบบพร้อมกัน
public interface ICache
{
// Often a no-op when value exists — ValueTask avoids allocation
ValueTask SetAsync(string key, string value);
ValueTask InvalidateAsync(string key);
}
public class InMemoryCache : ICache
{
private readonly Dictionary<string, string> _data = new();
public ValueTask SetAsync(string key, string value)
{
_data[key] = value;
return ValueTask.CompletedTask; // no allocation
}
}การวัดการจัดสรรหน่วยความจำด้วย BenchmarkDotNet
ใช้ [MemoryDiagnoser] ใน BenchmarkDotNet เพื่อวัดการจัดสรรหน่วยความจำต่อการดำเนินการ วิธีนี้เป็นวิธีเดียวที่เชื่อถือได้ในการตรวจสอบว่า ValueTask ช่วยได้จริง
[MemoryDiagnoser]
public class CacheBenchmark
{
private readonly CachedRepo _repo = new();
[Benchmark]
public async Task<Product?> Task_GetById()
=> await _repo.GetByIdTask(1);
[Benchmark]
public async ValueTask<Product?> ValueTask_GetById()
=> await _repo.GetByIdValueTask(1);
}
// Run: dotnet run -c Release
// Compare Gen0 allocations column — ValueTask should show 0B on cache hitValueTask ใน SocketsHttpHandler
สแตก HTTP ของ .NET ใช้ ValueTask อย่างกว้างขวางภายใน การทำความเข้าใจเรื่องนี้ช่วยให้คุณเขียนไคลเอ็นต์ HTTP ประสิทธิภาพสูงที่สอดคล้องกับกลยุทธ์การจัดสรรหน่วยความจำของรันไทม์
// HttpClient uses ValueTask-based sockets internally.
// When using HttpClient.GetAsync(), the response is awaited with Task<HttpResponseMessage>.
// For streaming large bodies, use:
using var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
await using var stream = await response.Content.ReadAsStreamAsync();
// Stream reads internally use ValueTask<int> for zero-allocation I/Oการใช้งานจริง: บริการแคชในเส้นทางการทำงานที่ถูกเรียกบ่อย
บริการแคชที่ทุกการเรียกอาจเป็นการพบข้อมูลในแคช — ValueTaskช่วยขจัดการจัดสรรหน่วยความจำในเส้นทางการทำงานที่ถูกเรียกบ่อย ขณะเดียวกันก็ยังรองรับการทำงานแบบอะซิงโครนัสสำรองเมื่อไม่พบข้อมูลในแคช
public class ProductCache
{
private readonly Dictionary<int, Product> _cache = new();
private readonly IProductRepository _repo;
public ProductCache(IProductRepository repo) => _repo = repo;
public ValueTask<Product?> GetAsync(int id)
{
// Cache hit: no allocation
if (_cache.TryGetValue(id, out var cached))
return ValueTask.FromResult<Product?>(cached);
// Cache miss: async fetch
return new ValueTask<Product?>(FetchAndCacheAsync(id));
}
private async Task<Product?> FetchAndCacheAsync(int id)
{
var product = await _repo.GetByIdAsync(id);
if (product is not null) _cache[id] = product;
return product;
}
}ตรวจสอบความเข้าใจ
กฎสำคัญเกี่ยวกับการรอ ValueTask<T> คืออะไร
สรุป: ValueTask และการหลีกเลี่ยงการจัดสรรหน่วยความจำ
ประเด็นสำคัญ:
ValueTask<T>: struct ที่หลีกเลี่ยงการจัดสรรหน่วยความจำบนฮีปเมื่อผลลัพธ์พร้อมใช้งานแบบพร้อมกัน- เหมาะที่สุดสำหรับเส้นทางการทำงานที่ถูกเรียกบ่อยและมักทำงานเสร็จแบบพร้อมกัน (เช่น พบข้อมูลในแคช)
- ห้ามรอ
ValueTask<T>มากกว่าหนึ่งครั้ง — ใช้.AsTask()เพื่อแชร์ผลลัพธ์ - ใช้
[MemoryDiagnoser]เพื่อตรวจสอบการประหยัดการจัดสรรหน่วยความจำก่อนปรับปรุงประสิทธิภาพ - เลือกใช้
Task<T>สำหรับการใช้งานทั่วไป และใช้ValueTask<T>เฉพาะเมื่อผลการวัดประสิทธิภาพสนับสนุนเท่านั้น
เรียนรู้ C# ด้วย AI tutor — ฟรี
เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป
- คอร์ส
- 93
- บทเรียน
- 346
คำถามที่พบบ่อย
บทเรียน “ValueTask และการหลีกเลี่ยงการจัดสรรหน่วยความจำ” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “ValueTask และการหลีกเลี่ยงการจัดสรรหน่วยความจำ” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส C# Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส C# Academy มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “ValueTask และการหลีกเลี่ยงการจัดสรรหน่วยความจำ”
ใช้ ValueTask เพื่อลดการจัดสรรบน heap ในเส้นทางการทำงานที่ใช้บ่อย ทำความเข้าใจข้อจำกัด และหลีกเลี่ยงการใช้งานผิดรูปแบบที่พบบ่อย คุณปฏิบัติ C# Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน C# Academy หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน C# Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน
บทเรียน “ValueTask และการหลีกเลี่ยงการจัดสรรหน่วยความจำ” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน C# Academy นี้ได้ไหม
ได้ บทเรียน C# Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- IAsyncEnumerable และ await foreach
- System.Threading.Channels
- ValueTask และการหลีกเลี่ยงการจัดสรรหน่วยความจำ
- ConfigureAwait และบริบทการซิงโครไนซ์