0Pricing
C# Academy · บทเรียน

งานตามช่วงเวลาและตัวจับเวลา

กำหนดเวลางานที่ทำซ้ำด้วย PeriodicTimer และ System.Threading.Timer พร้อมจัดการการยกเลิกอย่างเหมาะสม

งานตามช่วงเวลาและตัวจับเวลา เป็นบทเรียน C# Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน C# Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส C# Academy มีบทเรียนทั้งหมด 4 บทเรียน

การตั้งเวลางานที่ทำซ้ำ

บริการเบื้องหลังจำนวนมากจำเป็นต้องทำงานตามกำหนดเวลาที่แน่นอน เช่น ทุก 5 นาที ทุกชั่วโมง หรือทุกวัน .NET มีกลไกหลายแบบ ได้แก่ PeriodicTimer (สมัยใหม่), System.Threading.Timer (แบบดั้งเดิม) และ Quartz.NET (อิงตาม cron)

PeriodicTimer: ทางเลือกสมัยใหม่

PeriodicTimer (เริ่มมีใน .NET 6) ออกแบบมาให้ใช้ในเมธอด async ตัวจับเวลาจะทำงานตรงตามกำหนดเวลาโดยไม่ใช้ฟังก์ชันเรียกกลับ และผสานการทำงานกับการยกเลิกได้อย่างราบรื่น

public class MetricsCollectorService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));

        while (await timer.WaitForNextTickAsync(ct))
        {
            // Runs every 60 seconds
            await CollectMetricsAsync(ct);
        }
        // Loop exits cleanly when ct is cancelled
    }
}

PeriodicTimer กับลูป Task.Delay

PeriodicTimer ดีกว่าลูป Task.Delay เพราะทำงานเป็นช่วงเวลาคงที่นับจากเวลาเริ่มต้น (ไม่ใช่เวลาเริ่มต้นบวกเวลาประมวลผล) จึงป้องกันการคลาดเคลื่อนที่สะสมในระหว่างการทำงานหลายรอบ

// Task.Delay loop — drifts! (interval = delay + work time)
while (!ct.IsCancellationRequested)
{
    await DoWorkAsync(ct); // takes 2 seconds
    await Task.Delay(5000, ct); // actual period: 7 seconds
}

// PeriodicTimer — no drift
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
while (await timer.WaitForNextTickAsync(ct))
{
    await DoWorkAsync(ct); // period is still 5 seconds from start
}

System.Threading.Timer

System.Threading.Timer เป็นตัวจับเวลาแบบดั้งเดิมของกลุ่มเธรดที่ใช้ฟังก์ชันเรียกกลับ รองรับทั้งโหมดทำงานเป็นคาบและโหมดทำงานเพียงครั้งเดียว โปรดระวัง: ฟังก์ชันเรียกกลับทำงานบนเธรดของกลุ่มเธรดโดยไม่รองรับ async

public class LegacyTimerService : BackgroundService, IDisposable
{
    private Timer? _timer;

    protected override Task ExecuteAsync(CancellationToken ct)
    {
        // Fire immediately then every 10 seconds
        _timer = new Timer(DoWork, null,
            dueTime:  TimeSpan.Zero,
            period:   TimeSpan.FromSeconds(10));
        return Task.CompletedTask;
    }

    private void DoWork(object? state)
    {
        // Runs on thread pool — avoid async void here
        Console.WriteLine($"Timer fired: {DateTime.UtcNow}");
    }

    public override void Dispose() { _timer?.Dispose(); base.Dispose(); }
}

การทำงานล่าช้าเพียงครั้งเดียว

ใช้ PeriodicTimer กับการทำงานเพียงหนึ่งรอบ หรือใช้ CancellationTokenSource.CancelAfter ร่วมกับ Task.Delay สำหรับการทำงานล่าช้าเพียงครั้งเดียว

// Run once after a delay
public async Task ScheduleOnceAsync(TimeSpan delay, Func<Task> action, CancellationToken ct)
{
    await Task.Delay(delay, ct);
    await action();
}

// Or with CancellationToken:
var cts = new CancellationTokenSource(TimeSpan.FromHours(1));
try
{
    await Task.Delay(Timeout.Infinite, cts.Token);
}
catch (OperationCanceledException)
{
    await RunScheduledJobAsync();
}

PeriodicTimer พร้อมการจัดการข้อผิดพลาด

ห่อการทำงานในแต่ละรอบด้วย try/catch เสมอ เพื่อไม่ให้ความล้มเหลวเพียงครั้งเดียวหยุดตัวจับเวลาไม่ให้ทำงานในรอบถัดไป

protected override async Task ExecuteAsync(CancellationToken ct)
{
    using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));

    while (await timer.WaitForNextTickAsync(ct))
    {
        try
        {
            await SyncInventoryAsync(ct);
        }
        catch (Exception ex)
        {
            // Log but don't crash — next tick will retry
            _logger.LogError(ex, "Inventory sync failed");
        }
    }
}

การปรับให้ตรงกับขอบเขตเวลา

หากต้องการให้ทำงานในเวลาที่กำหนดของนาฬิกา (เช่น ทุกต้นชั่วโมง) ให้คำนวณระยะเวลาล่าช้าเริ่มต้นจนถึงขอบเขตเวลาถัดไปก่อนเริ่มตัวจับเวลา

