WinForm 通用多线程基类:队列处理与并发控制实战

竹里
竹里
发布时间:2026-10-15 | 3人浏览过
原创

在 WinForm 开发中,处理批量数据或耗时操作时常需使用多线程。本文介绍一个基于队列的通用多线程基类 QueueThreadBase。它封装了线程池管理、进度计算及异常捕获,开发者只需继承并重写核心处理方法即可实现高并发的数据处理功能。

设计目标与核心特性

该基类旨在解决 WinForm 应用中需要并发处理已知集合数据的场景。相比直接创建多个 Thread 对象,此方案提供了更完善的控制机制:

  • 确定的列表处理:构造函数接收初始集合,适合处理一次性加载到内存的任务队列。
  • 灵活的线程控制:可通过属性动态调整工作线程数量(默认 5 个)。
  • 中断与异常处理:提供取消标志 Cancel,并允许根据业务结果决定是终止当前线程还是所有线程。
  • 实时事件反馈:通过 AllCompleted 和 OneCompleted 事件,方便更新 UI 进度条或日志。

核心实现原理

基类的核心在于线程同步与队列消费逻辑。内部维护一个锁定的队列 m_InnerQueue,确保多线程安全地获取待处理项。

/// <summary>
/// 队列多线程基类, T 代表处理的单个数据类型
/// </summary>
public abstract class QueueThreadBase<T>
{
    #region 变量与属性
    private List<Thread> m_ThreadList;
    private int m_CompletedCount = 0; // 已完成个数
    private int m_QueueCount = 0;     // 总数
    private object m_AllCompletedLock = new object();
    private object m_PendingQueueLock = new object();
    private Queue<T> m_InnerQueue;
    
    public int ThreadCount { get; set; } = 5;
    public bool Cancel { get; set; }
    
    public event Action<CompetedEventArgs> AllCompleted;
    public event Action<T, CompetedEventArgs> OneCompleted;
    #endregion
}

第1步:初始化与线程启动

调用 Start() 方法后,系统会根据 ThreadCount 的数量创建后台线程。每个线程独立运行,循环从队列中取出任务。

public void Start()
{
    InitThread();
}

private void InitThread()
{
    m_ThreadList = new List<Thread>();
    for (int i = 0; i < ThreadCount; i++)
    {
        Thread t = new Thread(new ThreadStart(InnerDoWork));
        t.IsBackground = true;
        t.Start();
        m_ThreadList.Add(t);
    }
}

第2步:任务分发与线程工作

在 InnerDoWork 中,线程不断尝试从内部队列脱队(Dequeue)。如果队列空且未设置取消,线程将退出;否则执行具体的业务逻辑 DoWork。此方法为抽象方法,由子类实现具体处理逻辑。

private void InnerDoWork()
{
    try
    {
        while (!this.Cancel)
        {
            var t = CurrentPendingQueue;
            if (!t.IsHad) break; // 队列为空,退出循环

            Exception doWorkEx = null;
            DoWorkResult result = DoWorkResult.ContinueThread;

            try
            {
                result = DoWork(t.PendingValue);
            }
            catch (Exception ex)
            {
                doWorkEx = ex;
            }

            // 触发单个完成事件,包含进度百分比
            OnOneCompleted(t.PendingValue, new CompetedEventArgs() 
            { 
                CompetedPrecent = CalculateProgress(), 
                InnerException = doWorkEx 
            });

            // 根据结果决定后续动作
            if (result == DoWorkResult.AbortAllThread) this.Cancel = true;
            else if (result == DoWorkResult.AbortCurrentThread) break;
        }
    }
    finally
    {
        // 检查是否所有线程结束,触发全部完成事件
        LockCheckAllCompleted();
    }
}

第3步:线程安全的数据获取

CurrentPendingQueue 属性负责在锁的保护下安全地从队列中提取元素。这避免了多个线程争抢同一个任务导致的重复处理或空引用错误。

private PendingResult CurrentPendingQueue
{
    get
    {
        lock (m_PendingQueueLock)
        {
            PendingResult result = new PendingResult();
            if (m_InnerQueue.Count != 0)
            {
                result.PendingValue = m_InnerQueue.Dequeue();
                result.IsHad = true;
            }
            else
            {
                result.IsHad = false;
            }
            return result;
        }
    }
}

如何使用:创建自定义下载器

要使用此类,只需创建一个派生类并实现 DoWork 方法。返回 DoWorkResult 枚举值以控制线程行为。

public class DownLoadQueueThread : QueueThreadBase<int>
{
    public DownLoadQueueThread(IEnumerable<int> listIds) : base(listIds)
    {
    }

