C#多线程编程:使用线程池

原文链接:https://www.cnblogs.com/wyt007/p/9486752.html

在之前的章节中我们讨论了创建线程和线程协作的几种方式。现在考虑另一种情况,即只花费极少的时间来完成创建很多异步操作。创建线程是昂贵的操作,所以为每个短暂的异步操作创建线程会产生显著的开销。
为了解决该问题,有一个常用的方式叫做池( pooling),线程池可以成功地适应于任何需要大量短暂的开销大的资源的情形。我们事先分配一定的资源,将这些资源放入到资源池。每次需要新的资源,只需从池中获取一个,而不用创建一个新的。当该资源不再被使用,时,就将其返回到池中。
.NET线程池是该概念的一种实现。通过System.Threading.ThreadPool类型可以使用线程池。线程池是受.NET通用语言运行时( Common Language Runtime,简称CLR)管理的。这意味着每个CLR都有一个线程池实例。ThreadPool类型拥有一个QueueUserWorkItem静态方法。该静态方法接受一个委托,代表用户自定义的一个异步操作。在该方法被调用后,委托会进入到内部队列中。如果池中没有任何线程,将创建一个新的工作线程( worker thread) 并将队列中第一个委托放入到该工作线程中。如果想线程池中放入新的操作,当之前的所有操作完成后,很可能只需重用一个线程来执行这些新的操作。然而,如果放置新的操作过快,线程池将创建更多的线程来执行这些操,作。创建太多的线程是有限制的,在这种情况下新的操作将在队列中等待直到线程池中的工作线程有能力来执行它们。
当停止向线程池中放置新操作时,线程池最终会删除一定时间后过期的不再使用的线程。这将释放所有那些不再需要的系统资源。我想再次强调线程池的用途是执行运行时间短的操作。使用线程池可以减少并行度耗费及节省操作系统资源。
我们只使用较少的线程,但是以比平常更慢的速度来执行异步操作, 使用一定数量的可用的工作线程批量处理这些操作。如果操作能快速地完成则比较适用线程池,但是执行长时间运行的计算密集型操作则会降低性能。
另一个重要事情是在ASPNET应用程序中使用线程池时要相当小心。ASPNET基础设施使用自己的线程池,如果在线程池中浪费所有的工作线程, Web服务器将不能够服务新的请求。在ASPNET中只推荐使用输入/输出密集型的异步操作,因为其使用了一个不同的方式,叫做IO线程。
在本章中,我们将学习使用线程池来执行异步操作。本章将覆盖将操作放入线程池的不同方式,以及如何取消一个操作,并防止其长时间运行。
保持线程中的操作都是短暂的是非常重要的。不要在线程池中放入长时间运行的操作,或者阻塞工作线程。这将导致所有工作线程变得繁忙,从而无法服务用户操作。这会导致性能问题和非常难以调试的错误。
请注意线程池中的工作线程都是后台线程。这意味着当所有的前台线程(包括主程序线程)完成后,所有的后台线程将停止工作。

在线程池中调用委托

本节将展示在线程池中如何异步的执行委托。另外,我们将讨论一个叫做异步编程模型(Asynchronous Programming Model,简称APM)的方式,这是NET历史中第一个异步编程模式。

class Program
{
    static void Main(string[] args)
    {
        int threadId = 0;

        RunOnThreadPool poolDelegate = Test;

        var t = new Thread(() => Test(out threadId));
        t.Start();
        t.Join();

        Console.WriteLine("Thread id: {0}", threadId);

        IAsyncResult r = poolDelegate.BeginInvoke(out threadId, Callback, "a delegate asynchronous call");
        r.AsyncWaitHandle.WaitOne();

        string result = poolDelegate.EndInvoke(out threadId, r);
        
        Console.WriteLine("Thread pool worker thread id: {0}", threadId);
        Console.WriteLine(result);

        Thread.Sleep(TimeSpan.FromSeconds(2));

        Console.ReadKey();
    }

