bluesky_lcj

导航

C#线程知识

一、线程概述

在操作系统中一个进程至少要包含一个线程,然后,在某些时候需要在同一个进程中同时执行多项任务,或是为了提供程序的性能,将要执行的任务分解成多个子任务执行。这就需要在同一个进程中开启多个线程。我们使用C#编写一个应用程序(控制台或桌面程序都可以),然后运行这个程序,并打开windows任务管理器,这时我们就会看到这个应用程序中所含有的线程数,如下图所示。

 

 

如果任务管理器没有“线程数”列,可以【查看】>【选择列】来显示“线程计数”列。从上图可以看出,几乎所有的进程都拥有两个以上的线程。从而可以看出,线程是提供应用程序性能的重要手段之一,尤其在多核CPU的机器上尤为明显。

二、用委托(Delegate)的BeginInvoke和EndInvoke方法操作线程

 

在C#中使用线程的方法很多,使用委托的BeginInvoke和EndInvoke方法就是其中之一。BeginInvoke方法可以使用线程异步地执行委托所指向的方法。然后通过EndInvoke方法获得方法的返回值(EndInvoke方法的返回值就是被调用方法的返回值),或是确定方法已经被成功调用。我们可以通过四种方法从EndInvoke方法来获得返回值。

三、直接使用EndInvoke方法来获得返回值

    当使用BeginInvoke异步调用方法时,如果方法未执行完,EndInvoke方法就会一直阻塞,直到被调用的方法执行完毕。如下面的代码所示:

 

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

namespace MyThread
{
    class Program
    {
        private static int newTask(int ms)
        {
            Console.WriteLine("任务开始");
            Thread.Sleep(ms);
            Random random = new Random();
            int n = random.Next(10000);
            Console.WriteLine("任务完成");
            return n;
        }

        private delegate int NewTaskDelegate(int ms);
            
       
        static void Main(string[] args)
        {
            NewTaskDelegate task = newTask;
            IAsyncResult asyncResult = task.BeginInvoke(2000, null, null);

            // EndInvoke方法将被阻塞2秒
            int result = task.EndInvoke(asyncResult);          
            Console.WriteLine(result);
        }
    }
}

    在运行上面的程序后,由于newTask方法通过Sleep延迟了2秒,因此,程序直到2秒后才输出最终结果(一个随机整数)。如果不调用EndInvoke方法,程序会立即退出,这是由于使用BeginInvoke创建的线程都是后台线程,这种线程一但所有的前台线程都退出后(其中主线程就是一个前台线程),不管后台线程是否执行完毕,都会结束线程,并退出程序。关于前台和后台线程的详细内容,将在后面的部分讲解。

    读者可以使用上面的程序做以下实验。首先在Main方法的开始部分加入如下代码:

Thread.Sleep(10000);

    以使Main方法延迟10秒钟再执行下面的代码,然后按Ctrl+F5运行程序,并打开企业管理器,观察当前程序的线程数,假设线程数是4,在10秒后,线程数会增至5,这是因为调用BeginInvoke方法时会建立一个线程来异步执行newTask方法,因此,线程会增加一个。

四、使用IAsyncResult asyncResult属性来判断异步调用是否完成

    虽然上面的方法可以很好地实现异步调用,但是当调用EndInvoke方法获得调用结果时,整个程序就象死了一样,这样做用户的感觉并不会太好,因此,我们可以使用asyncResult来判断异步调用是否完成,并显示一些提示信息。这样做可以增加用户体验。代码如下:

 

static void Main(string[] args)
{
    NewTaskDelegate task = newTask;
    IAsyncResult asyncResult = task.BeginInvoke(2000, null, null);
 
    while (!asyncResult.IsCompleted)
    {
        Console.Write("*");
        Thread.Sleep(100);
    }
    // 由于异步调用已经完成,因此, EndInvoke会立刻返回结果
    int result = task.EndInvoke(asyncResult);          
    Console.WriteLine(result);
}


    上面代码的执行结果如下图所示。

 

 

    由于是异步,所以“*”可能会在“任务开始”前输出,如上图所示。


五、使用WaitOne方法等待异步方法执行完成

    使用WaitOne方法是另外一种判断异步调用是否完成的方法。代码如下:

 

static void Main(string[] args)
{
    NewTaskDelegate task = newTask;
    IAsyncResult asyncResult = task.BeginInvoke(2000, null, null);

    while (!asyncResult.AsyncWaitHandle.WaitOne(100, false))
    {
         Console.Write("*");             
    }

    int result = task.EndInvoke(asyncResult);
    Console.WriteLine(result);
}

    WaitOne的第一个参数表示要等待的毫秒数,在指定时间之内,WaitOne方法将一直等待,直到异步调用完成,并发出通知,WaitOne方法才返回true。当等待指定时间之后,异步调用仍未完成,WaitOne方法返回false,如果指定时间为0,表示不等待,如果为-1,表示永远等待,直到异步调用完成。