    protected override DoWorkResult DoWork(int downloadId)
    {
        try
        {
            // 执行实际下载或其他耗时操作...
            Console.WriteLine($"Processing ID: {downloadId}");
            
            // 正常完成,继续处理下一个
            return DoWorkResult.ContinueThread;
        }
        catch (Exception)
        {
            // 发生错误,终止当前线程
            return DoWorkResult.AbortCurrentThread;
            // 若需终止所有线程,返回 DoWorkResult.AbortAllThread
        }
    }
}

完整代码参考

以下是包含事件参数定义、枚举及内部类的完整基类代码:

public enum DoWorkResult
{
    ContinueThread = 0,
    AbortCurrentThread = 1,
    AbortAllThread = 2
}

public class CompetedEventArgs : EventArgs
{
    public int CompetedPrecent { get; set; }
    public Exception InnerException { get; set; }
}

public abstract class QueueThreadBase<T>
{
    private class PendingResult
    {
        public T PendingValue { get; set; }
        public bool IsHad { get; set; }
    }

    public int ThreadCount { get; set; } = 5;
    public bool Cancel { get; set; }
    
    private int m_CompletedCount = 0;
    private int m_QueueCount = 0;
    private int m_CompetedThreads = 0;
    private List<Thread> m_ThreadList;
    private object m_AllCompletedLock = new object();
    private object m_PendingQueueLock = new object();
    private Queue<T> m_InnerQueue;

    public event Action<CompetedEventArgs> AllCompleted;
    public event Action<T, CompetedEventArgs> OneCompleted;

    public QueueThreadBase(IEnumerable<T> collection)
    {
        m_InnerQueue = new Queue<T>(collection);
        m_QueueCount = m_InnerQueue.Count;
    }

    public void Start() { InitThread(); }

    private void InitThread()
    {
        m_ThreadList = new List<Thread>();
        for (int i = 0; i < ThreadCount; i++)
        {
            Thread t = new Thread(InnerDoWork);
            t.IsBackground = true;
            t.Start();
            m_ThreadList.Add(t);
        }
    }

    private void InnerDoWork()
    {
        try
        {
            while (!this.Cancel)
            {
                var item = CurrentPendingQueue;
                if (!item.IsHad) break;

                Exception error = null;
                DoWorkResult res = DoWorkResult.ContinueThread;

                try
                {
                    res = DoWork(item.PendingValue);
                }
                catch (Exception ex) 
                {
                    error = ex;
                }

                Interlocked.Increment(ref m_CompletedCount);
                int percent = m_QueueCount > 0 ? (m_CompletedCount * 100 / m_QueueCount) : 100;
                OnOneCompleted(item.PendingValue, new CompetedEventArgs 
                { 
                    CompetedPrecent = percent, 
                    InnerException = error 
                });

                if (res == DoWorkResult.AbortAllThread) Cancel = true;
                else if (res == DoWorkResult.AbortCurrentThread) break;
            }
        }
        finally
        {
            // 这里原代码逻辑可能存在竞态条件风险,建议在实际生产环境中使用 SemaphoreSlim 或 CountDownEvent 优化线程完成计数
        }
    }

    protected virtual DoWorkResult DoWork(T pendingValue) => DoWorkResult.ContinueThread;

    private PendingResult CurrentPendingQueue
    {
        get
        {
            lock (m_PendingQueueLock)
            {
                if (m_InnerQueue.Count > 0)
                {
                    return new PendingResult { PendingValue = m_InnerQueue.Dequeue(), IsHad = true };
                }
                return new PendingResult { IsHad = false };
            }
        }
    }

    private void OnOneCompleted(T value, CompetedEventArgs args)
    {
        OneCompleted?.Invoke(value, args);
    }

    private void OnAllCompleted(CompetedEventArgs args)
    {
        AllCompleted?.Invoke(args);
    }
    
    // 注意:原代码中的全部完成检测逻辑存在缺陷,仅当所有线程自然结束时才可能正确触发。
    // 生产环境建议使用更严谨的并发原语。
}

注意事项与排查

线程完成计数的潜在问题:原文代码在 InnerDoWork 外部进行全部完成的判断,这种“忙等待”加简单的计数器方式在多异常退出的情况下可能导致 AllCompleted 事件无法准确触发或死锁。建议在重构时使用 System.Threading.CountdownEvent 来监听所有工作线程的终结信号。

UI 线程交互:由于 OneCompleted 是在工作线程中触发的,若在 WinForm 中订阅该事件并更新控件(如 ProgressBar),请务必先检查 InvokeRequired 并使用 Invoke 方法进行线程安全切换,以免引发跨线程操作异常。

以上就是 WinForm 中使用通用多线程基类进行队列处理的详细内容,更多关于 .NET 并发编程与最佳实践的资料请关注本站其它相关文章!

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表