线程可以处于一个或多个状态,由ThreadState枚举表示。使用Thread类中的一些方法后状态会随之变化。枚举成员如下:
线程的生存期如下:
一、线程睡眠
若线程想要访问的资源不可使用,只能期望隔段时间后,重新尝试讯问资源,这时就要让该线程睡眠等待,线程就会处WaitSleepJoin状态。
Imports System.Threading Public Class ThreadSleep Public Shared worker As Thread Public Shared worker2 As Thread Public Shared Sub Main() Console.WriteLine("Entering the Sub Main!") worker = New Thread(AddressOf Counter) worker2 = New Thread(AddressOf Counter2) worker2.Priority = ThreadPriority.Highest worker.Start() worker2.Start() Console.WriteLine("Exiting the Sub Main!") Console.ReadLine() End Sub Public Shared Sub Counter() Console.WriteLine("Entering Counter") For i As Integer = 1 To 50 Console.Write(i & " ") If i = 10 Then Console.WriteLine(" worker_线程暂停1秒...") worker.Sleep(1000) '等效于worker.Sleep(TimeSpan.FromSeconds(1)) Console.WriteLine("worker线程恢复:") End If Next Console.WriteLine() Console.WriteLine("Exiting Counter") End Sub Public Shared Sub Counter2() Console.WriteLine("Entering Countcr2") For i As Integer = 51 To 100 Console.Write(i & " ") If i = 70 Then Console.WriteLine(" worker2 线程暂停5秒...") worker2.Sleep(5000) Console.WriteLine("worker2线程恢复:") End If Next Console. WriteLine() Console.WriteLine("Exiting Counter2") End Sub End Class
说明:由于worker2线程有较高优先权,所以先执行,至70时暂停5秒;worker较低优先权随后执行,至10时暂停1秒,后继续输出,直到5秒后worder2睡醒,再输出。同时注意,由于线程的优先都是受操作系统调控安排,可以看到worker线程的1、2先输出,继而是worder2的51、52等输出,故优先权并不是绝对意义上的优先。
二、中断(唤醒)线程
线程睡眠时就进入WaitSleepJoin状态,有时有必要主动中断睡眠、唤醒这个线程,唯一就是使用lnterrupt()方法。这样线程就会从睡眠队列进行执行队列。
Imports System.Threading Public Class Interrupt Public Shared sleeper As Thread Public Shared worker As Thread Public Shared Sub Main() Console.WriteLine("Entering the Sub Main!") sleeper = New Thread(AddressOf SleepingThread) worker = New Thread(AddressOf AwakeTheThread) sleeper.Start() worker.Start() Console.WriteLine("Exiting the Sub Main!") Console.ReadLine() End Sub Public Shared Sub SleepingThread() For i As Integer = 1 To 50 Console.Write(i & " ") If i = 10 Or i = 20 Or i = 30 Then Console.WriteLine("Going to sleep at: " & i) Try 'ThreadInterruptedException 在中断的线程中引发,但要在该线程阻塞之后才引发。 sleeper.Sleep(30) '这里易抛异常,用Try Catch ex As Exception '原因见4.2(原因http://blog.csdn.net/wguoyong/article/details/50918322) End Try End If Next End Sub Public Shared Sub AwakeTheThread() Dim i As Integer For i = 51 To 100 Console.Write(i & " ") If sleeper.ThreadState = System.Threading.ThreadState.WaitSleepJoin Then Console.WriteLine("Interrupting the sleeping thread") sleeper.Interrupt() End If Next End Sub End Class
说明:这只是我们想象中让睡眠的线程中断唤醒,但实际上,中断唤醒并不是立即执行,加上线程是由操作系统在调配,更增加了不可控性。上面程序最易出现的就是抛出异常,由于延迟执行中断,中断可能出现在方法AwakeTheThread中任意一段代码中。
线程中睡眠、阻塞、挂起的区别(http://www.cnblogs.com/jason-liu-blogs/archive/2012/12/19/2825202.html)
注意:.net中线程的挂起Suspend方法与恢复Resume方法已经过时。
三、终止线程
在调用Thread.Abort方法的线程上引发 ThreadAbortException,以开始终止此线程的过程。调用此方法通常会终止线程。简单地说:它是以抛异常的方式达到终止线程的目的。所以Abort只是抛异常后将线程置为AbortRequested状态,此时并没有真正结束。
四、连接线程
Join( )方法阻塞线程,直到当前的线程终止。如果一个线程依赖于另一个线程,那么这个方法是很有用的。连接两个线程的意思是,当调用Join( )方法时,运行着的线程将进入WaitSleepJoin状态,而直到调用Join( )方法的方法完成了任务,线程才会返回到Running状态。对处于 ThreadState.Unstarted 状态的线程不能调用 Join。
使用此方法确保线程已终止。如果线程不终止,则调用方将无限期阻塞。如果调用 Join 时该线程已终止,此方法将立即返回。
Imports System.Threading Public Class JoiningThread Public Shared SecondThread As Thread Public Shared FirstThread As Thread Shared Sub First() '第一线程 For i As Integer = 1 To 50 Console.Write(i & " ") Next Console.WriteLine("First finish!") End Sub Shared Sub Second() '第二线程 FirstThread.Join() '连接到第一线程,只有第一线程终止后,第二线程才能接手运行 Console.WriteLine("Second start...") For i As Integer = 51 To 100 Console.Write(i & " ") Next End Sub Public Shared Sub Main() FirstThread = New Thread(AddressOf First) SecondThread = New Thread(AddressOf Second) FirstThread.Start() SecondThread.Start() Console.ReadLine() End Sub End Class
五、何时选择线程
尽管线程耗费内存(保存线程现场、指令代码)和耗费CPU,导致我们尽可能少用线程。但是,线程的确给我们带来了很多方便,
1.后台运行
程序中的一些费时功能(如计算、下载等)与UI同处一个线程时,会产生UI界面响应不过来、类似死机的假相,特别是大量的后台数据查询等,此时用线程正好。例:搜索文件。
Imports System.Threading Imports System.IO Public Class Form1 Dim searchTerm As String Dim totalFiles As Integer Dim blnSimple As Boolean Delegate Sub ClearText() Delegate Sub AddText(ByVal txt As String) Private Sub btnSimple_Click(sender As Object, e As EventArgs) Handles btnSimple.Click Search() blnSimple = True End Sub Public Sub Search() Invoke(New ClearText(AddressOf ClearList)) searchTerm = TextBox1.Text totalFiles = 0 SearchDirectory("F:\Tools") End Sub Public Sub SearchDirectory(ByVal Path As String) Dim di As New DirectoryInfo(Path) Dim f() As FileInfo = di.GetFiles(searchTerm) Dim myFile As FileInfo For Each myFile In f If ListBox1.InvokeRequired = True Then Invoke(New AddText(AddressOf AddList), myFile.FullName) End If Next Dim d() As DirectoryInfo = di.GetDirectories(searchTerm) Dim myDir As DirectoryInfo For Each myDir In d SearchDirectory(myDir.FullName) Next End Sub Private Sub btnMutli_Click(sender As Object, e As EventArgs) Handles btnMutli.Click Dim t As New Thread(AddressOf Search) blnSimple = False t.Start() End Sub Private Sub ClearList() ListBox1.Items.Clear() End Sub Private Sub AddList(ByVal it As String) ListBox1.Items.Add(it) End Sub End Class
说明:用SimpleThread按钮运行,明显有停滞出现;但用多线程MultiThread却没任何感觉,很流畅。另外由于其它线程无法直接操作控件:
如果使用多线程来提高 Windows 窗体应用程序的性能,则必须确保以线程安全方式调用控件。访问 Windows 窗体控件本质上不是线程安全的。 如果有两个或多个线程操作某一控件的状态,则可能会迫使该控件进入一种不一致的状态。 还可能会出现其他与线程相关的 Bug,例如争用情况和死锁。 确保以线程安全方式访问控件非常重要。
常用的方法来判断控件是否需要委托invoke执行,用Control.InvokeRequired 属性:
判断当前的线程是不是该窗体所在的线程,如果不是,需要Invoke到窗体线程去。当有一个非当前线程的线程访问的时候自动就为True了,在当前线程中访问一直是False的。
C#中禁止跨线程直接访问控件,InvokeRequired是为了解决这个问题而产生的,当一个控件的InvokeRequired属性值为真时,说明有一个创建它以外的线程想访问它。此时它将会在内部调用new MethodInvoker(LoadGlobalImage)来完成下面的步骤,这个做法保证了控件的安全,你可以这样理解,有人想找你借钱,他可以直接在你的钱包中拿,这样太不安全,因此必须让别人先要告诉你,你再从自己的钱包把钱拿出来借给别人,这样就安全了
2.外部资源
在访问外部资源(访问非本地系统上的资源)的时候。这可能是一个数据库进程或者是网络上的一个网络文件共享。网络性能可能对应用程序性能产生不利的影响。当用户界面花费时间取得数据及更新列表框时,用户界面会冻结。我们可以只通过创建一个新线程并在这个线程中执行数据库代码的方式,再次修正这种情况。
六、不适合创建线程
有些情况创建线程是非常不利的。
1. 再次访问的执行顺序
线程不会以你希望的默认顺序执行!
同样方法代码,尽管被不同的线程先后进行调用,但它们的执行顺序并不确定,先执行的线程并不一定最前,后执行的线程也并不一定最后结果。简言之,线程的执行顺序并不是视觉上确定的先后顺序。
Imports System.Threading Public Class ThreadOrder Shared tl As Thread Shared t2 As Thread Public Shared Sub WriteFinished(ByVal threadName As String) Select Case threadName Case "Tl" Console.WriteLine() Console.WriteLine("Tl Finished") Case "T2" Console.WriteLine() Console.WriteLine("T2 Finished”) End Select End Sub Public Shared Sub Main() tl = New Thread(AddressOf Increment) t2 = New Thread(AddressOf Increment) tl.Name = "Tl" t2.Name = "T2" tl.Start() '1、t1先执行 t2.Start() Console.ReadLine() End Sub Public Shared Sub Increment() For i As Long = 1 To 1000000 If i Mod 100000 = 0 Then Console.Write("{" + Thread.CurrentThread.Name + "}") End If Next WriteFinished(Thread.CurrentThread.Name) End Sub End Class
说明:尽管t1先执行,但并不一定它最先走进CPU。线程由于是操作系统来调配,顺序变得不可控。下面三次运行的结果都不相同,注意谁先显示,谁先结束。
这样看可能有点麻烦,上面代码不变,只是增加一个全局的计数器,把每次结果后接计数器,看一下3次输出结果:顺序与结束的混乱一下就看出来了。
2.循环中的线程
循环中依次产生的线程并不一定依次执行和依次完成,甚至产生阻塞或互锁。
Public Shared Sub SendAllEmail() For i As Integer = 0 To al.Count - 1 Dim t As Thread = LoopingThreads.CreateEmail(AddressOf Mailer.MailMethod, al(i), "[email protected]", "Threading in a loop", "Mail Example") t.Start() t.Join(Timeout.Infinite) Next End Sub
上面依次逐一发送邮件,但如果有一个发生不成功,会怎么样?另外看似是队列进入,但能不能到达CPU谁也没法说清,如果皇帝就是cpu,妃子就是线程,若妃子们都打扮好了(线程Start),就等待着皇帝宠幸了。但皇帝决定翻牌宠幸谁呢?皇帝可不管谁先打扮好(Start),他看中那个就宠幸谁先,这个是皇帝决定的。尽管皇家有祖制(线程先后轮询规则),但毕竟皇上主观性太大,无法确定谁先谁后。
一个通用的编程惯例是将工作放到一个队列中,由一个服务处理。例如,—家银行可能将一个基于XML的文件放到一个网络目录中,使得在另一个服务器上运行的服务程序获得这个文件。这个服务将浏览目录,査找新的文件,然后一次处理一个文件。 如果一次不只将一个文件放到目录中,那么服务将依次地处理文件。在一个典型的环境中,新文件将很少被放在这个目录中。基于这个信息,乍一看上去,查找到一个文件的时候好像是启动一个线程的好时机。您可能是对的,但是考虑—下,如果处理这些文件的服务程序停止了那么会发生什么呢。如果一个网络问题在很长时间里阻止服务访问这个目录,那么会发生什么呢?目录中的文件会堆积起来。当服务最后再次启动时,或者运行服务再次访问目录时,每一个文件基本上都会在服务上产生一个新线程。任何使用过这个模型的用户都可以告诉您这种情况可以使服务器停止运行。
如果您的工作被放到一个队列中,并且您觉得应该使用多线程, 您可以考虑使用线程池。