    private delegate string RunOnThreadPool(out int threadId);

    private static void Callback(IAsyncResult ar)
    {
        Console.WriteLine("Starting a callback...");
        Console.WriteLine("State passed to a callbak: {0}", ar.AsyncState);
        Console.WriteLine("Is thread pool thread: {0}", Thread.CurrentThread.IsThreadPoolThread);
        Console.WriteLine("Thread pool worker thread id: {0}", Thread.CurrentThread.ManagedThreadId);
    }


    private static string Test(out int threadId)
    {
        Console.WriteLine("Starting...");
        Console.WriteLine("Is thread pool thread: {0}", Thread.CurrentThread.IsThreadPoolThread);
        Thread.Sleep(TimeSpan.FromSeconds(2));
        threadId = Thread.CurrentThread.ManagedThreadId;
        return string.Format("Thread pool worker thread id was: {0}", threadId);
    }
}

当程序运行时,使用旧的方式创建了一个线程,然后启动它并等待完成。由于线程的构造函数只接受一个无任何返回结果的方法,我们使用了lambda表达式来将对Test方法的调用包起来。我们通过打印出Thread.CurrentThread.IsThreadPoolThread属性值来确保该线程不是来自线程池。我们也打印出了受管理的线程ID来识别代码是被哪个线程执行的。
然后定义了一个委托并调用Beginlnvoke方法来运行该委托。BeginInvoke方法接受一个回调函数。该回调函数会在异步操作完成后会被调用,并且一个用户自定义的状态会传给该回调函数。该状态通常用于区分异步调用。结果,我们得到了一个实现了IAsyncResult接口的result对象。BeginInvoke立即返回了结果,当线程池中的工作线程在执行异步操作时,仍允许我们继续其他工作。当需要异步操作的结果时,可以使用BeginInvoke方法调用返回的result对象。我们可以使用result对象的IsCompleted属性轮询结果。但是在本例子中,使用的是AsyncWaitHandle属性来等待直到操作完成。当操作完成后,会得到一个结果,可以通过委托调用EndInvoke方法,将IAsyncResult对象传递给委托参数。
事实上使用AsyncWaitHandle并不是必要的。如果注释掉r.AsyncWaitHandle.WaitOne,代码照样可以成功运行, 因为EndInvoke方法事实上会等待异步操作完成。调用 "EndInvoke方法(或者针对其他异步API的EndOperationName方法)是非常重要的, '因为该方法会将任何未处理的异常抛回到调用线程中。当使用这种异步API时,请确保始终调用了Begin和End方法。
当操作完成后,传递给BeginInvoke方法的回调函数将被放置到线程池中,确切地说是,一个工作线程中。如果在Main方法定义的结尾注释掉Thread.Sleep方法调用,回调函数将不,会被执行。这是因为当主线程完成后,所有的后台线程会被停止,包括该回调函数。对委托和回调函数的异步调用很可能会被同一个工作线程执行。通过工作线程ID可以容易地看出。使用BeginOperationName/EndOperationName方法和.NET中的IAsyncResult对象等方 ,式被称为异步编程模型(或APM模式),这样的方法对被称为异步方法。该模式也被应用于多个.NET类库的API中,但在现代编程中,更推荐使用任务并行库( Task Parallel Library,简称TPL)来组织异步API。

向线程池中放入异步操作