六、使用回调方式返回结果

    上面介绍的几种方法实际上只相当于一种方法。这些方法虽然可以成功返回结果,也可以给用户一些提示,但在这个过程中,整个程序就象死了一样(如果读者在GUI程序中使用这些方法就会非常明显),要想在调用的过程中,程序仍然可以正常做其它的工作,就必须使用异步调用的方式。下面我们使用GUI程序来编写一个例子,代码如下:

 

private delegate int MyMethod();
private int method()
{
    Thread.Sleep(10000);
    return 100;
}
private void MethodCompleted(IAsyncResult asyncResult)
{
    if (asyncResult == null) return;
    textBox1.Text = (asyncResult.AsyncState as
    MyMethod).EndInvoke(asyncResult).ToString();
}

private void button1_Click(object sender, EventArgs e)
{

    MyMethod my = method;
    IAsyncResult asyncResult = my.BeginInvoke(MethodCompleted, my);
}

    要注意的是,这里使用了BeginInvoke方法的最后两个参数(如果被调用的方法含有参数的话,这些参数将作为BeginInvoke的前面一部分参数,如果没有参数,BeginInvoke就只有两个参数了)。第一个参数是回调方法委托类型,这个委托只有一个参数,就是IAsyncResult,如MethodCompleted方法所示。当method方法执行完后,系统会自动调用MethodCompleted方法。BeginInvoke的第二个参数需要向MethodCompleted方法中传递一些值,一般可以传递被调用方法的委托,如上面代码中的my。这个值可以使用IAsyncResult.AsyncState属性获得。

    由于上面的代码通过异步的方式访问的form上的一个textbox,因此,需要按ctrl+f5运行程序(不能直接按F5运行程序,否则无法在其他线程中访问这个textbox,关于如果在其他线程中访问GUI组件,并在后面的部分详细介绍)。并在form上放一些其他的可视控件,然在点击button1后,其它的控件仍然可以使用,就象什么事都没有发生过一样,在10秒后,在textbox1中将输出100。

Thread类的应用

 

二、 定义一个线程类


    我们可以将Thread类封装在一个MyThread类中,以使任何从MyThread继承的类都具有多线程能力。MyThread类的代码如下:

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace MyThread
{
   abstract class MyThread
    {
       Thread thread = null;

       abstract public void run();   

        public void start()
        {
            if (thread == null)
                thread = new Thread(run);
            thread.Start();
        }
    }
}

    可以用下面的代码来使用MyThread类。

 

class NewThread : MyThread
{
      override public void run()
      {
          Console.WriteLine("使用MyThread建立并运行线程");
      }
  }

  static void Main(string[] args)
  {

      NewThread nt = new NewThread();
      nt.start();
  }

     我们还可以利用MyThread来为线程传递任意复杂的参数。详细内容见下节。

 

三、     为线程传递参数

Thread类有一个带参数的委托类型的重载形式。这个委托的定义如下:

[ComVisibleAttribute(false)]

public delegate void ParameterizedThreadStart(Object obj)


这个Thread类的构造方法的定义如下:

public Thread(ParameterizedThreadStart start);

下面的代码使用了这个带参数的委托向线程传递一个字符串参数:

 

public static void myStaticParamThreadMethod(Object obj)
{
    Console.WriteLine(obj);
}

static void Main(string[] args)
{
      Thread thread = new Thread(myStaticParamThreadMethod);
      thread.Start("通过委托的参数传值");
}

要注意的是,如果使用的是不带参数的委托,不能使用带参数的Start方法运行线程,否则系统会抛出异常。但使用带参数的委托,可以使用thread.Start()来运行线程,这时所传递的参数值为null。

    也可以定义一个类来传递参数值,如下面的代码如下:

 

class MyData
{
    private String d1;
    private int d2;
    public MyData(String d1, int d2)
    {
          this.d1 = d1;
          this.d2 = d2;
    }
    public void threadMethod()
    {
          Console.WriteLine(d1);
          Console.WriteLine(d2);
    }
}

MyData myData = new MyData("abcd",1234);
Thread thread = new Thread(myData.threadMethod);
thread.Start();


    如果使用在第二节定义的MyThread类,传递参数会显示更简单,代码如下:

 

class NewThread : MyThread
{
    private String p1;
    private int p2;
    public NewThread(String p1, int p2)
    {
        this.p1 = p1;
        this.p2 = p2;
    }

    override public void run()
    {
        Console.WriteLine(p1);
        Console.WriteLine(p2);
    }
}

NewThread newThread = new NewThread("hello world", 4321);
newThread.start();

