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) 编辑 收藏 举报