class Program
{
    static void Main(string[] args)
    {
        const int x = 1;
        const int y = 2;
        const string lambdaState = "lambda state 2";

        ThreadPool.QueueUserWorkItem(AsyncOperation);
        Thread.Sleep(TimeSpan.FromSeconds(1));

        ThreadPool.QueueUserWorkItem(AsyncOperation, "async state");
        Thread.Sleep(TimeSpan.FromSeconds(1));

        ThreadPool.QueueUserWorkItem( state => {
                Console.WriteLine("Operation state: {0}", state);
                Console.WriteLine("Worker thread id: {0}", Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(TimeSpan.FromSeconds(2));
            }, "lambda state");

        ThreadPool.QueueUserWorkItem( _ =>
        {
            Console.WriteLine("Operation state: {0}, {1}", x+y, lambdaState);
            Console.WriteLine("Worker thread id: {0}", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(TimeSpan.FromSeconds(2));
        }, "lambda state");

        Thread.Sleep(TimeSpan.FromSeconds(2));

        Console.ReadKey();
    }

    private static void AsyncOperation(object state)
    {
        Console.WriteLine("Operation state: {0}", state ?? "(null)");
        Console.WriteLine("Worker thread id: {0}", Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(TimeSpan.FromSeconds(2));
    }
}

首先定义了AsyncOperation方法,其接受单个object类型的参数。然后使用QueueUser WorkItem方法将该方法放到线程池中。接着再次放入该方法,但是这次给方法调用传入了一个状态对象。该对象将作为状态参数传递给AsynchronousOperation方法。
在操作完成后让线程睡眠一秒钟,从而让线程池拥有为新操作重用线程的可能性。如果注释掉所有的Thread.Sleep调用,那么所有打印出的线程ID多半是不一样的。如果ID是一样的,那很可能是前两个线程被重用来运行接下来的两个操作。
首先将一个lambda表达式放置到线程池中。这里没什么特别的。我们使用了labmbda表达式语法,从而无须定义一个单独的方法。
然后,我们使用闭包机制,从而无须传递lambda表达式的状态。闭包更灵活,允许我,们向异步操作传递一个以上的对象而且这些对象具有静态类型。所以之前介绍的传递对象给,方法回调的机制既冗余又过时。在C#中有了闭包后就不再需要使用它了。

线程池与并行度

本节将展示线程池如何工作于大量的异步操作,以及它与创建大量单独的线程的方式有何不同。

class Program
{
    static void Main(string[] args)
    {
        const int numberOfOperations = 500;
        var sw = new Stopwatch();
        sw.Start();
        UseThreads(numberOfOperations);
        sw.Stop();
        Console.WriteLine("Execution time using threads: {0}", sw.ElapsedMilliseconds);

        sw.Reset();
        sw.Start();
        UseThreadPool(numberOfOperations);
        sw.Stop();
        Console.WriteLine("Execution time using threads: {0}", sw.ElapsedMilliseconds);

        Console.ReadKey();
    }

    static void UseThreads(int numberOfOperations)
    {
        using (var countdown = new CountdownEvent(numberOfOperations))
        {
            Console.WriteLine("Scheduling work by creating threads");
            for (int i = 0; i < numberOfOperations; i++)
            {
                var thread = new Thread(() => {
                    Console.Write("{0},", Thread.CurrentThread.ManagedThreadId);
                    Thread.Sleep(TimeSpan.FromSeconds(0.1));
                    countdown.Signal();
                });
                thread.Start();
            }
            countdown.Wait();
            Console.WriteLine();
        }
    }

    static void UseThreadPool(int numberOfOperations)
    {
        using (var countdown = new CountdownEvent(numberOfOperations))
        {
            Console.WriteLine("Starting work on a threadpool");
            for (int i = 0; i < numberOfOperations; i++)
            {
                ThreadPool.QueueUserWorkItem( _ => {
                    Console.Write("{0},", Thread.CurrentThread.ManagedThreadId);
                    Thread.Sleep(TimeSpan.FromSeconds(0.1));
                    countdown.Signal();
                });
            }
            countdown.Wait();
            Console.WriteLine();
        }
    }
}

当主程序启动时,创建了很多不同的线程,每个线程都运行一个操作。该操作打印出线,程ID并阻塞线程100毫秒。结果我们创建了500个线程,全部并行运行这些操作。虽然在我的机器上的总耗时是300毫秒,但是所有线程消耗了大量的操作系统资源。
然后我们使用了执行同样的任务,只不过不为每个操作创建一个线程,而将它们放入到线程池中。然后线程池开始执行这些操作。线程池在快结束时创建更多的线程,但是仍然花费了更多的时间,在我机器上是12秒。我们为操作系统节省了内存和线程数,但是为此付出了更长的执行时间。

实现一个取消选项

本节将通过一个示例来展示如何在线程池中取消异步操作。

class Program
{
    static void Main(string[] args)
    {
        using (var cts = new CancellationTokenSource())
        {
            CancellationToken token = cts.Token;
            ThreadPool.QueueUserWorkItem(_ => AsyncOperation1(token));
            Thread.Sleep(TimeSpan.FromSeconds(2));
            cts.Cancel();
        }

        using (var cts = new CancellationTokenSource())
        {
            CancellationToken token = cts.Token;
            ThreadPool.QueueUserWorkItem(_ => AsyncOperation2(token));
            Thread.Sleep(TimeSpan.FromSeconds(2));
            cts.Cancel();
        }

        using (var cts = new CancellationTokenSource())
        {
            CancellationToken token = cts.Token;
            ThreadPool.QueueUserWorkItem(_ => AsyncOperation3(token));
            Thread.Sleep(TimeSpan.FromSeconds(2));
            cts.Cancel();
        }

        Thread.Sleep(TimeSpan.FromSeconds(2));
    }

    static void AsyncOperation1(CancellationToken token)
    {
        Console.WriteLine("Starting the first task");
        for (int i = 0; i < 5; i++)
        {
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("The first task has been canceled.");
                return;
            }
            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        Console.WriteLine("The first task has completed succesfully");
    }

    static void AsyncOperation2(CancellationToken token)
    {
        try
        {
            Console.WriteLine("Starting the second task");

            for (int i = 0; i < 5; i++)
            {
                token.ThrowIfCancellationRequested();
                Thread.Sleep(TimeSpan.FromSeconds(1));
            }
            Console.WriteLine("The second task has completed succesfully");
        }
        catch (OperationCanceledException)
        {
            Console.WriteLine("The second task has been canceled.");
        }
    }

    private static void AsyncOperation3(CancellationToken token)
    {
        bool cancellationFlag = false;
        token.Register(() => cancellationFlag = true);
        Console.WriteLine("Starting the third task");
        for (int i = 0; i < 5; i++)
        {
            if (cancellationFlag)
            {
                Console.WriteLine("The third task has been canceled.");
                return;
            }
            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        Console.WriteLine("The third task has completed succesfully");
    }
}

本节中介绍了CancellationTokenSource和CancellationToken两个新类。它们在.NET4.0被引人, 目前是实现异步操作的取消操作的事实标准。由于线程池已经存在了很长时间,并,没有特殊的API来实现取消标记功能,但是仍然可以对线程池使用上述API。
在本程序中使用了三种方式来实现取消过程。第一个是轮询来检查CancellationToken.IsCancellationRequested属性。如果该属性为true,则说明操作需要被取消,我们必须放弃该操作。
第二种方式是抛出一个OperationCancelledException异常。这允许在操作之外控制取消过程,即需要取消操作时,通过操作之外的代码来处理。
最后一种方式是注册一个回调函数。当操作被取消时,在线程池将调用该回调函数。这允许链式传递一个取消逻辑到另一个异步操作中。

在线程池中使用等待事件处理器及超时

本节将描述如何在线程池中对操作实现超时,以及如何在线程池中正确地等待。

class Program
{
    static void Main(string[] args)
    {
        RunOperations(TimeSpan.FromSeconds(5));
        RunOperations(TimeSpan.FromSeconds(7));
    }

    static void RunOperations(TimeSpan workerOperationTimeout)
    {
        using (var evt = new ManualResetEvent(false))
        using (var cts = new CancellationTokenSource())
        {
            Console.WriteLine("Registering timeout operations...");
            var worker = ThreadPool.RegisterWaitForSingleObject(evt,
                (state, isTimedOut) => WorkerOperationWait(cts, isTimedOut), null, workerOperationTimeout, true);

            Console.WriteLine("Starting long running operation...");

            ThreadPool.QueueUserWorkItem(_ => WorkerOperation(cts.Token, evt));

            Thread.Sleep(workerOperationTimeout.Add(TimeSpan.FromSeconds(2)));
            worker.Unregister(evt);
        }
    }

    static void WorkerOperation(CancellationToken token, ManualResetEvent evt)
    {
        for(int i = 0; i < 6; i++)
        {
            if (token.IsCancellationRequested)
            {
                return;
            }
            Thread.Sleep(TimeSpan.FromSeconds(1));
        }
        evt.Set();
    }

    static void WorkerOperationWait(CancellationTokenSource cts, bool isTimedOut)
    {
        if (isTimedOut)
        {
            cts.Cancel();
            Console.WriteLine("Worker operation timed out and was canceled.");
        }
        else
        {
            Console.WriteLine("Worker operation succeded.");
        }
    }
}

线程池还有一个有用的方法: ThreadPool.RegisterWaitForSingleObject,该方法允许我们将回调函数放入线程池中的队列中。当提供的等待事件处理器收到信号或发生超时时,该回调函数将被调用。这允许我们为线程池中的操作实现超时功能。
首先按顺序向线程池中放入一个耗时长的操作。它运行6秒钟然后一旦成功完成,会设置一个ManualResetEvent信号类。其他的情况下,比如需要取消操作,则该操作会被丢弃。 
然后我们注册了第二个异步操作。当从ManualResetEvent对象接受到一个信号后,该异步操作会被调用。如果第一个操作顺利完成,会设置该信号量。另一种情况是第一个操作还未完成就已经超时。如果发生了该情况,我们会使用CancellationToken来取消第一个操作。
最后,为操作提供5秒的超时时间是不够的。这是因为操作会花费6秒来完成,只能取消该操作。所以如果提供7秒的超时时间是可行的,该操作会顺利完成。
当有大量的线程必须处于阻塞状态中等待一些多线程事件发信号时,以上方式非常有用。借助于线程池的基础设施,我们无需阻塞所有这样的线程。可以释放这些线程直到信号事件被设置。在服务器端应用程序中这是个非常重要的应用场景,因为服务器端应用程序要求高伸缩性及高性能。

使用计时器

本节将描述如何使用System.Threading.Timer对象来在线程池中创建周期性调用的异步。

class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Press 'Enter' to stop the timer...");
            DateTime start = DateTime.Now;
            _timer = new Timer(_ => TimerOperation(start), null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2));

            Thread.Sleep(TimeSpan.FromSeconds(6));

            _timer.Change(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(4));

            Console.ReadLine();

            _timer.Dispose();

            Console.ReadKey();
        }

        static Timer _timer;

        static void TimerOperation(DateTime start)
        {
            TimeSpan elapsed = DateTime.Now - start;
            Console.WriteLine("{0} seconds from {1}. Timer thread pool thread id: {2}", elapsed.Seconds, start,Thread.CurrentThread.ManagedThreadId);
        }
    }

我们首先创建了一个Timer实例。第一个参数是一个1ambda表达式,将会在线程池中被执行。我们调用TimerOperation方法并给其提供一个起始时间。由于无须使用用户状态对象,所以第二个参数为null,然后指定了什么时候会第一次运行TimerOperation,以及之后再次调用的间隔时间。所以第一个值实际上说明一秒后会启动第一次操作,然后每隔两秒再次运行。
之后等待6秒后修改计时器。在调用timer.Change方法一秒后启动TimerOperation,然后每隔4秒再次运行。
计时器还可以更复杂:可以以更复杂的方式使用计时器。比如,可以通过Timeout.Infinet值提供给计时器个间隔参数来只允许计时器操作一次。然后在计时器异步操作内,能够设置下一次计时器操作将被执行的时间。具体时间取决于自定义业务逻辑。

使用BackgroundWorker组件

class Program
{
    static void Main(string[] args)
    {
        var bw = new BackgroundWorker();
        bw.WorkerReportsProgress = true;
        bw.WorkerSupportsCancellation = true;

        bw.DoWork += Worker_DoWork;
        bw.ProgressChanged += Worker_ProgressChanged;
        bw.RunWorkerCompleted += Worker_Completed;

        bw.RunWorkerAsync();

        Console.WriteLine("Press C to cancel work");
        do
        {
            if (Console.ReadKey(true).KeyChar == 'C')
            {
                bw.CancelAsync();
            }
            
        }
        while(bw.IsBusy);
    }