protected override async Task ExecuteAsync(CancellationToken ct)
{
    // Wait until the next full hour
    var now = DateTime.UtcNow;
    var nextHour = now.Date.AddHours(now.Hour + 1);
    var initialDelay = nextHour - now;

    await Task.Delay(initialDelay, ct);

    // Then tick every hour exactly
    using var timer = new PeriodicTimer(TimeSpan.FromHours(1));

    await RunJobAsync(ct); // run at the first tick
    while (await timer.WaitForNextTickAsync(ct))
        await RunJobAsync(ct);
}

การป้องกันการทำงานพร้อมกันในแต่ละรอบ

หากงานอาจใช้เวลานานกว่าหนึ่งช่วงของตัวจับเวลา ให้ป้องกันการทำงานพร้อมกันด้วย SemaphoreSlim หรือข้ามรอบนั้นหากงานยังทำงานอยู่

private readonly SemaphoreSlim _lock = new(1, 1);

protected override async Task ExecuteAsync(CancellationToken ct)
{
    using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));

    while (await timer.WaitForNextTickAsync(ct))
    {
        // Skip if previous tick is still running
        if (!await _lock.WaitAsync(0, ct))
        {
            _logger.LogWarning("Skipping tick — previous still running");
            continue;
        }
        try { await DoWorkAsync(ct); }
        finally { _lock.Release(); }
    }
}

การหน่วงแบบสุ่มสำหรับบริการแบบกระจาย

เมื่ออินสแตนซ์หลายรายการของบริการเริ่มทำงานพร้อมกัน การเพิ่มความคลาดเคลื่อนแบบสุ่มให้กับระยะเวลาล่าช้าเริ่มต้นจะช่วยป้องกันปัญหาการแห่เข้าถึงทรัพยากรร่วมกัน

protected override async Task ExecuteAsync(CancellationToken ct)
{
    // Add jitter: 0-5 seconds random delay on startup
    var jitter = TimeSpan.FromSeconds(Random.Shared.NextDouble() * 5);
    await Task.Delay(jitter, ct);

    using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
    while (await timer.WaitForNextTickAsync(ct))
        await RunJobAsync(ct);
}

ตัวอย่างการใช้งานจริง: บริการรีเฟรชแคช

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

public class CacheRefreshService : BackgroundService
{
    private readonly IServiceScopeFactory _factory;
    private readonly ILogger<CacheRefreshService> _log;

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        // Spread startups across instances
        await Task.Delay(TimeSpan.FromSeconds(Random.Shared.Next(0, 30)), ct);

        using var timer = new PeriodicTimer(TimeSpan.FromMinutes(10));
        while (await timer.WaitForNextTickAsync(ct))
        {
            try
            {
                using var scope = _factory.CreateScope();
                var cache = scope.ServiceProvider.GetRequiredService<ICacheService>();
                await cache.RefreshAllAsync(ct);
                _log.LogInformation("Cache refreshed at {Time}", DateTime.UtcNow);
            }
            catch (Exception ex) { _log.LogError(ex, "Cache refresh failed"); }
        }
    }
}

ตรวจสอบความเข้าใจ

เหตุใดจึงนิยมใช้ PeriodicTimer มากกว่าลูป Task.Delay สำหรับงานที่ทำซ้ำ

สรุป: งานที่ทำเป็นคาบและตัวจับเวลา

ประเด็นสำคัญ:

  • PeriodicTimer: รองรับ async การยกเลิก และไม่มีการคลาดเคลื่อน จึงเป็นทางเลือกสมัยใหม่
  • System.Threading.Timer: อิงตามฟังก์ชันเรียกกลับ เหมาะสำหรับผสานเข้ากับโค้ดเดิม
  • ห่อการทำงานในแต่ละรอบด้วย try/catch เพื่อไม่ให้ข้อผิดพลาดหยุดตัวจับเวลา
  • เพิ่มการหน่วงแบบสุ่มสำหรับอินสแตนซ์แบบกระจาย เพื่อหลีกเลี่ยงการแห่เข้าถึงทรัพยากรพร้อมกัน
  • ใช้ SemaphoreSlim เพื่อข้ามรอบหากรอบก่อนหน้ายังทำงานอยู่
  • คำนวณระยะเวลาล่าช้าเริ่มต้นเพื่อให้รอบการทำงานตรงกับขอบเขตเวลาของนาฬิกา

คำถามที่พบบ่อย

บทเรียน “งานตามช่วงเวลาและตัวจับเวลา” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “งานตามช่วงเวลาและตัวจับเวลา” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส C# Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส C# Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “งานตามช่วงเวลาและตัวจับเวลา”

กำหนดเวลางานที่ทำซ้ำด้วย PeriodicTimer และ System.Threading.Timer พร้อมจัดการการยกเลิกอย่างเหมาะสม คุณปฏิบัติ C# Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน C# Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน C# Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน

บทเรียน “งานตามช่วงเวลาและตัวจับเวลา” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน C# Academy นี้ได้ไหม

ได้ บทเรียน C# Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

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

  1. IHostedService และ BackgroundService
  2. โครงการ Worker Service
  3. งานตามช่วงเวลาและตัวจับเวลา
  4. งานตามกำหนดเวลาด้วย Quartz.NET
← กลับไปที่ C# Academy