Java回调函数正确管理:长耗时任务场景的最佳实践
读卡器回调函数处理长耗时任务的最佳实践
核心原则只有一个:回调函数必须快速返回,绝对不能阻塞读卡器的事件触发线程。一旦回调被阻塞,读卡器的新卡片检测、事件上报都会停滞,这正是你遇到的问题。下面是具体的落地方案:
1. 异步解耦:把长任务丢到后台执行
回调只做「接收卡片信息、参数校验、提交任务」这三件事,立即返回。剩下的远程调用等长耗时逻辑,交给后台线程处理。
以C#为例,用Task.Run或自定义线程池提交异步任务:
// 提前初始化任务工厂,控制并发数(避免打垮远程服务器) private readonly TaskFactory _taskFactory = new TaskFactory( new LimitedConcurrencyLevelTaskScheduler(5)); public void CallbackFunction(CardContent PresentedCard) { // 快速校验参数,无效直接返回 if (PresentedCard == null || string.IsNullOrEmpty(PresentedCard.CardId)) return; // 复制卡片数据,避免读卡器后续修改原对象(部分读卡器会复用对象实例) var cardData = new CardContent { CardId = PresentedCard.CardId, CardInfo = PresentedCard.CardInfo, // 复制其他需要的字段 }; // 提交后台任务,不阻塞当前回调线程 _taskFactory.StartNew(() => ProcessCardWithRemoteServers(cardData)) .ContinueWith(t => { // 捕获任务异常,记录日志或告警 if (t.Exception != null) { // Log.Error(t.Exception, $"处理卡片[{cardData.CardId}]失败"); } }, TaskContinuationOptions.OnlyOnFaulted); } // 实际处理远程调用的方法 private void ProcessCardWithRemoteServers(CardContent card) { // 遍历远程服务器,逐个调用接口 foreach (var serverConfig in _remoteServerConfigs) { try { // 这里写调用远程接口的逻辑,比如HTTP请求、Socket通信等 SendCardDataToServer(serverConfig, card); } catch (Exception ex) { // 单个服务器调用失败,记录日志后继续处理下一个 // Log.Warn(ex, $"向服务器[{serverConfig.Url}]发送卡片[{card.CardId}]数据失败"); } } }
注:
LimitedConcurrencyLevelTaskScheduler是C#里自定义的线程池调度器,用来限制同时运行的任务数,避免并发过高导致远程服务器过载。如果你不想自定义,也可以用SemaphoreSlim来做并发控制(见下一节)。
2. 并发控制:避免任务无限制堆积
如果短时间内有大量卡片事件,无限制提交任务会导致本地资源耗尽或远程服务器被打垮。必须加并发限制:
用SemaphoreSlim实现简单的并发控制:
// 最多同时运行5个远程处理任务 private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(5, 10); public void CallbackFunction(CardContent PresentedCard) { if (PresentedCard == null) return; var cardData = new CardContent { /* 复制字段 */ }; // 用异步方式等待信号量,不阻塞回调线程 _ = Task.Run(async () => { await _semaphore.WaitAsync(); try { await ProcessCardWithRemoteServersAsync(cardData); } finally { // 无论成功失败,都释放信号量 _semaphore.Release(); } }); } // 异步版远程处理方法(推荐用HttpClient异步调用) private async Task ProcessCardWithRemoteServersAsync(CardContent card) { using var httpClient = new HttpClient(); foreach (var server in _remoteServers) { try { var response = await httpClient.PostAsJsonAsync(server.Url, card); response.EnsureSuccessStatusCode(); // 处理服务器响应 } catch (HttpRequestException ex) { // Log.Warn(ex, $"调用服务器[{server.Url}]失败"); } } }
3. 卡片事件的缓冲与去重
场景A:需要处理所有卡片事件
如果必须响应每一次卡片触发(哪怕前一个任务没完成),可以用阻塞队列缓冲任务,后台用固定数量的线程消费队列,避免任务直接堆积:
// 限制队列最大容量,防止内存溢出 private readonly BlockingCollection<CardContent> _cardTaskQueue = new BlockingCollection<CardContent>(20); // 类初始化时启动消费线程 public YourReaderWrapperClass() { // 启动2个消费线程,根据实际情况调整数量 for (int i = 0; i < 2; i++) { Task.Run(() => ConsumeCardTaskQueue()); } } public void CallbackFunction(CardContent PresentedCard) { if (PresentedCard == null) return; var cardData = new CardContent { /* 复制字段 */ }; // 尝试加入队列,队列满时直接丢弃并记录日志(也可以选择阻塞,但不推荐) if (!_cardTaskQueue.TryAdd(cardData, TimeSpan.FromMilliseconds(50))) { // Log.Warn("卡片任务队列已满,丢弃当前卡片事件"); } } private void ConsumeCardTaskQueue() { // 持续从队列取任务处理 foreach (var card in _cardTaskQueue.GetConsumingEnumerable()) { try { ProcessCardWithRemoteServers(card); } catch (Exception ex) { // Log.Error(ex, $"处理队列中的卡片[{card.CardId}]失败"); } } }
场景B:避免重复处理同一张卡
如果同一张卡短时间内重复触发(比如用户反复插拔),可以加去重逻辑:
// 用哈希集合记录最近处理过的卡片ID,搭配定时器清理过期记录 private readonly HashSet<string> _processedCardIds = new HashSet<string>(); private readonly Timer _cleanupTimer; public YourReaderWrapperClass() { // 每5分钟清理一次过期的卡片ID _cleanupTimer = new Timer(_ => { lock (_processedCardIds) { _processedCardIds.Clear(); } }, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)); } public void CallbackFunction(CardContent PresentedCard) { if (PresentedCard == null) return; var cardId = PresentedCard.CardId; lock (_processedCardIds) { if (_processedCardIds.Contains(cardId)) { // 重复卡片,直接返回 return; } _processedCardIds.Add(cardId); } var cardData = new CardContent { /* 复制字段 */ }; // 提交后台任务... }
4. 错误处理与重试
远程调用大概率会遇到网络波动、服务器超时等问题,必须加重试机制:
- 简单场景下自己写重试逻辑:
private async Task SendCardDataToServerAsync(ServerConfig server, CardContent card) { int retryCount = 3; while (retryCount > 0) { try { using var httpClient = new HttpClient(); var response = await httpClient.PostAsJsonAsync(server.Url, card); response.EnsureSuccessStatusCode(); return; } catch (HttpRequestException) { retryCount--; if (retryCount == 0) throw; // 重试前等待一段时间,避免频繁请求 await Task.Delay(TimeSpan.FromSeconds(1)); } } }
5. 优雅关闭
程序退出时,要确保后台任务能优雅结束,避免数据丢失:
- 关闭阻塞队列:
_cardTaskQueue.CompleteAdding() - 等待后台任务完成:
Task.WaitAll(_backgroundTasks) - 释放资源:
_semaphore.Dispose()、_cleanupTimer.Dispose()
内容的提问来源于stack exchange,提问作者guiguietben
相关产品推荐
相关产品推荐

