使用C#.Net实现文件夹内百万级文件的持续计数
解决ASP.NET实时文件计数更新的方案
一、先说说你原来代码的问题
你之前的方案有几个核心痛点导致无法正常工作:
Directory.EnumerateFiles(...).Count()是一次性计算总数,没法返回中间进度,用户自然看不到逐步增长的数字;- 后台线程直接操作
Session和页面控件是不安全的——ASP.NET的UI元素和Session都是和请求线程绑定的,后台线程不能直接修改这些内容; - 服务器端
Timer的Tick事件会触发页面回发,导致页面反复刷新,而且回发后原来的线程状态很容易丢失; - 用
Thread.Abort()终止线程是危险操作,可能引发资源泄漏。
二、重构后的解决方案
我们调整思路,用异步渐进计数+客户端AJAX无刷新更新的方式实现你要的实时效果:
1. 后台代码改造
用线程安全容器存储每个用户的计数进度,用异步任务替代直接创建Thread,同时支持安全终止任务:
using System; using System.Collections.Concurrent; using System.IO; using System.Threading; using System.Threading.Tasks; using System.Web.Configuration; using System.Web.Services; public partial class GetCount : System.Web.UI.Page { // 线程安全字典:存储每个用户的当前文件计数(键用SessionID) private static readonly ConcurrentDictionary<string, long> _userFileCounts = new ConcurrentDictionary<string, long>(); // 线程安全字典:存储每个用户的任务取消令牌 private static readonly ConcurrentDictionary<string, CancellationTokenSource> _userCts = new ConcurrentDictionary<string, CancellationTokenSource>(); protected void Page_Load(object sender, EventArgs e) { // 初始化当前用户的计数为0 if (!_userFileCounts.ContainsKey(Session.SessionID)) { _userFileCounts.TryAdd(Session.SessionID, 0); } } protected void btnStartCount_Click(object sender, EventArgs e) { string basePath = WebConfigurationManager.AppSettings["Path"]; string targetPath = Path.Combine(basePath, TextBox1.Text.Trim()); if (!Directory.Exists(targetPath)) { lblCount.Text = "目标路径不存在!"; return; } // 如果已有未完成的任务,先取消 if (_userCts.TryGetValue(Session.SessionID, out var existingCts)) { existingCts.Cancel(); existingCts.Dispose(); _userCts.TryRemove(Session.SessionID, out _); } // 重置当前用户的计数 _userFileCounts.TryUpdate(Session.SessionID, 0, _userFileCounts[Session.SessionID]); // 创建新的取消令牌 var cts = new CancellationTokenSource(); _userCts.TryAdd(Session.SessionID, cts); // 启动异步任务进行渐进式计数 Task.Run(() => CountFilesProgressively(targetPath, cts.Token), cts.Token); } protected void btnStopCount_Click(object sender, EventArgs e) { if (_userCts.TryGetValue(Session.SessionID, out var cts)) { cts.Cancel(); cts.Dispose(); _userCts.TryRemove(Session.SessionID, out _); lblCount.Text = "已停止计数"; } } // 渐进式文件计数方法 private void CountFilesProgressively(string path, CancellationToken token) { long currentCount = 0; try { // 遍历所有文件,逐个累加计数 foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) { // 检查是否需要取消任务 if (token.IsCancellationRequested) { break; } currentCount++; // 更新当前用户的计数进度 _userFileCounts.TryUpdate(Session.SessionID, currentCount, _userFileCounts[Session.SessionID]); // 短暂休眠,避免占用过多CPU资源 Thread.Sleep(10); } } catch (Exception ex) { // 异常时标记错误状态 _userFileCounts.TryUpdate(Session.SessionID, -1, _userFileCounts[Session.SessionID]); } finally { // 任务结束后清理取消令牌 _userCts.TryRemove(Session.SessionID, out _); } } // 供AJAX调用的WebMethod,获取当前计数 [WebMethod(EnableSession = true)] public static string GetCurrentCount() { string sessionId = System.Web.HttpContext.Current.Session.SessionID; if (_userFileCounts.TryGetValue(sessionId, out long count)) { if (count == -1) { return "计数过程中出现错误"; } return count.ToString(); } return "0"; } // 供AJAX调用的停止任务方法 [WebMethod(EnableSession = true)] public static void StopCurrentTask() { string sessionId = System.Web.HttpContext.Current.Session.SessionID; if (_userCts.TryGetValue(sessionId, out var cts)) { cts.Cancel(); cts.Dispose(); _userCts.TryRemove(sessionId, out _); } } }
2. 前端页面改造
用客户端定时器+AJAX实现无刷新实时更新计数:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="GetCount.aspx.cs" Inherits="YourNamespace.GetCount" %> <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>实时文件计数</title> <script src="https://code.jquery.com/jquery-3.6.4.min.js"></script> <script> var updateTimer; $(function () { // 开始计数按钮逻辑 $("#btnStartCount").click(function () { // 清空之前的定时器 if (updateTimer) clearInterval(updateTimer); // 每2秒请求一次最新计数(可调整间隔) updateTimer = setInterval(function () { $.ajax({ type: "POST", url: "GetCount.aspx/GetCurrentCount", contentType: "application/json; charset=utf-8", dataType: "json", success: function (res) { $("#lblCount").text(`当前文件数:${res.d}`); }, error: function () { $("#lblCount").text("获取计数失败"); } }); }, 2000); }); // 停止计数按钮逻辑 $("#btnStopCount").click(function () { clearInterval(updateTimer); // 通知后台停止任务 $.ajax({ type: "POST", url: "GetCount.aspx/StopCurrentTask", contentType: "application/json; charset=utf-8" }); }); // 页面关闭时清理资源 window.onbeforeunload = function () { clearInterval(updateTimer); $.ajax({ type: "POST", url: "GetCount.aspx/StopCurrentTask", contentType: "application/json; charset=utf-8", async: false }); }; }); </script> </head> <body> <form id="form1" runat="server"> <div> <asp:TextBox ID="TextBox1" runat="server" PlaceHolder="输入子文件夹路径"></asp:TextBox> <asp:Button ID="btnStartCount" runat="server" Text="开始计数" OnClick="btnStartCount_Click" /> <asp:Button ID="btnStopCount" runat="server" Text="停止计数" OnClick="btnStopCount_Click" /> <br /><br /> <asp:Label ID="lblCount" runat="server" Text="当前文件数:0"></asp:Label> </div> </form> </body> </html>
三、关键注意事项
- 线程安全:用
ConcurrentDictionary存储用户进度,避免多线程竞争问题; - 任务安全终止:用
CancellationTokenSource替代Thread.Abort(),防止资源泄漏; - 无刷新体验:客户端定时器+AJAX完全避免了页面回发,实现你想要的实时更新效果;
- IIS权限:确保IIS应用池的身份拥有目标文件夹的读取权限,否则会抛出访问异常;
- 应用池回收:如果计数任务耗时极长,可调整IIS应用池的回收策略,或者考虑用Windows服务单独处理这类长时间任务。
内容的提问来源于stack exchange,提问作者polisetty dhana harsha
相关产品推荐
相关产品推荐

