代码改变世界

C# 线程手册 第五章 扩展多线程应用程序 在C#中使用线程池

2012-04-09 22:08  DanielWise  阅读(2437)  评论(3编辑  收藏  举报

  本章的之前部分内容主要介绍如何在.NET Framework 中使用线程池的概念。现在我们要介绍如何使用C# 实现创建并使用线程池的.NET 应用程序。如之前描述的那样,System.Threading 命名空间中包含的ThreadPool 类可以被用于在.NET 应用程序中创建一个线程池。

  在我们真正编码之前,我们必须对ThreadPool 类中的两个重要规则非常清楚。分别是:

  1. 每个应用程序域中只能有一个ThreadPool 对象

  2. 我们第一次调用ThreadPool.QueueUserWorkItem() 方法时会创建一个ThreadPool 对象,通过一个定时器或者注册的等待操作调用的回调方法(内部使用应用程序域的线程池)也可以创建一个ThreadPool 对象。

 

  首先,让我们通过几个例子来看一下为什么线程池比单独开启一个线程要好。在第一个例子(ThreadDemo.cs)中我们将使用独立线程(相对于线程池来说,以下类同)来启动两个长时间运行任务,而在第二个例子(ThreadPoolDemo.cs )中我们将使用一个线程池启动两个同样的任务:

/*************************************
/* Copyright (c) 2012 Daniel Dong
 * 
 * Author:Daniel Dong
 * Blog:  www.cnblogs.com/danielWise
 * Email: guofoo@163.com
 * 
 */

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

namespace ThreadDemo
{
    class ThreadDemo
    {
        public void LongTask1()
        {
            for (int i = 0; i <= 999; i++)
            {
                Console.WriteLine("Long Task 1 is being executed");
            }
        }

        public void LongTask2()
        {
            for (int i = 0; i <= 999; i++)
            {
                Console.WriteLine("Long Task 2 is being executed");
            }
        }

        static void Main(string[] args)
        {
            ThreadDemo td = new ThreadDemo();
            for (int i = 0; i < 10; i++)
            {
                Thread t1 = new Thread(new ThreadStart(td.LongTask1));
                t1.Start();

                Thread t2 = new Thread(new ThreadStart(td.LongTask2));
                t2.Start();
            }

            Console.Read();
        }
    }
}

  在上面的例子中,我们使用独立线程t1 和 t2 启动了两个任务LongTask1 和 LongTask2. 需要注意的是我们在一个循环中重复调用线程,目的是为了对操作系统处理能力施压,通过结果对比,我们可以清楚地看到使用线程池的优势。下面的ThreadPoolDemo 类显示了使用ThreadPool 类的代码:

/*************************************
/* Copyright (c) 2012 Daniel Dong
 * 
 * Author:Daniel Dong
 * Blog:  www.cnblogs.com/danielWise
 * Email: guofoo@163.com
 * 
 */

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

namespace ThreadPoolDemo
{
    class ThreadPoolDemo
    {
        public void LongTask1(object obj)
        {
            for (int i = 0; i <= 999; i++)
            {
                Console.WriteLine("Long Task 1 is being executed");
            }
        }

        public void LongTask2(object obj)
        {
            for (int i = 0; i <= 999; i++)
            {
                Console.WriteLine("Long Task 2 is being executed");
            }
        }

        static void Main(string[] args)
        {
            ThreadPoolDemo tpd = new ThreadPoolDemo();
            for (int i = 0; i < 10; i++)
            {
                ThreadPool.QueueUserWorkItem(new WaitCallback(tpd.LongTask1));
                ThreadPool.QueueUserWorkItem(new WaitCallback(tpd.LongTask2));
            }

            Console.Read();
        }
    }
}

  让我们讨论一下上面的例子。它包括两个独立的任务LongTask1 和 LongTask2, 这两个任务都是简单地在一个循环中向控制台显示消息。将任务操作过程委托给WaitCallback() 方法可以免除为每个独立任务设置线程属性的过程并可以使用ThreadPool 启动这些任务, 具体请看下面的代码块:

ThreadPool.QueueUserWorkItem(new WaitCallback(tpd.LongTask1));
ThreadPool.QueueUserWorkItem(new WaitCallback(tpd.LongTask2));

  注意QueueUserWorkItem 是ThreadPool 类中的一个静态方法因而可以由ThreadPool类直接调用。这个例子也有一个Console.Read() 语句,它会让控制台一直等待用户输入回车键或其他任意键。

  通过一个接一个地运行ThreadDemo 和 ThreadPoolDemo 应用程序,我们可以使用任务管理器对两者进行比较。取决于操作系统的处理能力所以在每个操作系统上结果都可能不同,但是相对结果将会是一样的。

  ThreadDemo 应用程序使用的线程数目图示:

2012-4-9 21-41-05

  ThreadPoolDemo 应用程序使用的线程数目图示:

2012-4-9 21-42-48

  通过两个截图的对比,我可以可以很明显地发现使用ThreadPool 不仅可以帮助降低应用程序使用的线程数量还可以减小CPU时间和应用程序使用的内存大小。

 

  下一个例子显示了如何向一个线程池中的线程传递参数以及接收它的返回值。线程池架构仅允许我们传递一个单一对象参数,但是通常我们向给被一个线程池中的线程执行的方法传递多个参数。然而,我们可以很容易地将所有必要的参数包装到一个类中并将类的实例作为一个参数传递给QueueUserWorkItem() 方法:

/*************************************
/* Copyright (c) 2012 Daniel Dong
 * 
 * Author:Daniel Dong
 * Blog:  www.cnblogs.com/danielWise
 * Email: guofoo@163.com
 * 
 */

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

namespace ThreadPoolState
{
    class ThreadPoolState
    {
        public void Task1(object stateObj)
        {
            ObjState stObj = (ObjState)stateObj;

            Console.WriteLine("Input Argument 1 in task 1: " + stObj.inarg1);
            Console.WriteLine("Input Argument 2 in task 1: " + stObj.inarg2);
            stObj.outval = "From Task 1 " + stObj.inarg1 + " " + stObj.inarg2;
        }

        public void Task2(object stateObj)
        {
            ObjState stObj = (ObjState)stateObj;

            Console.WriteLine("Input Argument 1 in task 2: " + stObj.inarg1);
            Console.WriteLine("Input Argument 2 in task 2: " + stObj.inarg2);
            stObj.outval = "From Task 2 " + stObj.inarg1 + " " + stObj.inarg2;
        }

        static void Main(string[] args)
        {
            ObjState stObj1 = new ObjState();
            stObj1.inarg1 = "String Param1 of task 1";
            stObj1.inarg2 = "String Param2 of task 1";

            ObjState stObj2 = new ObjState();
            stObj2.inarg1 = "String Param1 of task 2";
            stObj2.inarg2 = "String Param2 of task 2";

            ThreadPoolState tps = new ThreadPoolState();
            //Queue a task
            ThreadPool.QueueUserWorkItem(new WaitCallback(tps.Task1), stObj1);
            //Queue another task
            ThreadPool.QueueUserWorkItem(new WaitCallback(tps.Task2), stObj2);

            Console.Read();
        }
    }
}

  ThreadPoolState 的输出结果如下:

2012-4-9 22-05-11

  我们现在来一步一步地分析上面的例子。这个例子与之前的例子非常类似除了传递了一个对象;我们使用ObjState 对象向线程池的任务队列传递输入和输出参数。

  ObjState 对象包含两个输入参数和一个输出参数,所有类型都是String, 如下面代码块显示:

internal class ObjState
{
    protected internal String inarg1;
    protected internal String inarg2;
    protected internal String outval;
}

  下一步我们定义了两个方法,task1 和 task2, 并为它们分别传递了一个ObjState 对象的实例作为参数。task1 和 task2 将输入参数对象的inarg1 和 inarg2 值组合起来并将结果存储到outval 变量中。如下面代码块所示:

public void Task1(object stateObj)
{
    ObjState stObj = (ObjState)stateObj;

    Console.WriteLine("Input Argument 1 in task 1: " + stObj.inarg1);
    Console.WriteLine("Input Argument 2 in task 1: " + stObj.inarg2);
    stObj.outval = "From Task 1 " + stObj.inarg1 + " " + stObj.inarg2;
}

public void Task2(object stateObj)
{
    ObjState stObj = (ObjState)stateObj;

    Console.WriteLine("Input Argument 1 in task 2: " + stObj.inarg1);
    Console.WriteLine("Input Argument 2 in task 2: " + stObj.inarg2);
    stObj.outval = "From Task 2 " + stObj.inarg1 + " " + stObj.inarg2;
}

  在Main() 方法中我们使用ThreadPool.QueueUserWorkItem() 方法在线程池中运行这两个任务,如下代码块所示:

static void Main(string[] args)
        {
            ObjState stObj1 = new ObjState();
            stObj1.inarg1 = "String Param1 of task 1";
            stObj1.inarg2 = "String Param2 of task 1";

            ObjState stObj2 = new ObjState();
            stObj2.inarg1 = "String Param1 of task 2";
            stObj2.inarg2 = "String Param2 of task 2";

            ThreadPoolState tps = new ThreadPoolState();
            //Queue a task
            ThreadPool.QueueUserWorkItem(new WaitCallback(tps.Task1), stObj1);
            //Queue another task
            ThreadPool.QueueUserWorkItem(new WaitCallback(tps.Task2), stObj2);

            Console.Read();
        }

  我们也可以使用ThreadPool.RegisterWaitForSingleObject() 方法来运行有等待操作的任务,这种情况下需要将任务的等待操作传递给WaitHandle. WaitHandle 通知包装到一个WaitOrTimerCallback 委托的方法。在这种情况下,线程池创建一个后台线程调用回调方法。下面的代码(RegWait.cs)描述了这个概念:

/*************************************
/* Copyright (c) 2012 Daniel Dong
 * 
 * Author:Daniel Dong
 * Blog:  www.cnblogs.com/danielWise
 * Email: guofoo@163.com
 * 
 */

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

namespace RegWait
{
    public class RegWait
    {
        private static int i = 0;

        static void Main(string[] args)
        {
            AutoResetEvent arev = new AutoResetEvent(false);
            ThreadPool.RegisterWaitForSingleObject(
                arev, new WaitOrTimerCallback(WorkItem), null, 2000, false);
            arev.Set();

            Console.Read();
        }

        public static void WorkItem(object o, bool signaled)
        {
            i += 1;
            Console.WriteLine("Thread Pool Work Item Invoked: " + i.ToString());
        }
    }
}

上面例子的输入结果与下面类似:

2012-4-9 22-06-17

  每个两秒会在控制台打印一行新的结果并输出递增变量i 的值,直到用户输入回车键调用Console.Read() 方法退出为止。

  程序一开始会创建一个名为arec 初始值为non-signaled 的AutoResetEvent 对象来通知线程池执行任务组件:

AutoResetEvent arev = new AutoResetEvent(false);

  我们调用RegisterWaitForSingleObject() 方法,参数State 值为null, timeout 值为2000毫秒,executeOnceOnly 为false. RegisterWaitForSingleObject() 注册一个委托并在指定的时间间隔通知工作组件。在我们的例子中,时间间隔设置为两秒,如下代码所示:

ThreadPool.RegisterWaitForSingleObject(
    arev, new WaitOrTimerCallback(WorkItem), null, 2000, false);

  为了触发事件我们需要使用AutoResetEvent对象的Set() 方法:

arev.Set();

  这个例子包含了在C# 程序中如何使用线程池的部分内容;下一部分我们将检查线程池的可扩展性并创建一个线程池管理应用程序。

 

下一篇介绍一个多线程的微软消息队列(MSMQ)监听器…