C# 线程(Thread)的常见解决方案

前言:多线程编程是应用程序开发中一个非常重要的部分,这里总结一些常见的线程(Thread)解决方案。注意:这里仅涉及Thread的常见解决方案,CLR 4.0以后的Task这里不做描述。

目录
1、使用BackgroundWorker组件
2、线程计时器
3、线程池
4、线程同步
5、总结


一、使用BackgroundWorker组件
创建多线程应用程序的非常常见的方式是使用 BackgroundWorker 组件。  可以通过编程方式创建 BackgroundWorker 对象,在WinForm中也可以将它从“工具箱”“组件”选项卡中拖到窗体上。
我们看一下它的事件:

C# 线程(Thread)的常见解决方案_第1张图片
BackgroundWorker 的用法相当简单:
1、 DoWork 事件调用要执行的后台操作;
2、ProgressChanged 事件更新后台操作进度信息( WorkerReportsProgress属性要设为true);
3、RunWorkerCompleted 事件在后台操作完成时触发;
4、RunWorkerAsync 方法开始执行后台操作;
5、CancelAsync方法请求取消后台操作。
我们看一个简单的示例:

C# 线程(Thread)的常见解决方案_第2张图片
创建Windows窗体应用程序。 添加一个名为resultLabelLabel控件并添加两个名为 startAsyncButtoncancelAsyncButtonButton 控件。 创建这两个按钮的 Click 事件处理程序。 从工具箱中的“组件”选项卡中,添加命名为 backgroundWorker1BackgroundWorker 组件。 创建 DoWorkProgressChangedBackgroundWorkerRunWorkerCompleted 事件处理程序。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace MyLib.WinFormEx
{
    public partial class BackgroundWorkerForm : Form
    {
        public BackgroundWorkerForm()
        {
            InitializeComponent();
            backgroundWorker1.WorkerReportsProgress = true;
            backgroundWorker1.WorkerSupportsCancellation = true;

            // DoWork 事件调用要执行的后台操作
            backgroundWorker1.DoWork += backgroundWorker1_DoWork;
            // ProgressChanged 事件更新后台操作进度信息(WorkerReportsProgress属性要设为true)
            backgroundWorker1.ProgressChanged += backgroundWorker1_ProgressChanged;
            // RunWorkerCompleted 事件在后台操作完成时触发
            backgroundWorker1.RunWorkerCompleted += backgroundWorker1_RunWorkerCompleted;
        }

        private void startAsyncButton_Click(object sender, EventArgs e)
        {
            if (backgroundWorker1.IsBusy != true)
            {
                // 开始执行后台操作
                backgroundWorker1.RunWorkerAsync();
            }
        }

        private void cancelAsyncButton_Click(object sender, EventArgs e)
        {
            if (backgroundWorker1.WorkerSupportsCancellation == true)
            {
                // 请求取消后台操作
                backgroundWorker1.CancelAsync();
            }
        }

        // DoWork 事件调用要执行的后台操作;
        private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
        {
            BackgroundWorker worker = sender as BackgroundWorker;

            for (int i = 1; i <= 10; i++)
            {
                if (worker.CancellationPending == true)
                {
                    e.Cancel = true;
                    break;
                }
                else
                {
                    // 执行耗时的操作并报告进度
                    System.Threading.Thread.Sleep(500);
                    worker.ReportProgress(i * 10);
                }
            }
        }

        // ProgressChanged 事件更新后台操作进度信息(WorkerReportsProgress属性要设为true);
        private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            resultLabel.Text = (e.ProgressPercentage.ToString() + "%");
        }

        // RunWorkerCompleted 事件在后台操作完成时触发;
        private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            if (e.Cancelled == true)
            {
                resultLabel.Text = "Canceled!";
            }
            else if (e.Error != null)
            {
                resultLabel.Text = "Error: " + e.Error.Message;
            }
            else
            {
                resultLabel.Text = "Done!";
            }
        }

    }
}

可以看到无论是反馈操作进度,还是操作完成触发事件,亦或是请求取消后台操作,都非常方便,当涉及单个线程的操作时,优先推荐使用 BackgroundWorker ,而不是Thread。
、线程计时器
System.Threading.Timer 类可用于定期在单独的线程上运行任务。例如,可以使用线程计时器来检查数据库的状态和完整性,或备份重要文件。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace MyLib.ConApp.ThreadTest
{
    public class ThreadTimerProgram
    {
        public static void Main(string[] args)
        {
            // 创建事件以在计时器回调中信号超时计数阈值。
            AutoResetEvent autoEvent = new AutoResetEvent(false);

            StatusChecker statusChecker = new StatusChecker(10);

            // 创建调用计时器方法的推断委托。
            TimerCallback tcb = statusChecker.CheckStatus;

            // 创建一个计时器, 它指示委托在一秒钟之后调用 CheckStatus, 此后每隔1/4 秒发出一次信号。
            Console.WriteLine("{0} 创建计时器.\n", DateTime.Now.ToString("h:mm:ss.fff"));
            Timer stateTimer = new Timer(tcb, autoEvent, 1000, 250);

            // 当自动事件信号时, 将周期更改为每1/2 秒。
            autoEvent.WaitOne(5000, false);
            stateTimer.Change(0, 500);
            Console.WriteLine("\n更改期间.\n");

            // 当自动事件第二次发出信号时, 释放计时器。
            autoEvent.WaitOne(5000, false);
            stateTimer.Dispose();
            Console.WriteLine("\n销毁计时器.");
        }

    }

    class StatusChecker
    {
        private int invokeCount;
        private int maxCount;

        public StatusChecker(int count)
        {
            invokeCount = 0;
            maxCount = count;
        }

        // 此方法由计时器委托调用。
        public void CheckStatus(Object stateInfo)
        {
            AutoResetEvent autoEvent = (AutoResetEvent)stateInfo;

            Console.WriteLine("{0} 正在检查状态 {1,2}.", DateTime.Now.ToString("h:mm:ss.fff"), (++invokeCount).ToString());

            if (invokeCount == maxCount)
            {
                // 重置计数器和信号主键。
                invokeCount = 0;
                autoEvent.Set();
            }
        }
    }
}

