什么是多线程?
最开始,线程只是用于分配单个处理器的处理时间的一种工具。但假如操作系统本身支持多个处理器,那么每个线程都可分配给一个不同的处理器,真正进入"并行运算"状态。
从程序设计语言的 角度看,多线程操作最有价值的特性之一就是程序员不必关心到底使用了多少个处理器。程序在逻辑意义上被分割为数个线程;假如机器本身安装了多个处理器,那 么程序会运行得更快,毋需作出任何特殊的调校。根据前面的论述,大家可能感觉线程处理非常简单。但必须注意一个问题:共享资源!如果有多个线程同时运行, 而且它们试图访问相同的资源,就会遇到一个问题。举个例子来说,两个线程不能将信息同时发送给一台打印机。为解决这个问题,对那些可共享的资源来说(比如打印机),它们在使用期间必须进入锁定状态。所以一个线程可将资源锁定,在完成了它的任务后,再解开(释放)这个锁,使其他线程可以接着使用同样的资源。
上面的对于线程间的访问,我们会在后面解释清楚,这里我摘要的概念,只是为了新学习的同学们有一个线程的大概概念,当然我们既然知道对于单核的计算机来说,CPU划分时间片给应用去执行程序,那么此时的线程一定是同步的吗,肯定不是同时执行的,只是CUP划分给不同线程间的时间片很短,给人感觉是同时执行的而已,当然,现在多核的电脑普及了,处理简单的线程是可以达到真正同步的效果。好了就不闲扯这些基本概念了,但是我们要知道进程,应用程序域,运行时宿主程序,线程。
这里就简要的提一下,应用程序域是CLR为应用程序提供隔离单元,一个进程可以有多个应用程序域,每一个应用程序域都可以加载一个应用程序,应用程序域通常由运行时宿主(例如:IE,window外壳程序,ASP.NET)创建,每个应用程序域都是由单个单个线程启动的,但是该应用程序域中的代码可以创建附加应用应用程序域和附加线程,之后就我们提到了多线程啦。。。。。。。
Thread类的一些常用方法
提到线程就必须提到Thread类,为了下面的演示我们在这里列举几个常见的方法和属性:
构造函数这个可是必看的,所以咋们先看这个:
1 public Thread( ParameterizedThreadStart start)
2 public Thread(ThreadStart start)
3 public Thread( ParameterizedThreadStart start, int maxStackSize)
4 public Thread( ThreadStart start, int maxStackSize)
然后当我们继续查看其参数的时候我们发现了
1 [ComVisibleAttribute(true)]
2 public delegate void ThreadStart()
3 [ComVisibleAttribute(false)]
4 public delegate void ParameterizedThreadStart( Object obj)
看到我们知道了线程构造函数需要的是两个无返回类型的委托,具体怎么用我们先介绍完几个常用的方法就开始测试.
Start () 导致操作系统将当前实例的状态更改为 ThreadState .Running 。
Join () 在继续执行标准的 COM 和 SendMessage 消息泵处理期间,阻塞调用线程,直到某个线程终止为止。
Sleep(Int32) 将当前线程挂起指定的时间。
VolatileRead(Byte)读取字段值无论处理器的数目或处理器缓存的状态如何,该值都是由计算机的任何处理器写入的最新值。
VolatileWrite(Byte , Byte) 立即向字段写入一个值,以使该值对计算机中的所有处理器都可见。
Interrupt 中断处于 WaitSleepJoin 线程状态的线程。
现在开始我们的主要工作
1.无参数的构造函数
我们先来创建两个线程,来执行两个无参数构造的函数,这里提一下,就是上面所说的线程的构造函数可以简单地记成有参和无参的构造函数,这里我们使用无参的构造函数public Thread(ThreadStart start),我们来看下效果,首先提示下:现在的系统基本都是支持抢先多任务处理的,我们这里就直接上代码:
1 using System;
2 using System.Threading;
3
4 namespace 多线程
5 {
6 class Program
7 {
8 static void Main(string[] args)
9 {
10 Test t = new Test();
11 Thread h1 = new Thread(t.InstanceMethod);
12 Thread h2 = new Thread(new ThreadStart(Test.staticMethod));
13 h1.Start();
14 h2.Start();
15
16 Console.ReadKey();
17 }
18 }
19 class Test
20 {
21 public void InstanceMethod()
22 {
23
24 for (int i = 0; i < 8; i++)
25 {
26 Console.WriteLine(i+"========线程1========");
27 Thread.Sleep(1000);
28 }
29 }
30 public static void staticMethod()
31 {
32 for (int i = 0; i < 8; i++)
33 {
34 Console.WriteLine(i + "========线程2========");
35 Thread.Sleep(1000);
36 }
37 }
38 }
39 }
从你和我迟钝的感官我们可以看出,这两个线程貌似是同时执行的,但是如果我们不使用Sleep()短暂的挂起线程,我们甚至连眨眼都来不及线程就结束了,难道他们真是同时执行的吗(我的是四核的CPU),难道他们是分配到不同的CPU上执行了,多说废话无用,我们自己来测试,我想了个最垃圾的方法,就是记录线程执行时的毫秒数,修改代码如下:
1 using System;
2 using System.Threading;
3
4 namespace 多线程
5 {
6 class Program
7 {
8 static void Main(string[] args)
9 {
10 Test t = new Test();
11 Thread h1 = new Thread(t.InstanceMethod);
12 Thread h2 = new Thread(new ThreadStart(Test.staticMethod));
13 h1.Start();
14 h2.Start();
15 Console.ReadKey();
16 }
17 }
18 class Test
19 {
20 public void InstanceMethod()
21 {
22
23 for (int i = 0; i < 8; i++)
24 {
25 Console.WriteLine(i+"========线程1:当前所处毫秒数:{0}========",DateTime.UtcNow.Millisecond);
26 Thread.Sleep(1000);
27 }
28 }
29 public static void staticMethod()
30 {
31 for (int i = 0; i < 8; i++)
32 {
33 Console.WriteLine(i + "========线程2:当前所处毫秒数:{0}========", DateTime.UtcNow.Millisecond);
34 Thread.Sleep(1000);
35 }
36 }
37 }
38 }
执行效果如下:
我们发现,原来这都是有操作系统决定的,什么那个线程先启动,人家操作系统才是东道主,人家自己决定,于是我们学到了一个道理,人在屋檐下,怎能不低头!
2.有参数的构造函数
现在我们使用有参数的构造函数来创建线程 public Thread( ParameterizedThreadStart start),这些都很简单我们就直接上代码:
1 using System; 2 using System.Threading; 3 4 namespace 多线程1 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 Test t = new Test(); 11 Thread h1 = new Thread(new ParameterizedThreadStart(t.InstanceMethod)); 12 Thread h2 = new Thread(new ParameterizedThreadStart(Test.staticMethod)); 13 h1.Start(8); 14 h2.Start("执行静态方法"); 15 Console.ReadKey(); 16 } 17 } 18 class Test 19 { 20 public void InstanceMethod(object times) 21 { 22 23 for (int i = 0; i < (int)times; i++) 24 { 25 Console.WriteLine("{0}========执行实例方法========", i); 26 Thread.Sleep(1000); 27 } 28 } 29 public static void staticMethod(object str) 30 { 31 for (int i = 0; i < 8; i++) 32 { 33 Console.WriteLine("{0}========{1}========", i, str); 34 Thread.Sleep(1000); 35 } 36 } 37 } 38 }
我们注意下ParameterizedThreadStart的参数,是一个Object类型的参数,因此我们在委托调用的函数内部进行了转换。
运行效果和上面都是一样的,但是既然提到了ParameterizedThreadStart的参数,是一个Object类型的参数,也就意味着我们什么类型都可以传进去,这样就既不安全,我们怎么改造呢,这里就引入了我们的下一个话题。
3.创建实现2中的效果一个可靠的委托方法
方法就是我们将所需的执行函数,以及参数都独立的封装到一个单独的类中。在类的内部通过该类的构造函数给需要执行的函数传递所需要的值,这样我们就可以照样使用public Thread( ParameterizedThreadStart start)。
现在我们看代码:
1 using System; 2 using System.Threading; 3 4 namespace 多线程 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 Temp t = new Temp(8, "执行静态方法"); 11 Thread h1 = new Thread(new ThreadStart(t.InstanceMethod)); 12 Thread h2 = new Thread(new ThreadStart(Temp.staticMethod)); 13 h1.Start(); 14 h2.Start(); 15 Console.ReadKey(); 16 } 17 } 18 /// <summary> 19 /// 封装了线程所需的数据和执行的方法 20 /// </summary> 21 class Temp 22 { 23 private int times; 24 private static string str; 25 public Temp(int times, string content) 26 { 27 this.times = times; 28 str = content; 29 } 30 public void InstanceMethod() 31 { 32 int lineCount = 0; 33 for (int i = 0; i < times; i++) 34 { 35 Console.WriteLine("{0}========执行实例方法========", i); 36 lineCount++; 37 Thread.Sleep(50); 38 } 39 Console.WriteLine("实例方法输出:{0}行数据",lineCount); 40 } 41 public static void staticMethod() 42 { 43 int lineCount = 0; 44 for (int i = 0; i < 8; i++) 45 { 46 Console.WriteLine("{0}========{1}========", i, str); 47 Thread.Sleep(50); 48 lineCount++; 49 } 50 Console.WriteLine("静态方法输出:{0}行数据", lineCount); 51 } 52 } 53 }
4.使用匿名方法简化一下委托操作
上面的代码都很简单,我们完全可以这么写:
1 using System; 2 using System.Threading; 3 4 namespace 多线程 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 Thread h = new Thread(delegate() { 11 for (int i = 0; i < 8; i++) 12 { 13 Console.WriteLine("热爱生活!"); 14 } 15 }); 16 h.Start(); 17 Console.ReadKey(); 18 } 19 } 20 }
好了,到了这里大家应该对于线程有个初步认识了,当然微软提供了特定的api用来设置某个线程绑定到特定的cpu上.有了上面的经验,我们就来学习新的知识
阻塞与中断线程
在上面的例子中我多次用到了sleep方法,地球人都知道它是用来挂起或者使线程休眠的。当然如果我们传入的参数为TimeOut.Infinit,那么当前的线程就会永远的休眠,知道被中断或者终止为止。也就意味着该线程被永久阻塞了。
这样似乎也没有什么大不了的,但是问题出现了,当我们在另一个线程都对一个已经被阻塞的线程调用Thread.Interrupt方法,就会子啊被阻塞线程中引发ThreadInterruptedException的异常,同时将中断被阻塞的线程,从而使该线程摆脱阻塞。也就是说我们可以在捕获到异常后执行适当的操作可以继续执行线程,但是如果我们忽略了该异常,则在在捕获到异常将停止该异常。下面我们来演示一下效果:
1 using System;
2 using System.Threading;
3
4 namespace 线程阻塞测试
5 {
6 class Program
7 {
8 static void Main(string[] args)
9 {
10 Thread thread = new Thread(delegate() {
11 Console.WriteLine("线程休眠");
12 try
13 {
14 Thread.Sleep(Timeout.Infinite);//永久休眠
15 }
16 catch(ThreadInterruptedException ex)
17 {
18 Console.WriteLine(ex.Message);
19 for (int i = 0; i <= 8; i++)
20 {
21 Console.WriteLine("继续执行线程"+Thread.CurrentThread.Name);
22 }
23 Console.WriteLine("线程执行完成!");
24 }
25 });
26 thread.Name = "测试线程";
27 Console.WriteLine("启动线程" + Thread.CurrentThread.Name);
28 thread.Start();
29 thread.Interrupt();
30 Console.WriteLine("此处该线程已被中断");
31 Console.ReadKey();
32 }
33 }
34 }
效果:
终止线程
从上面的例子我们得知了线程中断的特点,但是不要把线程的中断和终止混淆,这完全是连个不同的状态,下面我们先上代码,再做分析:
1 using System;
2 using System.Threading;
3
4 namespace 终止线程
5 {
6 class Program
7 {
8 static void Main(string[] args)
9 {
10 Thread myThread = new Thread(delegate()
11 {
12 try
13 {
14 for (int i = 0; i < 100; i++)
15 {
16 Console.WriteLine("线程正在正常运行!");
17 Thread.Sleep(50);//这样可以执行2次循环
18 }
19 }
20 catch (ThreadAbortException e)
21 {
22 Console.WriteLine("捕获线程异常.");
23 Console.WriteLine("异常消息: {0}", e.Message);
24 Thread.ResetAbort();//防止再次引发该异常
25 }
26 Console.WriteLine("当前线程{0}状态{1},正在运行", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState.ToString());
27 Thread.Sleep(1000);
28 Console.WriteLine("模拟一些线程清理操作");
29 });
30 myThread.Start();
31 Thread.Sleep(100);//把时间片让给myThread
32 Console.WriteLine("终止当前线程");
33 myThread.Abort();
34 myThread.Join();
35 Console.WriteLine("线程终止");
36
37 Console.ReadKey();
38 }
39 }
40 }
运行效果如下:
从上面我们可以看出线程终止不会立即退出,我们还可以在try语句的catch和finally块中捕获该异常,再做一些清理工作,这里是清理而不是类似于线程中断后的继续执行,这个需要区分一下,其中上面代码中的Thread.ResetAbort();这一句是相当重要的,如果没有这一句,就会在catch块的结尾处重新引发一个异常,为了确定这个事实,我们对代码再做一些修改,把异常抛到外层去,做个小测试,之后再看有什么特点。
1 using System;
2 using System.Threading;
3
4 namespace 终止线程
5 {
6 class Program
7 {
8 static void Main(string[] args)
9 {
10 Thread myThread = new Thread(delegate()
11 {
12 try
13 {
14 try
15 {
16 for (int i = 0; i < 100; i++)
17 {
18 Console.WriteLine("线程正在正常运行!");
19 Thread.Sleep(50);//这样可以执行2次循环
20 }
21 }
22 catch (ThreadAbortException e)
23 {
24 Console.WriteLine("内层捕获线程异常.");
25 Console.WriteLine("内层异常消息: {0}", e.Message);
26 // Thread.ResetAbort();//防止再次引发该异常
27 }
28
29 Console.WriteLine("当前线程{0}状态{1},正在运行", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState.ToString());
30 Thread.Sleep(1000);
31 Console.WriteLine("模拟一些线程清理操作");
32 }
33 catch (ThreadAbortException e)
34 {
35 Console.WriteLine("外层捕获线程异常.");
36 Console.WriteLine("外层捕获异常消息: {0}", e.Message);
37 }
38 });
39 myThread.Start();
40 Thread.Sleep(100);//把时间片让给myThread
41 Console.WriteLine("终止当前线程");
42 myThread.Abort();
43 myThread.Join();
44 Console.WriteLine("线程终止");
45
46 Console.ReadKey();
47 }
48 }
49 }
运行效果如下:
可以看到,catch的结尾的确引发了一个异常,而且catch块之后的语句都没有执行。
当然我们还可以附加一个finally块,来处理之后的catch块之后的语句。
未完待续。。。