c#网络编程实战笔记2-线程管理
1. 概述与概念
什么是线程?
线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。
什么是多线程?
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
多线程的好处:
可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率,当然也可以提升用户体验。多线程的典型应用就是当从数据库中读取大量数据时,会造成界面假死,用户无法操作界面上的其他内容。而使用多线程就可以解决这个问题。
多线程的不利方面:
线程也是程序,所以线程需要占用内存,线程越多占用内存也越多;
多线程需要协调和管理,所以需要CPU时间跟踪线程;
线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;
线程太多会导致控制太复杂,最终可能造成很多Bug;
注意:任何程序在执行时,至少有一个主线程。
一个程序从main开始之后,进程启动,为整个程序提供各种资源,而此时将启动一个线程,这个线程就是主线程,它将调度资源,进行具体的操作。Thread开启的线程是主线程下的子线程,是父子关系,此时该程序即为多线程的,这些线程共同进行资源的调度和执行。
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 Thread t = new Thread(WriteY); 10 t.Start(); 11 for (int i = 0; i < 1000;i++ ) 12 { 13 Console.Write("x"); 14 } 15 } 16 static void WriteY() 17 { 18 for (int i = 0; i < 1000; i++) 19 { 20 Console.Write("y"); 21 } 22 } 23 } 24 }
主线程创建了一个新线程“t”,它运行了一个重复打印字母"y"的方法,同时主线程重复打印字母“x”。
Thread类有几个至关重要的方法,描述如下:
Start():启动线程;
Sleep(int):静态方法,暂停当前线程指定的毫秒数;
Abort():通常使用该方法来终止一个线程;
Suspend():该方法并不终止未完成的线程,它仅仅挂起线程,以后还可恢复;
Resume():恢复被Suspend()方法挂起的线程的执行;
用通俗易懂的话来说,多线程可以让计算机"同时"做多件事情,节约时间。多线程可以让一个程序“同时”处理多个事情。
一旦线程开始,线程的IsAlive属性返回true,直到该线程结束,当传递给线程的构造函数的委托完成执行后,该线程就结束了,一旦结束,线程是不能重新开始的。
CLR分配每个线程到它自己的内存堆栈上,来保证局部变量的分离运行。在接下来的方法中我们定义了一个局部变量,然后在主线程和新创建的线程上同时地调用这个方法。
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 new Thread(Go).Start(); 10 Go(); 11 } 12 static void Go() 13 { 14 for (int cycles = 0; cycles < 5; cycles++) 15 { 16 Console.Write('?'); 17 } 18 } 19 } 20 }
变量cycles的副本分别在各自的内存堆栈中创建,输出也一样,可预见,会有10个问号输出。
当线程们引用了一些公用的目标实例的时候,他们会共享数据。
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 bool done; 8 static void Main(string[] args) 9 { 10 Program pro = new Program(); 11 new Thread(pro.Go).Start(); 12 pro.Go(); 13 } 14 void Go() 15 { 16 if (!done) 17 { 18 done = true; 19 Console.WriteLine("Done"); 20 } 21 } 22 } 23 }
因为在相同的Program实例中,两个线程都调用了Go(),它们共享了done字段,这个结果输出的是一个"Done",而不是两个。
静态字段提供了另一种在线程间共享数据的方式,下面是一个以done为静态字段的例子
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static bool done; 8 static void Main(string[] args) 9 { 10 new Thread(Go).Start(); 11 Go(); 12 } 13 static void Go() 14 { 15 if (!done) 16 { 17 done = true; 18 Console.WriteLine("Done"); 19 } 20 } 21 } 22 }
上述两个例子(实例成员和静态变量)足以说明另一个关键概念,那就是线程安全(或反之,它的不足之处!),输出实际上是不确定的,"Done"可以被打印两次(虽然不大可能)。然而,如果我们在Go方法里调换指令的顺序, "Done"被打印两次的机会会大幅地上升:
1 static void Go() 2 { 3 if (!done) 4 { 5 Console.WriteLine("Done"); 6 done = true; 7 } 8 }
问题就是一个线程在判断if块的时候,正好另一个线程正在执行WriteLine语句——在它将done设置为true之前。
补救措施是当读写公共字段的时候,提供一个排他锁;C#提供了lock语句来达到这个目的
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static bool done; 8 static object locker = new object(); 9 static void Main(string[] args) 10 { 11 new Thread(Go).Start(); 12 Go(); 13 } 14 static void Go() 15 { 16 lock (locker) 17 { 18 if (!done) 19 { 20 Console.WriteLine("Done"); 21 done = true; 22 } 23 } 24 } 25 } 26 }
当两个线程争夺一个锁的时候(在这个例子里是locker),一个线程等待,或者说被阻止到那个锁变的可用。在这种情况下,就确保了在同一时刻只有一个线程能进入临界区,所以"Done"只被打印了1次。代码以如此方式在不确定的多线程环境中被叫做线程安全。
一个线程,一旦被阻止,它就不再消耗CPU的资源了。暂停或阻止是多线程的同步活动的本质特征。等待一个排它锁被释放是一个线程被阻止的原因,另一个原因是线程想要暂停一段时间。
你可以通过调用另一个线程的Join方法来等待这个线程结束。可以让并发行处理变成串行化
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 Thread t = new Thread(Go); 10 t.Start(); 11 t.Join(); 12 Console.WriteLine("Thread t has ended!"); 13 } 14 static void Go() 15 { 16 for (int i = 0; i < 1000; i++) 17 { 18 Console.Write("y"); 19 } 20 } 21 } 22 }
这个程序打印1000个“y”后紧接着打印“Thread t has ended!”,你可以在调用Join时传入一个超时时间,按毫秒或作为TimeSpan都可以,如果线程结束则返回true,如果超时则返回false(超时就不再等了)。
Thread.Sleep:暂停当前的线程一段时间:
1 Thread.Sleep(TimeSpan.FromHours(1));//1小时 2 Thread.Sleep(500);//500毫秒 3 Thread.Sleep(0);//会立刻放弃线程的当前时间片,自动地将CPU交付给其他线程。
当一个线程因Sleep或者Join而等待时,他会被阻塞并且不会消耗CPU资源。
Thread.Sleep(0) MSDN上的解释是挂起此线程能使其他等待线程执行。这样的解释容易导致误解,我们可以这样理解,其实是让当前线程挂起,使得其他线程可以和当前线程再次的抢占Cpu资源。
2. 创建和开始使用多线程
线程用Thread类来创建,通过委托来指明方法从哪里开始运行。调用Start方法后,线程开始运行,线程一直到它所调用的方法返回后结束。
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 Thread t = new Thread(new ThreadStart(Go)); 10 t.Start(); 11 Go(); 12 } 13 static void Go() 14 { 15 Console.WriteLine("Hello!"); 16 } 17 } 18 }
在这个例子中,线程t执行Go()方法,大约与此同时主线程也调用了Go(),结果是两个几乎同时hello被打印出来。
ThreadStart委托是系统自带的委托,定义如下:
public delegate void ThreadStart();
一个线程可以通过C#堆委托简短的语法更便利地创建出来,不再需要使用ThreadStart,在这种情况,ThreadStart被编译器自动推断出来。
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 Thread t = new Thread(Go); 10 t.Start(); 11 Go(); 12 } 13 static void Go() 14 { 15 Console.WriteLine("Hello!"); 16 } 17 } 18 }
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 Thread t = new Thread( 10 delegate() 11 { 12 Console.WriteLine("Hello!"); 13 } 14 ); 15 t.Start(); 16 } 17 } 18 }
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 Thread t = new Thread(() => Console.WriteLine("Hello!")); 10 t.Start(); 11 } 12 } 13 }
将数据传入委托中
在上面的例子里,我们想更好地区分开每个线程的输出结果,让其中一个线程输出大写字母。我们传入一个状态字到Go中来完成整个任务,但我们不能使用ThreadStart委托,因为它不接受参数,所幸的是,.NET framework定义了另一个版本的委托叫做ParameterizedThreadStart,它可以接收一个单独的object类型参数。
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 Thread t = new Thread(Go); 10 t.Start(true); 11 Go(false); 12 } 13 static void Go(object upperCase) 14 { 15 bool upper = (bool)upperCase; 16 Console.WriteLine(upper?"HELLO!":"hello"); 17 } 18 } 19 }
ParameterizedThreadStart委托是系统自带的委托,定义如下:
Public delegate void ParameterizedThreadStart(object obj);
在整个例子中,编译器自动推断出ParameterizedThreadStart委托,因为Go方法接收一个单独的object参数。
ParameterizedThreadStart的特性是在使用之前我们必须对我们想要的类型(这里是bool)进行装箱操作,并且它只能接受一个参数。一个代替方案是使用一个匿名方法调用一个普通的方法。
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 bool u = true; 10 Thread t = new Thread( 11 delegate() 12 { 13 Go(u); 14 } 15 ); 16 t.Start(); 17 u = false; 18 Go(u); 19 } 20 static void Go(bool upper) 21 { 22 Console.WriteLine(upper ? "HELLO!" : "hello"); 23 } 24 } 25 }
优点是目标方法(这里是Go),可以接收任意数量的参数,并且没有装箱操作。不过这需要将一个外部变量放入到匿名方法中。
匿名方法产生了一种怪异的现象,当外部变量被后来的部分修改了值的时候,可能会透过外部变量进行无意的互动,比如上面的程序会输出2个”hello”。有意的互动(通常通过字段)被认为是足够了!一旦线程开始运行了,外部变量最好被处理成只读的——除非有人愿意使用适当的锁。
另一种较常见的方式是将对象实例的方法而不是静态方法传入到线程中,对象实例的属性可以告诉线程要做什么。
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 bool u; 8 static void Main(string[] args) 9 { 10 Program pr1 = new Program(); 11 pr1.u = true; 12 Thread t1 = new Thread(pr1.Go); 13 t1.Start(); 14 Program pr2 = new Program(); 15 pr2.Go(); 16 } 17 void Go() 18 { 19 Console.WriteLine(u ? "HELLO!" : "hello"); 20 } 21 } 22 }
Lambda expressions和被捕获的变量
正如我们看到的,lambda expressions是传递数据到线程的最有效的方式,然而,你必须注意线程开始后意外修改被捕获的变量,因为这些变量是共享的。
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 for (int i = 0; i < 10; i++) 10 { 11 new Thread(()=>Console.Write(i)).Start(); 12 } 13 } 14 } 15 }
输出是不确定的。如下所示:
0223557799
问题在于在整个循环生命周期,变量i关联同一个内存地址,因此,每个线程在调用Console.Write方法时,引用变量的值在一直发生变化。
解决方案是使用一个临时变量。
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 for (int i = 0; i < 10; i++) 10 { 11 int temp = i; 12 new Thread(()=>Console.Write(temp)).Start(); 13 } 14 } 15 } 16 }
输出结果为:
0235461789
(0到9,每个数字都随机顺序显示出来)
补充:用lambda expressions创建无参线程和有参线程
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main() 8 { 9 Thread t1 = new Thread( 10 () => Console.WriteLine("runing in a thread,id:{0}", Thread.CurrentThread.ManagedThreadId) 11 ); 12 t1.Start(); 13 Console.WriteLine("This is a main thread,id:{0}", Thread.CurrentThread.ManagedThreadId); 14 Console.Read(); 15 } 16 } 17 }
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 public struct Data 8 { 9 public string Message; 10 } 11 static void Main() 12 { 13 Data d=new Data(); 14 d.Message="Info"; 15 Thread t1=new Thread( 16 (object obj)=>{ 17 Data data=(Data)obj; 18 Console.WriteLine("running in a thread,id:{0},Data:{1}",Thread.CurrentThread.ManagedThreadId,data.Message); 19 } 20 ); 21 t1.Start(d); 22 Console.WriteLine("This is a main thread,id:{0}",Thread.CurrentThread.ManagedThreadId); 23 Console.Read(); 24 } 25 } 26 }
终止线程
线程启动后,当不需要某个线程继续执行的时候,有两种终止线程的方法。
第一种方法是事先设置一个布尔型的字段,在其他线程中通过修改该布尔值表示是否需要终止该线程,在该线程中,循环判断该布尔值,以确定是否退出线程,这是结束线程比较好的方法,实际应用中一般使用这种方法。
第二种方法是调用Thread类的Abort方法,该方法的最终效果是强行终止线程。例如:
1 Thread t=new Thread(方法名); 2 //... 3 t.Abort();
使用Abort方法终止线程时,线程实际上不一定会立即结束。这是因为系统在结束线程前要进行代码清理等工作,这种机制可以使线程的终止比较安全,但清理代码需要一段时间,而我们并不知道这个工作将占用多长时间。因此,调用了线程的Abort方法后,如果系统自动清理代码的工作没有结束,可能会出现类似死机一样的假象。为了解决这个问题,可以在主线程中调用子线程的Join方法,并在Join方法中指定主线程等待子线程结束的等待时间。
Interrupt和Abort:这两个关键字都是用来强制终止线程,不过两者还是有区别的。
1.
Interrupt: 抛出的是 ThreadInterruptedException 异常。
Abort: 抛出的是 ThreadAbortException 异常。
2.
Interrupt:如果终止工作线程,只能管到一次,工作线程的下一次sleep就管不到了,相当于一个contine操作。
Abort:这个就是相当于一个break操作,工作线程彻底死掉。
1 using System; 2 using System.Threading; 3 using System.Diagnostics; 4 namespace Test 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 Thread t = new Thread(new ThreadStart(Run)); 11 12 t.Start(); 13 14 //阻止动作 15 t.Interrupt(); 16 17 Console.Read(); 18 } 19 20 static void Run() 21 { 22 for (int i = 1; i <= 3; i++) 23 { 24 Stopwatch watch = new Stopwatch(); 25 26 try 27 { 28 watch.Start(); 29 Thread.Sleep(2000); 30 watch.Stop(); 31 32 Console.WriteLine("第{0}延迟执行:{1}ms", i, watch.ElapsedMilliseconds); 33 } 34 catch (ThreadInterruptedException e) 35 { 36 Console.WriteLine("第{0}延迟执行:{1}ms,不过抛出异常", i, watch.ElapsedMilliseconds); 37 } 38 } 39 } 40 } 41 }
1 using System; 2 using System.Threading; 3 using System.Diagnostics; 4 namespace Test 5 { 6 class Program 7 { 8 static void Main(string[] args) 9 { 10 Thread t = new Thread(new ThreadStart(Run)); 11 12 t.Start(); 13 14 //阻止动作 15 t.Abort(); 16 17 Console.Read(); 18 } 19 20 static void Run() 21 { 22 for (int i = 1; i <= 3; i++) 23 { 24 Stopwatch watch = new Stopwatch(); 25 26 try 27 { 28 watch.Start(); 29 Thread.Sleep(2000); 30 watch.Stop(); 31 32 Console.WriteLine("第{0}延迟执行:{1}ms", i, watch.ElapsedMilliseconds); 33 } 34 catch (ThreadInterruptedException e) 35 { 36 Console.WriteLine("第{0}延迟执行:{1}ms,不过抛出异常", i, watch.ElapsedMilliseconds); 37 } 38 } 39 } 40 } 41 }
Volatile关键字
终止线程最常用的方法是在线程中声明一个布尔型的字段,在其他线程中通过修改该字段的值作为传递给该线程是否需要终止的判断条件。为了达到这个目的,我们还需要学习一个新的关键字,即C#语言提供的volatile修饰符。
volatile修饰符表示所声明的字段可以被多个并发执行的线程修改。如果某个字段声明包含volatile关键字,则该字段将不再被编译器优化。这样,可以确保该字段在任何时间呈现的都是最新的值。
对于由多个线程访问的字段,而且该字段没有用lock语句对访问进行序列化,声明字段时应该使用volatile修饰符。
volatile修饰符只能包含在类和结构的字段声明中,不能将局部变量声明为volatile。
在一个线程中访问另一个线程的控件
在默认情况下,.NET Framework不允许在一个线程中直接访问另一个线程中的控件,这是因为如果有两个或多个线程同时访问某一控件,则可能会迫使该控件进入一种不确定的状态。甚至可能出现不同线程争用控件引起的死锁问题。
但是,为了在窗体上显示线程中处理的信息,我们可能经常需要在一个线程中访问另一个线程中的控件。有两种办法可以实现这个功能,一种是使用委托(Delegate)和事件(Event)来完成这个工作,另一种是利用BackgroundWorker组件实现这个功能。
为了让不是创建控件的线程共享该控件对象,Windows应用程序中的每一个控件都提供一个Invoke方法,该方法利用委托实现其他线程对该控件的操作。具体用法是,首先查询控件的InvokeRequired属性的值,如果该属性返回true,说明访问该控件的线程不是当前线程,此时就利用委托来访问控件,否则直接访问控件。例如:
1 private delegate void AddMessageDelegate(string message); 2 public void AddMessage(string message) 3 { 4 if(richTextBox1.InvokeRequired) 5 { 6 AddMessageDelegate d=AddMessage; 7 richTextBox1.Invoke(d,message); 8 } 9 else 10 { 11 richTextBox1.AppendText(message); 12 } 13 }
BackgroundWorker组件
在System.ComponentModel命名空间中,有一个BackgroundWorker类,该类允许我们在单独的专用线程上执行操作。有些耗时的操作,比如下载和数据库事务,在长时间运行时可能会导致用户界面似乎处于停止响应的状态。如果需要及时响应的用户界面,而且面临与这类操作相关的长时间延迟,则使用BackgroundWorker组件最合适。
若要在后台执行耗时的操作,可以创建一个BackgroundWorker,侦听那些报告操作进度并在操作完成时发出信号的事件。可以通过编程方式创建BackgroundWorker,也可以将它从“工具箱”的“组件”选项卡中拖到窗体上。
表 BackgroundWorker组件的常用成员
名称 | 说明 |
CancellationPending属性 | 获取一个值,指示应用程序是否已请求取消后台操作 |
IsBusy属性 | 获取一个值,指示BackgroundWorker是否正在运行异步操作 |
WorkerReportsProgress属性 | 获取或设置一个值,该值指示BackgroundWorker能否报告进度更新 |
WorkerSupportsCancellation属性 | 获取或设置一个值,该值指示BackgroundWorker是否支持异步取消 |
CancelAsync方法 | 请求取消挂起的后台操作 |
ReportProgress方法 | 引发ProgressChanged事件 |
RunWorkerAsync方法 | 开始执行后台操作 |
DoWork事件 | 调用RunWorkerAsync时发生 |
ProgressChanged事件 | 调用ReportProgress时发生 |
RunWorkerCompleted事件 | 当后台操作已完成、被取消或引发异常时发生 |
举例:利用BackgroundWorker组件执行后台线程。后台线程不停地产生10000以内的随机整数,同时判断产生的这个随机数是否能被5整除,如果该数能被5整除,在主窗体的RichTextBox中显示这个数。操作界面可以启动后台线程,也可以停止后台线程。
1 using System; 2 using System.ComponentModel; 3 using System.Windows.Forms; 4 using System.Net; 5 using System.Threading; 6 7 namespace BackgroundWorkerExample 8 { 9 public partial class Form1 : Form 10 { 11 public Form1() 12 { 13 InitializeComponent(); 14 //指示BackgroundWorker能否报告进度更新 15 backgroundWorker1.WorkerReportsProgress = true; 16 //指示BackgroundWorker是否支持异步取消 17 backgroundWorker1.WorkerSupportsCancellation = true; 18 button2.Enabled = false; 19 } 20 21 private void button1_Click(object sender, EventArgs e) 22 { 23 richTextBox1.Text = "开始产生10000以内的随机数……\n\n"; 24 button1.Enabled = false; 25 button2.Enabled = true; 26 //在后台开始操作,触发backgroundWorker1_DoWork事件 27 backgroundWorker1.RunWorkerAsync(); 28 } 29 30 private void button2_Click(object sender, EventArgs e) 31 { 32 //请求取消挂起的后台操作,触发backgroundWorker1_RunWorkerCompleted事件 33 backgroundWorker1.CancelAsync(); 34 button1.Enabled = true; 35 button2.Enabled = false; 36 } 37 /// <summary> 38 /// 调用RunWorkerAsync时发生的事件(操作开始时在另一个线程上运行的事件处理程序) 39 /// </summary> 40 /// <param name="sender"></param> 41 /// <param name="e"></param> 42 private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) 43 { 44 //不要直接使用组件实例名称(backgroundWorker1),因为有多个BackgroundWorker时, 45 //直接使用会产生耦合问题,应该通过下面的转换使用它 46 BackgroundWorker worker = sender as BackgroundWorker; 47 //下面的内容相当于线程要处理的内容。//注意:不要在此事件中和界面控件打交道 48 Random r = new Random(); 49 int numCount = 0; 50 //获取一个值,指示应用程序是否已请求取消后台操作 51 while (worker.CancellationPending == false) 52 { 53 int num = r.Next(10000); 54 if (num % 5 == 0) 55 { 56 numCount++; 57 //引发ProgressChanged事件,在ProgressChanged事件中与界面程序打交道 58 worker.ReportProgress(0,num); 59 Thread.Sleep(1000); 60 } 61 } 62 e.Result = numCount; 63 } 64 /// <summary> 65 /// 调用ReportProgress时发生的事件(当辅助线程指示某些操作已经进行时引发) 66 /// </summary> 67 /// <param name="sender"></param> 68 /// <param name="e"></param> 69 private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) 70 { 71 int num = (int)e.UserState; 72 richTextBox1.Text += num + " "; 73 } 74 /// <summary> 75 /// 当后台操作已完成、被取消或引发异常时发生的事件(当辅助线程完成(无论是成功、失败还是取消时引发)) 76 /// </summary> 77 /// <param name="sender"></param> 78 /// <param name="e"></param> 79 private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) 80 { 81 if (e.Error == null) 82 { 83 richTextBox1.Text += "\n\n操作停止,共产生" + e.Result + "个能被5整除的随机数"; 84 } 85 else 86 { 87 richTextBox1.Text += "\n操作过程中产生错误:"+e.Error; 88 } 89 } 90 //DoWork事件处理程序用于完成后台执行的具体操作,RunWorkerAsync方法用于启动该操作。 91 //ProgressChanged事件用于完成和前台界面相关的具体操作,ReportProgress方法用于引发 92 //该事件,并向前台界面传递相关的参数。 93 } 94 }
命名线程
线程可以通过它的Name属性进行命名,这非常有利于调试:可以用Console.WriteLine打印出线程的名字,Microsoft Visual Studio可以将线程的名字显示在调试工具栏的位置上。线程的名字可以在被任何时间设置——但只能设置一次,重命名会引发异常。
程序的主线程也可以被命名,下面例子里主线程通过CurrentThread命名:
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 Thread.CurrentThread.Name = "main"; 10 Thread worker = new Thread(Go); 11 worker.Name = "worker"; 12 worker.Start(); 13 Go(); 14 } 15 static void Go() 16 { 17 Console.WriteLine("Hello from "+Thread.CurrentThread.Name); 18 } 19 } 20 }
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 [STAThread]//指示应用程序的COM线程模式单元是单线程模式(STA) 8 static void Main(string[] args) 9 { 10 Thread.CurrentThread.Name="System Thread"; 11 Console.WriteLine(Thread.CurrentThread.Name+" 'Status':"+Thread.CurrentThread.ThreadState); 12 Console.ReadLine(); 13 } 14 } 15 }
前台和后台线程
线程默认为前台线程,这意味着任何前台线程在运行都会保持程序存活。C#也支持后台线程,当所有前台线程结束后,它们不维持程序的存活。
改变线程从前台到后台不会以任何方式改变它在CPU协调程序中的优先级和状态。
线程的IsBackground属性控制它的前后台状态。
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main() 8 { 9 Thread t1 = new Thread( 10 () => 11 { 12 Console.WriteLine("branch thread Start,id:{0}", Thread.CurrentThread.ManagedThreadId); 13 Thread.Sleep(3000); 14 Console.WriteLine("branch thread End"); 15 } 16 ); 17 t1.Name = "NewBKThread"; 18 t1.IsBackground = true;//Thread类默认创建的是前台线程,设定IsBackground属性可转为后台线程 19 t1.Start(); 20 Thread.Sleep(50);//为了使后台线程能打出branch thread Start,id 21 Console.WriteLine("This is a main thread,id:{0}", Thread.CurrentThread.ManagedThreadId); 22 } 23 //注意: 24 //当IsBackground为true时的输出为: 25 //branch thread Start,id:3 26 //This is a main thread,id:1 27 //当IsBackground为false时的输出为: 28 //branch thread Start,id:3 29 //This is a main thread,id:1 30 //branch thread End 31 } 32 }
线程优先级
线程的Priority 属性确定了线程相对于其它同一进程的活动的线程拥有多少执行时间,以下是级别:
enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest } ;
只有多个线程同时为活动时,优先级才有作用。
设置一个线程的优先级为高一些,并不意味着它能执行实时的工作,因为它受限于程序的进程的级别。要执行实时的工作,必须提升在System.Diagnostics 命名空间下Process的级别。
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
ProcessPriorityClass.High 其实是一个短暂缺口的过程中的最高优先级别:Realtime。设置进程级别到Realtime通知操作系统:你不想让你的进程被抢占了。如果你的程序进入一个偶然的死循环,可以预期,操作系统被锁住了,除了关机没有什么可以拯救你了!基于此,High大体上被认为最高的有用进程级别。
如果一个实时的程序有一个用户界面,提升进程的级别是不太好的,因为当用户界面UI过于复杂的时候,界面的更新耗费过多的CPU时间,拖慢了整台电脑。降低主线程的级别、提升进程的级别、确保实时线程不进行界面刷新,但这样并不能避免电脑越来越慢,因为操作系统仍会拨出过多的CPU给整个进程。最理想的方案是使实时工作和用户界面在不同的进程(拥有不同的优先级)运行,通过Remoting或共享内存方式进行通信,共享内存需要Win32 API中的 P/Invoking。(可以搜索看看CreateFileMapping 和 MapViewOfFile)
线程状态
通过属性Thread.ThreadState获取当前线程状态
运行Thread.Start()后,状态为Unstarted。
系统线程调度器选择了运行该线程后,状态为Running。
调用Thread.Sleep(),状态为WaitSleepJoin。
停止另一个线程,调用Thread.Abort()。接到中止命令的线程中会抛出ThreadAbortException。
涉及的状态有AbortRequested、Aborted。
继续停止的线程,调用Thread.ResetAbort()。线程将会在抛出ThreadAbortException后的语句后继续进行。
等待线程的结束,调用ThreadInstance.Join()。
该调用会停止当前线程,当前线程状态设为WaitSleepJoin。
等待加入的线程处理完成,再继续当前线程的处理。
异常处理
任何线程创建范围内try/catch/finally块,当线程开始执行便不再与其有任何关系。
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 try 10 { 11 new Thread(Go).Start(); 12 } 13 catch (Exception ex) 14 { 15 //不会在这得到异常 16 Console.WriteLine("Exception!"); 17 } 18 } 19 static void Go() 20 { 21 throw null; 22 } 23 } 24 }
这里try/catch语句一点用也没有,新创建的线程将引发NullReferenceException异常。当你考虑到每个线程有独立的执行路径的时候,便知道这行为是有道理的。
补救方法是在线程处理的方法内加入他们自己的异常处理。
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 new Thread(Go).Start(); 10 } 11 static void Go() 12 { 13 try 14 { 15 throw null;//这个异常在下面会被捕捉到 16 } 17 catch (Exception ex) 18 { 19 Console.WriteLine("Exception!"); 20 } 21 } 22 } 23 }
3. 锁机制
3.1 Monitor类
这个算是实现锁机制的纯正类,在锁定的临界区中只允许让一个线程访问,其他线程排队等待,主要整理为两组方法。
Monitor.Enter和Monitor.Exit
Lock确实减少了我们不必要的劳动并且让代码更可观,但是如果我们要精细的控制,则必须使用原生类,这里要注意一个问题就是“锁住什么”的问题,一般情况下我们锁住的都是静态对象,我们知道静态对象属于类级别,当有很多线程共同访问的时候,那个静态对象对多个线程来说是一个,不像实例字段会被认为是多个。
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 for(int i=0;i<10;i++) 10 { 11 Thread t=new Thread(Run); 12 t.Start(); 13 } 14 } 15 //资源 16 static object obj=new object(); 17 static int count=0; 18 static void Run() 19 { 20 Thread.Sleep(10); 21 Console.WriteLine("当前数字:{0}",++count); 22 } 23 } 24 }
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 for(int i=0;i<10;i++) 10 { 11 Thread t=new Thread(Run); 12 t.Start(); 13 } 14 } 15 //资源 16 static object obj=new object(); 17 static int count=0; 18 static void Run() 19 { 20 Thread.Sleep(10); 21 //进入临界区 22 Monitor.Enter(obj); 23 Console.WriteLine("当前数字:{0}",++count); 24 //退出临界区 25 Monitor.Exit(obj); 26 } 27 } 28 }
Monitor.Wait和Monitor.Pulse
首先这两个方法是成对出现,通常使用在Enter,Exit之间。
Wait:暂时的释放资源锁,然后该线程进入”等待队列“中,那么自然别的线程就能获取到资源锁。
Pulse:唤醒“等待队列”中的线程,那么当时被Wait的线程就重新获取到了锁。
这里我们是否注意到了两点:
1. 可能A线程进入到临界区后,需要B线程做一些初始化操作,然后A线程继续干剩下的事情。
2. 用上面的两个方法,我们可以实现线程间的彼此通信。
1 using System; 2 using System.Collections.Generic; 3 using System.Text; 4 using System.Threading; 5 namespace Test 6 { 7 public class Program 8 { 9 public static void Main(string[] args) 10 { 11 LockObj obj=new LockObj(); 12 //注意,这里使用的是同一个资源对象obj 13 Jack jack=new Jack(obj); 14 John john=new John(obj); 15 Thread t1=new Thread(new ThreadStart(jack.Run)); 16 Thread t2=new Thread(new ThreadStart(john.Run)); 17 t1.Start(); 18 t1.Name="Jack"; 19 t2.Start(); 20 t2.Name="John"; 21 Console.WriteLine(); 22 } 23 } 24 //锁定对象 25 public class LockObj 26 { 27 28 } 29 public class Jack 30 { 31 private LockObj obj; 32 public Jack(LockObj obj) 33 { 34 this.obj=obj; 35 } 36 public void Run() 37 { 38 Monitor.Enter(this.obj); 39 Console.WriteLine("{0}:我已进入茅房。",Thread.CurrentThread.Name); 40 Console.WriteLine("{0}:擦,太臭了,我还是撤了!",Thread.CurrentThread.Name); 41 //暂时的释放锁资源 42 Monitor.Wait(this.obj); 43 Console.WriteLine("{0}:兄弟说的对,我还是进去吧。",Thread.CurrentThread.Name); 44 //唤醒等待队列中的线程 45 Monitor.Pulse(this.obj); 46 Console.WriteLine("{0}:拉完了,真舒服。",Thread.CurrentThread.Name); 47 Monitor.Exit(this.obj); 48 } 49 } 50 public class John 51 { 52 private LockObj obj; 53 public John(LockObj obj) 54 { 55 this.obj=obj; 56 } 57 public void Run() 58 { 59 Monitor.Enter(this.obj); 60 Console.WriteLine("{0}:直奔茅厕,兄弟,你还是进来吧,小心憋坏了!",Thread.CurrentThread.Name); 61 //唤醒等待队列中的线程 62 Monitor.Pulse(this.obj); 63 Console.WriteLine("{0}:哗啦啦...",Thread.CurrentThread.Name); 64 //暂时的释放锁资源 65 Monitor.Wait(this.obj); 66 Console.WriteLine("{0}:拉完了,真舒服。",Thread.CurrentThread.Name); 67 Monitor.Exit(this.obj); 68 } 69 } 70 }
3.2 ReaderWriterLock类
Monitor实现的是在读写两种情况的临界区中只可以让一个线程访问,那么如果业务中存在”读取密集型“操作,就好比数据库一样,读取的操作永远比写入的操作多。针对这种情况,我们使用Monitor的话很吃亏,不过没关系,可以使用ReadWriterLock
,能够实现”写入串行“,”读取并行“。
ReaderWriteLock中主要用3组方法:
AcquireWriterLock:获取写入锁。
ReleaseWriterLock:释放写入锁。
AcquireReaderLock:获取读锁。
ReleaseReaderLock:释放读锁。
UpgradeToWriterLock:将读锁转为写锁。
DowngradeFromWriterLock:将写锁还原为读锁。
1 using System; 2 using System.Threading; 3 using System.Collections.Generic; 4 namespace test 5 { 6 class Program 7 { 8 static List<string> list = new List<string>(); 9 static ReaderWriterLock rw = new ReaderWriterLock(); 10 static void Main(string[] args) 11 { 12 Thread t1 = new Thread(AutoAddFunc); 13 Thread t2 = new Thread(AutoReadFunc); 14 t1.Start(); 15 t2.Start(); 16 Console.Read(); 17 } 18 public static void AutoAddFunc() 19 { 20 //18000ms插入一次 21 Timer timer1 = new Timer(new TimerCallback(Add), null, 0, 18000); 22 } 23 public static void AutoReadFunc() 24 { 25 //6000ms自动读取一次 26 Timer timer1 = new Timer(new TimerCallback(Read), null, 0, 6000); 27 Timer timer2 = new Timer(new TimerCallback(Read), null, 0, 6000); 28 Timer timer3 = new Timer(new TimerCallback(Read), null, 0, 6000); 29 } 30 public static void Add(object obj) 31 { 32 int num = new Random().Next(0, 1000); 33 //写锁 34 rw.AcquireWriterLock(TimeSpan.FromSeconds(30)); 35 list.Add(num.ToString()); 36 Console.WriteLine("我是线程{0},我插入的数据是{1}。", Thread.CurrentThread.ManagedThreadId, num); 37 //释放锁 38 rw.ReleaseWriterLock(); 39 } 40 public static void Read(object obj) 41 { 42 //读锁 43 rw.AcquireReaderLock(TimeSpan.FromSeconds(30)); 44 Console.WriteLine("我是线程{0},我读取的集合为:{1}", Thread.CurrentThread.ManagedThreadId, string.Join(",",list.ToArray())); 45 //释放锁 46 rw.ReleaseReaderLock(); 47 } 48 } 49 }
4. 线程池
如果你很懒,如果你的执行任务比较短,如果你不想对线程做更精细的控制,那么把这些繁琐的东西丢给线程池吧。
线程池是在后台执行多个任务的线程集合。一般在服务器端应用程序中使用线程池接受客户端传入的请求,每个传入请求都分配给线程池中的一个线程,从而达到异步处理请求的目的。
在服务器端应用程序中,如果每收到一个请求就创建一个新线程,然后在新线程中为其请求服务的话,将不可避免地造成系统开销的增大。实际上,创建太多的线程可能会导致由于过度使用系统资源而耗尽内存。为了防止资源不足,服务器端应用程序可以采用线程池来限制同一时刻处理的线程数目。
线程池不会占用主线程,也不会延迟后续请求的处理。一旦池中的某个线程用完规定的时间段,它将返回到等待线程队列中,等待被再次使用。这种重用使应用程序可以避免为每个任务创建新线程引起的资源和时间消耗。线程池有一个最大线程数限制。如果所有线程都繁忙,则额外的任务将放入队列中,直到有线程可用时才能够得到处理。一旦一项工作任务被加入到线程池的队列中,就不能取消该任务,直到该任务完成。
线程池适用于需要多个线程而实际执行时间又不多的场合,如有些经常处于等待状态的线程。当服务器应用程序接受大量短小线程的请求时,使用线程池技术是非常合适的,它可以大大减少创建和销毁线程的次数,从而提高服务器的工作效率。但是如果线程要求运行的时间比较长,由于线程的运行时间比现成的创建时间要长得多,仅靠减少线程的创建时间对系统效率的提高就不是那么明显了,此时就不合适使用线程池技术,而需要借助其他的技术来提高服务器的服务效率。
在System.Threading命名空间下,有一个ThreadPool类,该类提供对线程池的操作,如发送工作项、处理异步I/O、代表其他线程等待、处理计时器等。
每个进程都有一个线程池。线程池的默认大小为每个可用处理器有25个线程。
使用线程池的限制:
1.其中的所有线程只能是后台线程。
2.无法设置线程的优先级或名称。
3.适用于耗时较短的任务。长期运行的线程,应使用Thread类创建。
GetMaxThreads,GetMinThreads
获取线程池提供的线程数。
1 using System; 2 using System.Threading; 3 class Program 4 { 5 static void Main(string[] args) 6 { 7 int workerThreads; 8 int completePortsThreads; 9 ThreadPool.GetMaxThreads(out workerThreads,out completePortsThreads); 10 Console.WriteLine("线程池中最大的线程数{0},线程池中异步IO线程的最大数目{1}",workerThreads,completePortsThreads); 11 ThreadPool.GetMinThreads(out workerThreads,out completePortsThreads); 12 Console.WriteLine("线程池中最小的线程数{0},线程池中异步IO线程的最小数目{1}",workerThreads,completePortsThreads); 13 } 14 }
可以将1023设置为10230吗?那么就会有10230个线程帮我做好多事,其实不然。
1. 线程很多的话,线程调度就越频繁,可能就会出现某个任务执行的时间比线程调度花费的时间短很多的尴尬局面。
2. 一个线程默认占用1M的堆栈空间,如果10230个线程将会占用差不多10G的内存空间。
SetMaxTheads,SetMinThreads
当然,默认的线程设置只是一个参考,出于性能和实际情况,可以对其修改。
1 using System; 2 using System.Threading; 3 class Program 4 { 5 static void Main(string[] args) 6 { 7 int workerThreads; 8 int completePortsThreads; 9 ThreadPool.SetMaxThreads(100,50); 10 ThreadPool.SetMinThreads(20,10); 11 ThreadPool.GetMaxThreads(out workerThreads,out completePortsThreads); 12 Console.WriteLine("线程池中最大的线程数{0},线程池中异步IO线程的最大数目{1}\n",workerThreads,completePortsThreads); 13 ThreadPool.GetMinThreads(out workerThreads,out completePortsThreads); 14 Console.WriteLine("线程池中最小的线程数{0},线程池中异步IO线程的最小数目{1}\n",workerThreads,completePortsThreads); 15 } 16 }
QueueUserWorkItem
需要容纳任务并执行的方法,该方法有一个WaitCallBack的委托,我们只需要把将要执行的任务丢给委托,CLR将会在线程池中调派空闲的线程执行。
1 using System; 2 using System.Threading; 3 class Program 4 { 5 static void Main(string[] args) 6 { 7 ThreadPool.QueueUserWorkItem(Run1); 8 Console.Read(); 9 } 10 static void Run1(object obj) 11 { 12 Console.WriteLine("我是线程{0},我是线程池中的线程吗?\n回答:{1}",Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread); 13 } 14 }
可能需要像普通的Thread一样带一些参数到工作线程中,QueueUserWorkItem的第二个重载版本解决了该问题。
1 using System; 2 using System.Threading; 3 class Program 4 { 5 static void Main(string[] args) 6 { 7 ThreadPool.QueueUserWorkItem(Run1,"我是主线程"); 8 Console.Read(); 9 } 10 static void Run1(object obj) 11 { 12 Console.WriteLine("我是线程{0},我是线程池中的线程吗?\n回答:{1}",Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread.IsThreadPoolThread); 13 Console.WriteLine(obj); 14 } 15 }
RegisterWaitForSingleObject
如果把要执行的任务丢给线程池后,相当于把自己的命运寄托在别人的手上。
1. 再也不能控制线程的优先级了。
2. 丢给线程池后,再也不能将要执行的任务取消了。
不过RegisterWaitForSingleObject提供了一些简单的线程间交互,因为该方法的第一个参数是WaitHandle,在VS对象浏览器中,我们发现EventWaitHandle继承了WaitHandle,而ManualResetEvent和AutoResetEvent都继承于EventWaitHandle,也就是说我们可以在RegisterWaitForSingleObject溶于信号量的概念。
1 using System; 2 using System.Threading; 3 class Program 4 { 5 static void Main(string[] args) 6 { 7 AutoResetEvent ar=new AutoResetEvent(false); 8 ThreadPool.RegisterWaitForSingleObject(ar,Run1,null,Timeout.Infinite,false); 9 Console.WriteLine("时间:{0} 工作线程请注意,您需要等待5s才能执行。\n",DateTime.Now); 10 //5s 11 Thread.Sleep(5000); 12 ar.Set(); 13 Console.WriteLine("时间:{0} 工作线程已执行。\n",DateTime.Now); 14 Console.Read(); 15 } 16 static void Run1(object obj,bool sign) 17 { 18 Console.WriteLine("当前时间:{0} 我是线程{1}\n",DateTime.Now,Thread.CurrentThread.ManagedThreadId); 19 } 20 }
我们知道在Threading下面有一个Timer计时器,当定期触发任务的时候都是由线程池提供并给予执行,那么这里我们溶于信号量的概念以后同样可以实现计时器的功能。
1 using System; 2 using System.Threading; 3 class Program 4 { 5 static void Main(string[] args) 6 { 7 AutoResetEvent ar=new AutoResetEvent(false); 8 //参数2000:其实就是WaitOne(2000),采取超时机制 9 ThreadPool.RegisterWaitForSingleObject(ar,Run1,null,2000,false); 10 Console.Read(); 11 } 12 static void Run1(object obj,bool sign) 13 { 14 Console.WriteLine("当前时间:{0} 我是线程{1}\n",DateTime.Now,Thread.CurrentThread.ManagedThreadId); 15 } 16 }
有时候,跑着跑着,需要在某个时刻停止它,RegisterWaitForSingleObject返回一个RegisteredWaitHandle类,可以通过RegisteredWaitHandle来动态的控制,比如说停止计数器的运行。
1 using System; 2 using System.Threading; 3 class Program 4 { 5 static void Main(string[] args) 6 { 7 RegisteredWaitHandle handle=null; 8 AutoResetEvent ar=new AutoResetEvent(false); 9 //参数2000:其实就是WaitOne(2000),采取超时机制 10 handle=ThreadPool.RegisterWaitForSingleObject(ar,Run1,null,2000,false); 11 //10s后停止 12 Thread.Sleep(10000); 13 handle.Unregister(ar); 14 Console.WriteLine("主线程结束子线程了。"); 15 Console.Read(); 16 } 17 static void Run1(object obj,bool sign) 18 { 19 Console.WriteLine("当前时间:{0} 我是线程{1}\n",DateTime.Now,Thread.CurrentThread.ManagedThreadId); 20 } 21 }
5. 多线程生命周期
1 using System; 2 using System.Threading; 3 class Program 4 { 5 6 static void Main(string[] args) 7 { 8 Thread thread = new Thread(Go); 9 thread.Start(); 10 for (int i = 0; i < 10; i++) 11 { 12 Console.WriteLine("主线程 "+i); 13 } 14 15 } 16 17 static void Go() 18 { 19 for (int i = 0; i < 10; i++) 20 { 21 Console.WriteLine("子线程 "+i); 22 } 23 } 24 25 }
下面给出一个图来说说明它们为什么产生这种效果:
新建:运行Main方法(也就是创建线程,他是一个主线程),新建一个线程,调用start方法进入就绪状态
就绪状态:表示有权限获取cpu的时间片,就是获取cpu分配执行这个线程的时间(为什么相互切换,输出这个多些,输出那个少些,就是因为分配的时间长短不一样)
运行状态:就绪状态拿到cpu分配的时间片之后就进入运行状态,因为main线程分配的cpu时间片只有那么一点时间,运行状态方法没有执行完它的时间就用完了,从而进入阻塞状态
阻塞状态:再去请求cpu分配执行时间片。
消亡:线程执行完毕
6. 多线程案例
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 MyClass myClass=new MyClass(); 10 Thread thread=new Thread(new ThreadStart(myClass.MyThread1)); 11 thread.Start(); 12 Console.WriteLine(thread.ThreadState); 13 //注意这里用readline()不能用readkey否则不能出现效果 14 Console.ReadLine(); 15 } 16 } 17 class MyClass 18 { 19 public void MyThread1() 20 { 21 Console.WriteLine("大家好,我是线程1"); 22 } 23 } 24 //解释下上述代码,首先在MyClass类中定义一个MyThread1方法,该方法无参数无返回值。然后在Main方法中,通过Thread类创建出一个Thread对象thread,但是其构造函数中需要传入一个委托变量,所以通过new ThreadStart(myClass.MyThread1)创建了一个委托变量,接下就可以通过thread.Start()启动线程,这里需要注意的是,调用thread的Start方法后,线程并不是马上执行,而仅仅是被标记为该线程可以执行了,至于线程何时执行,需要听从cpu的调度。 25 }
1 using System; 2 using System.Threading; 3 namespace test 4 { 5 class Program 6 { 7 static void Main(string[] args) 8 { 9 Thread thread=new Thread(new ParameterizedThreadStart(ParameterRun)); 10 string[] strs={"王五","李四","张三"}; 11 thread.Start(strs); 12 Console.ReadLine(); 13 } 14 static void ParameterRun(object obj) 15 { 16 Console.WriteLine("我是带参数的线程方法"); 17 string[] arr=obj as string[]; 18 foreach(string s in arr) 19 { 20 Console.WriteLine(s); 21 } 22 } 23 } 24 //这个案例和案例1的唯一区别是创建Thread实例时需要一个带参数的委托变量作为构造函数的参数,而且符合委托规范的方法必须没有返回值,且只能有一个参数,并且参数类型是object的。ParameterRun方法参数赋值时,需要在thread.Start()中进行。 25 }
1 using System; 2 using System.Windows.Forms; 3 using System.Threading; 4 namespace test2 5 { 6 public partial class Form1 : Form 7 { 8 public Form1() 9 { 10 InitializeComponent(); 11 } 12 private Thread thread; 13 //摇奖机是否为启动状态 14 private bool isStart = false; 15 16 private void button1_Click(object sender, EventArgs e) 17 { 18 if (isStart) 19 { 20 button1.Text = "开始"; 21 isStart = false; 22 } 23 else 24 { 25 button1.Text = "停止"; 26 isStart = true; 27 //单独开启一个线程摇号,避免主线程假死 28 thread = new Thread(Show); 29 thread.Start(); 30 } 31 } 32 public void Show() 33 { 34 Random random = new Random(); 35 while (isStart) 36 { 37 label1.Text = random.Next(0, 10).ToString(); 38 label2.Text = random.Next(0, 10).ToString(); 39 label3.Text = random.Next(0, 10).ToString(); 40 //让当前线程睡一会儿 41 Thread.Sleep(100); 42 } 43 } 44 45 private void Form1_Load(object sender, EventArgs e) 46 { 47 //不检查控件的跨线程操作 48 Control.CheckForIllegalCrossThreadCalls = false; 49 } 50 51 private void Form1_FormClosing(object sender, FormClosingEventArgs e) 52 { 53 //在窗体关闭(主线程)前关闭子线程 54 if (thread != null) 55 { 56 thread.Abort(); 57 } 58 } 59 //写该程序时,需注意几点: 60 //1.在窗体的Load事件中设置不检查控件的跨线程操作 61 //2.在主窗体关闭前,结束子线程的执行 62 //3.为了避免随机数生活速度过快,使用Thread.Sleep(),让生成随机数的线程休息一段时间 63 } 64 }
7. 总结
线程是如何工作的
线程被一个线程协调程序管理着——一个CLR委托给操作系统的函数。线程协调程序确保将所有活动的线程被分配适当的执行时间;并且那些等待或阻止的线程——比如说在排它锁中、或在用户输入——都是不消耗CPU时间的。
在单核处理器的电脑中,线程协调程序完成一个时间片之后迅速地在活动的线程之间进行切换执行。这就导致“波涛汹涌”的行为,例如在第一个例子,每次重复的X 或 Y 块相当于分给线程的时间片。在Windows XP中时间片通常在10毫秒内选择要比CPU开销在处理线程切换的时候的消耗大的多。(即通常在几微秒区间)
在多核的电脑中,多线程被实现成混合时间片和真实的并发——不同的线程在不同的CPU上运行。这几乎可以肯定仍然会出现一些时间切片, 由于操作系统的需要服务自己的线程,以及一些其他的应用程序。
线程由于外部因素(比如时间片)被中断被称为被抢占,在大多数情况下,一个线程方面在被抢占的那一时那一刻就失去了对它的控制权。
线程 vs. 进程
属于一个单一的应用程序的所有的线程逻辑上被包含在一个进程中,进程指一个应用程序所运行的操作系统单元。
线程于进程有某些相似的地方:比如说进程通常以时间片方式与其它在电脑中运行的进程的方式与一个C#程序线程运行的方式大致相同。二者的关键区别在于进程彼此是完全隔绝的。线程与运行在相同程序其它线程共享(堆heap)内存,这就是线程为何如此有用:一个线程可以在后台读取数据,而另一个线程可以在前台展现已读取的数据。
何时使用多线程
多线程程序一般被用来在后台执行耗时的任务。主线程保持运行,并且工作线程做它的后台工作。对于Windows Forms程序来说,如果主线程试图执行冗长的操作,键盘和鼠标的操作会变的迟钝,程序也会失去响应。由于这个原因,应该在工作线程中运行一个耗时任务时添加一个工作线程,即使在主线程上有一个有好的提示“处理中...”,以防止工作无法继续。这就避免了程序出现由操作系统提示的“没有相应”,来诱使用户强制结束程序的进程而导致错误。模式对话框还允许实现“取消”功能,允许继续接收事件,而实际的任务已被工作线程完成。BackgroundWorker恰好可以辅助完成这一功能。
在没有用户界面的程序里,比如说Windows Service, 多线程在当一个任务有潜在的耗时,因为它在等待另台电脑的响应(比如一个应用服务器,数据库服务器,或者一个客户端)的实现特别有意义。用工作线程完成任务意味着主线程可以立即做其它的事情。
另一个多线程的用途是在方法中完成一个复杂的计算工作。这个方法会在多核的电脑上运行的更快,如果工作量被多个线程分开的话(使用Environment.ProcessorCount属性来侦测处理芯片的数量)。
一个C#程序称为多线程的可以通过2种方式:明确地创建和运行多线程,或者使用.NET framework的暗中使用了多线程的特性——比如BackgroundWorker类, 线程池,threading timer,远程服务器,或Web Services或ASP.NET程序。在后面的情况,人们别无选择,必须使用多线程;一个单线程的ASP.NET web server不是太酷,即使有这样的事情;幸运的是,应用服务器中多线程是相当普遍的;唯一值得关心的是提供适当锁机制的静态变量问题。
何时不要使用多线程
多线程也同样会带来缺点,最大的问题是它使程序变的过于复杂,拥有多线程本身并不复杂,复杂是的线程的交互作用,这带来了无论是否交互是否是有意的,都会带来较长的开发周期,以及带来间歇性和非重复性的bugs。因此,要么多线程的交互设计简单一些,要么就根本不使用多线程。除非你有强烈的重写和调试欲望。
当用户频繁地分配和切换线程时,多线程会带来增加资源和CPU的开销。在某些情况下,太多的I/O操作是非常棘手的,当只有一个或两个工作线程要比有众多的线程在相同时间执行任务块的多。稍后我们将实现生产者/耗费者 队列,它提供了上述功能。
更深入的讲解见:
http://www.cnblogs.com/ChrisChen3121/archive/2013/04/15/3021723.html#sec-1