、线程池

“线程池”是可以用来在后台执行多个任务的线程集合。 这使主线程可以自由地异步执行其他任务。 线程池通常用于服务器应用程序。 每个传入请求都将分配给线程池中的一个线程,因此可以异步处理请求,而不会占用主线程,也不会延迟后续请求的处理。 一旦池中的某个线程完成任务,它将返回到等待线程队列中,等待被再次使用。 这种重用使应用程序可以避免为每个任务创建新线程的开销。 线程池通常具有最大线程数限制。 如果所有线程都繁忙,则额外的任务将放入队列中,直到有线程可用时才能够得到处理。 虽然可以实现自己的线程池,但是强烈推荐通过 ThreadPool 类使用 .NET Framework 提供的线程池。 对于线程池,可使用要运行的过程的委托调用 ThreadPool.QueueUserWorkItem 方法。
        public void DoWork()
        {
            // 对任务进行排队。
            System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(SomeLongTask));
            // 对另一个任务进行排队。
            System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(AnotherLongTask));
        }

        private void SomeLongTask(Object state)
        {
            // 插入代码以执行长任务。
        }

        private void AnotherLongTask(Object state)
        {
            // 插入代码以执行长任务。
        }


4、线程同步
线程的异步特性意味着必须协调对资源(如文件句柄、网络连接和内存)的访问。 否则,两个或更多的线程可能在同一时间访问相同的资源,而每个线程都不知道其他线程的操作。 结果将产生不可预知的数据损坏。
     4.1 锁(lock)
    lock (C#)  语句可以用来确保代码块完成运行,而不会被其他线程中断。 这是通过在代码块运行期间为给定对象获取互斥锁来实现的。lock 语句有一个作为参数的对象,在该参数的后面还有一个一次只能由一个线程执行的代码块。
public class TestThreading
    {
        private System.Object lockThis = new System.Object();

        public void Process()
        {
            lock (lockThis)
            {
                // 访问线程敏感的资源。
            }
        }

    }

提供给 lock 关键字的参数必须为基于引用类型的对象,该对象用来定义锁的范围。强烈建议 避免锁定 public 类型或锁定不受应用程序控制的对象实例。

    4.2 同步事件和等待句柄

     使用锁或监视器对于防止同时执行区分线程的代码块很有用,但是这些构造不允许一个线程向另一个线程传达事件。 这要“同步事件”,它是有两个状态(终止和非终止)的对象,可以用来激活和挂起线程。 让线程等待非终止的同步事件可以将线程挂起,将事件状态更改为终止可以将线程激活。 如果线程尝试等待已经终止的事件,则线程将继续执行,而不会延迟。

     同步事件有两种: AutoResetEventManualResetEvent 它们之间唯一的不同在于,无论何时,只要 AutoResetEvent 激活线程,它的状态将自动从终止变为非终止。 相反, ManualResetEvent 允许它的终止状态激活任意多个线程,只有当它的 Reset 方法被调用时才还原到非终止状态。

    可以通过调用 WaitOne WaitAnyWaitAll 等中的某个等待方法使线程等待事件。 WaitHandle.WaitOne() 使线程一直等待,直到单个事件变为终止状态; WaitHandle.WaitAny() 阻止线程,直到一个或多个指示的事件变为终止状态; WaitHandle.WaitAll() 阻止线程,直到所有指示的事件都变为终止状态。 当调用事件的 Set 方法时,事件将变为终止状态。
    
class Program
    {
        static AutoResetEvent autoEvent;

        static void DoWork()
        {
            Console.WriteLine("3、工作线程已启动, 正在等待事件...");
            autoEvent.WaitOne();
            Console.WriteLine("5、工作线程已重新激活, 现在退出...");

            Console.Read();
        }

        static void Main()
        {
            autoEvent = new AutoResetEvent(false);

            Console.WriteLine("1、主线程启动辅助线程...");
            Thread t = new Thread(DoWork);
            t.Start();

            Console.WriteLine("2、主线程休眠1秒...");
            Thread.Sleep(1000);

            Console.WriteLine("4、主线程向辅助线程发出信号...");
            autoEvent.Set();
        }


    }

    在上面的示例中,创建了一个线程,并由 Main 函数启动该线程。 新线程使用 WaitOne 方法等待一个事件。 在该事件被执行 Main 函数的主线程终止之前,该线程一直处于挂起状态。 一旦该事件终止,辅助线程将返回。 运行结果:
C# 线程(Thread)的常见解决方案_第3张图片

5、总结
    多线程编程始终是一个复杂的过程,但只要掌握了基本原理,理清了思路,也并不难,上面仅仅罗列了一些多线程编程的常见解决措施,这些措施能够满足绝大多数情况,但绝不是多线程编程的全部。

你可能感兴趣的:(C#)