Task.Delay sẽ thất bại nếu bạn chờ lâu hơn 49.7 ngày. Cụ thể, một đoạn code như: await Task.Delay(TimeSpan.FromDays(50)); sẽ gây ra lỗi. Nhưng tại sao lại như vậy và bạn có nên quan tâm?
Tại sao lại như vậy?
Nếu bạn thử chạy đoạn code sau:
using System;
using System.Threading.Tasks;
await Task.Delay(TimeSpan.FromDays(50));
Bạn sẽ nhận được thông báo lỗi:
System.ArgumentOutOfRangeException: The value needs to translate in milliseconds to -1 (signifying an infinite timeout), 0, or a positive integer less than or equal to the maximum allowed timer duration. (Parameter ‘delay’)
Sử dụng TimeSpan.FromDays(49) thì lại hoạt động bình thường.
Hmm, nhưng chúng ta đang truyền một giá trị dương mà? Đúng vậy, nhưng bên trong, Task.Delay sử dụng uint làm đơn vị cơ sở. Do đó, giá trị tối đa là uint.MaxValue - 1 tương đương với “4294967294U”. Đây là giá trị tính bằng mili giây, nên: 4294967294 ms / 1000 (để ra giây) / 60 (để ra phút) / 60 (để ra giờ) / 24 (để ra ngày) = 49.7103 ngày.
Task.Delay bên trong cũng kiểm tra điều này:
internal static uint ValidateTimeout(TimeSpan timeout, ExceptionArgument argument)
{
long totalMilliseconds = (long) timeout.TotalMilliseconds;
if (totalMilliseconds < -1L || totalMilliseconds > 4294967294L)
ThrowHelper.ThrowArgumentOutOfRangeException(argument, ExceptionResource.Task_InvalidTimerTimeSpan);
return (uint) totalMilliseconds;
}
Phương thức này được gọi khi bạn sử dụng Task.Delay.
Tôi có nên quan tâm?
Có lẽ là: Không. Chỉ khi bạn viết các logic nền cần trì hoãn công việc đến một thời điểm rất xa trong tương lai. Tôi đã gặp phải vấn đề này khi viết bộ lập lịch công việc của riêng mình: NCronJob. Về cơ bản, người dùng có thể nhập bất kỳ ký pháp CRON nào và nó sẽ hoạt động. Vì vậy, nếu bạn muốn một công việc chỉ được thực thi một lần mỗi năm, đương nhiên điều đó phải hoạt động. Vậy chúng ta có thể làm gì?
Tôi đã viết một phương thức trợ giúp như sau:
/// <summary>
/// Có ngữ nghĩa tương tự <see cref="Task.Delay(TimeSpan, TimeProvider, CancellationToken)"/> nhưng an toàn khi sử dụng cho các khoảng thời gian trì hoãn dài.
/// </summary>
/// <param name="targetTimeSpan">Khoảng <see cref="TimeSpan" /> cần chờ trước khi hoàn thành tác vụ, hoặc <see cref="Timeout.InfiniteTimeSpan" /> để chờ vô thời hạn.</param>
/// <param name="timeProvider"><see cref="TimeProvider" /> dùng để diễn giải <paramref name="targetTimeSpan" />.</param>
/// <param name="token">Một cancellation token để theo dõi trong khi chờ tác vụ hoàn thành.</param>
public static async Task LongDelaySafe(TimeSpan targetTimeSpan, TimeProvider timeProvider, CancellationToken token)
{
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(targetTimeSpan, TimeSpan.Zero);
var remainingTime = targetTimeSpan;
while (remainingTime > TimeSpan.Zero)
{
var delay = TimeSpan.FromMilliseconds(Math.Min(remainingTime.TotalMilliseconds, uint.MaxValue - 1));
await Task.Delay(delay, timeProvider, token).ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
remainingTime -= delay;
}
}
Hoặc nếu bạn muốn sử dụng cú pháp extension của C# 14 để có thể gọi: Task.LongDelaySafe:
internal static class TaskExtensions
{
extension(Task)
{
/// <summary>
/// Có ngữ nghĩa tương tự <see cref="Task.Delay(TimeSpan, TimeProvider, CancellationToken)"/> nhưng an toàn khi sử dụng cho các khoảng thời gian trì hoãn dài.
/// </summary>
/// <param name="targetTimeSpan">Khoảng <see cref="TimeSpan" /> cần chờ trước khi hoàn thành tác vụ, hoặc <see cref="Timeout.InfiniteTimeSpan" /> để chờ vô thời hạn.</param>
/// <param name="timeProvider"><see cref="TimeProvider" /> dùng để diễn giải <paramref name="targetTimeSpan" />.</param>
/// <param name="token">Một cancellation token để theo dõi trong khi chờ tác vụ hoàn thành.</param>
public static async Task LongDelaySafe(
TimeSpan targetTimeSpan,
TimeProvider timeProvider,
CancellationToken token)
{
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(targetTimeSpan, TimeSpan.Zero);
var remainingTime = targetTimeSpan;
while (remainingTime > TimeSpan.Zero)
{
var delay = TimeSpan.FromMilliseconds(Math.Min(remainingTime.TotalMilliseconds, uint.MaxValue - 1));
await Task.Delay(delay, timeProvider, token).ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
remainingTime -= delay;
}
}
}
}
Về cơ bản, chúng ta chia nhỏ và chinh phục ở ranh giới uint.MaxValue và await nhiều lần cho đến khi đạt được thời gian cụ thể mà người dùng yêu cầu.