    static void Worker_DoWork(object sender, DoWorkEventArgs e)
    {
        Console.WriteLine("DoWork thread pool thread id: {0}", Thread.CurrentThread.ManagedThreadId);
        var bw = (BackgroundWorker) sender;
        for (int i = 1; i <= 100; i++)
        {

            if (bw.CancellationPending)
            {
                e.Cancel = true;
                return;
            }

            if (i%10 == 0)
            {
                bw.ReportProgress(i);
            }

            Thread.Sleep(TimeSpan.FromSeconds(0.1));
        }
        e.Result = 42;
    }

    static void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        Console.WriteLine("{0}% completed. Progress thread pool thread id: {1}", e.ProgressPercentage,
            Thread.CurrentThread.ManagedThreadId);
    }

    static void Worker_Completed(object sender, RunWorkerCompletedEventArgs e)
    {
        Console.WriteLine("Completed thread pool thread id: {0}", Thread.CurrentThread.ManagedThreadId);
        if (e.Error != null)
        {
            Console.WriteLine("Exception {0} has occured.", e.Error.Message);
        }
        else if (e.Cancelled)
        {
            Console.WriteLine("Operation has been canceled.");
        }
        else
        {
            Console.WriteLine("The answer is: {0}", e.Result);
        }
    }
}

