งานตามช่วงเวลาและตัวจับเวลา
กำหนดเวลางานที่ทำซ้ำด้วย 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- IHostedService และ BackgroundService
- โครงการ Worker Service
- งานตามช่วงเวลาและตัวจับเวลา
- งานตามกำหนดเวลาด้วย Quartz.NET