四、     前台和后台线程

    使用Thread建立的线程默认情况下是前台线程,在进程中,只要有一个前台线程未退出,进程就不会终止。主线程就是一个前台线程。而后台线程不管线程是否结束,只要所有的前台线程都退出(包括正常退出和异常退出)后,进程就会自动终止。一般后台线程用于处理时间较短的任务,如在一个Web服务器中可以利用后台线程来处理客户端发过来的请求信息。而前台线程一般用于处理需要长时间等待的任务,如在Web服务器中的监听客户端请求的程序,或是定时对某些系统资源进行扫描的程序。下面的代码演示了前台和后台线程的区别。

 

public static void myStaticThreadMethod()
{
    Thread.Sleep(3000);
}

Thread thread = new Thread(myStaticThreadMethod);
// thread.IsBackground = true;
thread.Start();

    如果运行上面的代码,程序会等待3秒后退出,如果将注释去掉,将thread设成后台线程,则程序会立即退出。

    要注意的是,必须在调用Start方法之前设置线程的类型,否则一但线程运行,将无法改变其类型。

    通过BeginXXX方法运行的线程都是后台线程。


五、   判断多个线程是否都结束的两种方法

确定所有线程是否都完成了工作的方法有很多,如可以采用类似于对象计数器的方法,所谓对象计数器,就是一个对象被引用一次,这个计数器就加1,销毁引用就减1,如果引用数为0,则垃圾搜集器就会对这些引用数为0的对象进行回收。

方法一:线程计数器

线程也可以采用计数器的方法,即为所有需要监视的线程设一个线程计数器,每开始一个线程,在线程的执行方法中为这个计数器加1,如果某个线程结束(在线程执行方法的最后为这个计数器减1),为这个计数器减1。然后再开始一个线程,按着一定的时间间隔来监视这个计数器,如是棕个计数器为0,说明所有的线程都结束了。当然,也可以不用这个监视线程,而在每一个工作线程的最后(在为计数器减1的代码的后面)来监视这个计数器,也就是说,每一个工作线程在退出之前,还要负责检测这个计数器。使用这种方法不要忘了同步这个计数器变量啊,否则会产生意想不到的后果。

方法二:使用Thread.join方法

join方法只有在线程结束时才继续执行下面的语句。可以对每一个线程调用它的join方法,但要注意,这个调用要在另一个线程里,而不要在主线程,否则程序会被阻塞的。

    个人感觉这种方法比较好。

    线程计数器方法演示:

 

    class ThreadCounter : MyThread
    {
        private static int count = 0;
        private int ms;
        private static void increment()
        {
            lock (typeof(ThreadCounter))  // 必须同步计数器
            {
                count++;
            }
        }
        private static void decrease()
        {
            lock (typeof(ThreadCounter))
            {
                count--;
            }
        }
        private static int getCount()
        {
            lock (typeof(ThreadCounter))
            {
                return count;
            }
        }
        public ThreadCounter(int ms)
        {
            this.ms = ms;
        }
        override public void run()
        {
            increment();
            Thread.Sleep(ms);
            Console.WriteLine(ms.ToString()+"毫秒任务结束");
            decrease();
            if (getCount() == 0)
                Console.WriteLine("所有任务结束");
        }
    }


ThreadCounter counter1 = new ThreadCounter(3000);
ThreadCounter counter2 = new ThreadCounter(5000);
ThreadCounter counter3 = new ThreadCounter(7000);

counter1.start();
counter2.start();
counter3.start();


    上面的代码虽然在大多数的时候可以正常工作,但却存在一个隐患,就是如果某个线程,假设是counter1,在运行后,由于某些原因,其他的线程并未运行,在这种情况下,在counter1运行完后,仍然可以显示出“所有任务结束”的提示信息,但是counter2和counter3还并未运行。为了消除这个隐患,可以将increment方法从run中移除,将其放到ThreadCounter的构造方法中,在这时,increment方法中的lock也可以去掉了。代码如:

        public ThreadCounter(int ms)
        {
            this.ms = ms;
            increment();
        }

    运行上面的程序后,将显示如图2的结果。

 

                                                                 图2


使用Thread.join方法演示

private static void threadMethod(Object obj)
{
    Thread.Sleep(Int32.Parse(obj.ToString()));
    Console.WriteLine(obj + "毫秒任务结束");
}
private static void joinAllThread(object obj)
{
    Thread[] threads = obj as Thread[];
    foreach (Thread t in threads)
        t.Join();
    Console.WriteLine("所有的线程结束");
}