当程序启动时,创建了一个BackgroundWorker组件的实例。显式地指出该后台工作线,程支持取消操作及该操作进度的通知。
接下来是最有意思的部分。我们没有使用线程池和委托,而是使用了另一个C#语法,称为事件。事件表示了一些通知的源或当通知到达时会有所响应的一系列订阅者。在本例中,我们将订阅三个事件,当这些事件发生时,将调用相应的事件处理器。当事件通知其订,阅者时,具有特殊的定义签名的方法将被调用。
因此,除了将异步API组织为Begin/End方法对,还可以只启动一个异步操作然后订阅给不同的事件。这些事件在该操作执行时会被触发。这种方式被称为基于事件的异步模式, ( Event-based Asynchronous Pattern,简称EAP)。这是历史上第二种用来构造异步程序的方式,现在更推荐使用TPL
我们共定义了三个事件。第一个是DoWork事件。当一个后台工作对象通过RunWorkerAsync方法启动一个异步操作时,该事件处理器将被调用。该事件处理器将会运行在线程池中。如果需要取消操作,则这里是主要的操作点来取消执行。同时也可以提供该操作的运行进程信息。最后,得到结果后,将结果设置给事件参数,然后RunWorkerCompleted事件处理器将被调用。在该方法中,可以知道操作是成功完成,还是发生错误,抑或被取消。
基于此, BackgroundWorker组件实际上被使用于Windows窗体应用程序(Windows Forms Applications,简称WPF)中。该实现通过后台工作事件处理器的代码可以直接与UI控制器交互。与线程池中的线程与UI控制器交互的方式相比较,使用BackgroundWorker组件的方式更加自然和好用。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值