C# Academy · บทเรียน

ValueTask และการหลีกเลี่ยงการจัดสรรหน่วยความจำ

ใช้ ValueTask เพื่อลดการจัดสรรบน heap ในเส้นทางการทำงานที่ใช้บ่อย ทำความเข้าใจข้อจำกัด และหลีกเลี่ยงการใช้งานผิดรูปแบบที่พบบ่อย

บทเรียน 3 จาก 412 ขั้นตอน

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 it

ValueTask ที่ไม่ใช่แบบทั่วไป

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 hit

ValueTask ใน 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. IAsyncEnumerable และ await foreach
  2. System.Threading.Channels
  3. ValueTask และการหลีกเลี่ยงการจัดสรรหน่วยความจำ
  4. ConfigureAwait และบริบทการซิงโครไนซ์
← กลับไปที่ C# Academy