static void Main(string[] args)
{
    Thread thread1 = new Thread(threadMethod);
    Thread thread2 = new Thread(threadMethod);
    Thread thread3 = new Thread(threadMethod);

     thread1.Start(3000);
     thread2.Start(5000);
     thread3.Start(7000);

     Thread joinThread = new Thread(joinAllThread);
     joinThread.Start(new Thread[] { thread1, thread2, thread3 });

}

如果设计一个服务器程序,每当处理用户请求时,都开始一个线程,将会在一定程序上消耗服务器的资源。为此,一个最好的解决方法就是在服务器启动之前,事先创建一些线程对象,然后,当处理客户端请求时,就从这些建好的线程中获得线程对象,并处理请求。保存这些线程对象的结构就叫做线程池。

    在C#中可以通过System.Threading.ThreadPool类来实现,在默认情况下,ThreadPool最大可建立500个工作线程和1000个I/O线程(根据机器CPU个数和.net framework版本的不同,这些数据可能会有变化)。下面是一个用C#从线程池获得线程的例子:


private static void execute(object state)
{
    Console.WriteLine(state);     
}
static void Main(string[] args)
{
 
    int workerThreads;
    int completionPortThreads;
        
    ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads);
    Console.WriteLine(workerThreads);
    Console.WriteLine(completionPortThreads);   
    ThreadPool.QueueUserWorkItem(execute,"线程1");   // 从线程池中得到一个线程,并运行execute
    ThreadPool.QueueUserWorkItem(execute, "线程2");
    ThreadPool.QueueUserWorkItem(execute, "线程3");
    Console.ReadLine();
}
    下图为上面代码的运行结果。

 


    要注意的是,使用ThreadPool获得的线程都是后台线程。

    下面的程序是我设计的一个下载文件服务器的例子。这个例子从ThreadPool获得线程,并处理相应的客户端请求。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Net.Sockets;
using System.IO;

namespace MyThread
{
    class FileServer
    {
        private String root;
        private Thread listenerThread;

        private void worker(object state)
        {
             TcpClient client = state as TcpClient;
             try
             {

                 client.ReceiveTimeout = 2000;
                 Stream stream = client.GetStream();
                 System.IO.StreamReader sr = new StreamReader(stream);
                 String line = sr.ReadLine();
                 String[] array = line.Split(' ');
                 String path = array[1].Replace('/', '\\');
                 String filename = root + path;
                 if (File.Exists(filename))  // 如果下载文件存在,开始下载这个文件
                 {
                     FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read,
                                                           FileShare.Read);
                     byte[] buffer = new byte[8192]; // 每次下载8K
                     int count = 0;
                     String responseHeader = "HTTP/1.1 200 OK\r\n" +
                                             "Content-Type:application/octet-stream\r\n" +
                                             "Content-Disposition:attachment;filename=" +
                                                   filename.Substring(filename.LastIndexOf("\\") + 1) + "\r\n\r\n";
                     byte[] header = ASCIIEncoding.ASCII.GetBytes(responseHeader);
                     stream.Write(header, 0, header.Length);
                     while ((count = fileStream.Read(buffer, 0, buffer.Count())) > 0)
                     {
                         stream.Write(buffer, 0, count);
                     }
                     Console.WriteLine(filename + "下载完成");
                 }
                 else  // 文件不存在,输出提示信息
                 {
                     String response = "HTTP/1.1 200 OK\r\nContent-Type:text/plain;charset=utf-8\r\n\r\n文件不存在";
                     byte[] buffer = ASCIIEncoding.UTF8.GetBytes(response);
                     stream.Write(buffer, 0, buffer.Length);
                 }

             }
             catch (Exception e)
             {
                 Console.WriteLine(e.Message);
             }
             finally
             {
                 if (client != null)
                 {
                     client.Close();
                 }
             }
        }

        private void listener()
        {
            TcpListener listener = new TcpListener(1234);
            listener.Start();  // 开始监听客户端请求
            TcpClient client = null;

            while (true)
            {
                client = listener.AcceptTcpClient();
                client.ReceiveTimeout =2000;
                ThreadPool.QueueUserWorkItem(worker, client);  // 从线程池中获得一个线程来处理客户端请求
            }
        }
        public FileServer(String root)
        {
            this.root= root;        
        }
        public void start()
        {
            listenerThread = new Thread(listener);
            listenerThread.Start();  // 开始运行监听线程
        }
    }
}
    FileServer类的使用方法:

    FileServer fs = new FileServer(“d:\\download”);

fs.start(); // 端口为1234

如果d:"download目录中有一个叫aa.exe的文件,在浏览器中输入如下的地址可下载:
    http://localhost:1234/aa.exe

 本文来自CSDN博客 :http://blog.csdn.net/yizhiduxiu11/archive/2009/01/19/3835923.aspx

posted on 2009-08-04 09:39  bluesky_lcj  阅读(291)  评论(0编辑  收藏  举报