Timer触发异步任务异常:API调用重复执行问题排查
Timer触发异步API调用时重复执行的异常原因及解决办法
问题现象
编写了定时调用OpenSea API的脚本,使用System.Timers.Timer每隔1秒触发一次异步任务TimerElapsedAsync。但执行到API调用的await openSeaApiClient.GetBestListingsInCollectionResponse(CollectionName)时,控制台会连续打印递增的Counter值(如Counter:2、3、4...),调试时也会持续触发Timer事件,无法正常推进后续流程。
核心代码片段
Program类代码
public class Program { private static OpenSeaApiClient openSeaApiClient; private static Timer timer; private static int counter = 1; private static Collection galaxyEggs9999; static async Task Main() { string apiKey = "myKey"; openSeaApiClient = new OpenSeaApiClient(apiKey); // 设置定时器每1秒执行一次API调用 timer = new Timer(TimeSpan.FromSeconds(1).TotalMilliseconds); timer.Elapsed += async (sender, e) => await TimerElapsedAsync(); timer.Start(); Console.WriteLine("Press Enter to exit."); Console.ReadKey(); timer.Stop(); timer.Dispose(); } private static async Task TimerElapsedAsync() { Console.WriteLine($"Counter: {counter++}"); if (galaxyEggs9999 == null) { galaxyEggs9999 = new Collection(openSeaApiClient, "galaxyeggs9999", "0xA08126f5E1ED91A635987071E6FF5EB2aEb67C48"); } await galaxyEggs9999.ExecuteAsync(); // 其他Collection实例执行逻辑 } }
Collection类核心代码
public class Collection { private readonly OpenSeaApiClient openSeaApiClient; private readonly string collectionName; private readonly string collectionContractAddress; public Collection(OpenSeaApiClient openSeaApiClient, string collectionName, string collectionContractAddress) { this.openSeaApiClient = openSeaApiClient; this.collectionName = collectionName; this.collectionContractAddress = collectionContractAddress; } public async Task ExecuteAsync() { // 其他逻辑... else { var bestListingsResponse = await openSeaApiClient.GetBestListingsInCollectionResponse(collectionName); floorPriceInEtherDouble = GetCollectionFloorPriceInEther(bestListingsResponse); // 其他逻辑... } } }
OpenSeaApiClient核心方法
public async Task<string> GetBestListingsInCollectionResponse(string collectionSlug) { string endpoint = $"api/v2/listings/collection/{collectionSlug}/best"; return await SendGetRequest(endpoint); }
原因分析
- 异步void事件处理的特性:
System.Timers.Timer的Elapsed事件期望返回void的委托,代码中使用的async (sender, e) => await TimerElapsedAsync()本质是异步void方法。这类方法在遇到第一个await(即API调用的异步等待)时会立即返回,Timer会判定当前事件处理已完成,到达下一个间隔就会再次触发事件。 - Timer的重入特性:默认情况下
System.Timers.Timer允许重入——前一次事件处理还未完成,到达设定间隔后会再次触发新的事件。而API调用属于耗时操作,await会释放当前线程,Timer到点就会再次执行TimerElapsedAsync,导致多个任务并发执行,因此Counter会连续递增。
解决办法
方法一:禁用Timer自动重置,手动重启
将Timer的AutoReset设为false,让Timer只触发一次,在异步任务完成后手动重启Timer,确保每次任务执行完成后才会触发下一次:
static async Task Main() { string apiKey = "myKey"; openSeaApiClient = new OpenSeaApiClient(apiKey); timer = new Timer(TimeSpan.FromSeconds(1).TotalMilliseconds); timer.AutoReset = false; // 关闭自动重置,只触发一次 timer.Elapsed += async (sender, e) => await TimerElapsedAsync(); timer.Start(); Console.WriteLine("Press Enter to exit."); Console.ReadKey(); timer.Stop(); timer.Dispose(); } private static async Task TimerElapsedAsync() { Console.WriteLine($"Counter: {counter++}"); if (galaxyEggs9999 == null) { galaxyEggs9999 = new Collection(openSeaApiClient, "galaxyeggs9999", "0xA08126f5E1ED91A635987071E6FF5EB2aEb67C48"); } await galaxyEggs9999.ExecuteAsync(); // 任务完成后重启Timer timer.Start(); }
方法二:用信号量控制并发
使用SemaphoreSlim限制同一时间只有一个任务在执行,避免Timer重入:
public class Program { private static OpenSeaApiClient openSeaApiClient; private static Timer timer; private static int counter = 1; private static Collection galaxyEggs9999; private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1); // 最多允许1个并发任务 static async Task Main() { string apiKey = "myKey"; openSeaApiClient = new OpenSeaApiClient(apiKey); timer = new Timer(TimeSpan.FromSeconds(1).TotalMilliseconds); timer.Elapsed += async (sender, e) => await TimerElapsedAsync(); timer.Start(); Console.WriteLine("Press Enter to exit."); Console.ReadKey(); timer.Stop(); timer.Dispose(); _semaphore.Dispose(); } private static async Task TimerElapsedAsync() { // 尝试获取信号量,若已被占用则直接返回,不执行本次任务 if (!_semaphore.Wait(0)) return; try { Console.WriteLine($"Counter: {counter++}"); if (galaxyEggs9999 == null) { galaxyEggs9999 = new Collection(openSeaApiClient, "galaxyeggs9999", "0xA08126f5E1ED91A635987071E6FF5EB2aEb67C48"); } await galaxyEggs9999.ExecuteAsync(); } finally { // 释放信号量,允许下一次任务执行 _semaphore.Release(); } } }
内容的提问来源于stack exchange,提问作者paro
相关产品推荐
相关产品推荐

