[转][.Net 线程处理系列]专题一:线程基础
引言:
最近一段时间都在研究关于.Net线程的内容,觉得线程是每个程序员都应该掌握的,所以写下这个线程的系列希望能给大家学习过程中一些帮助,同时也是自己对线程的巩固,当中如果有什么错漏还请大家指出,这样我们可以互相得到进步。
目录:
一、线程的介绍
二、线程调度和优先级
三、前台线程和后台线程
四、简单线程的使用
一、线程的介绍
在介绍线程之前, 很有必要知道什么是进程,以及与线程的关系。
进程(Process)是应用程序的实例要使用的资源的一个集合(从可以简化理解:进程就是一种资源,是应用程序所用的资源)。每个应用程序都在各自的进程中运行来确保应用程序不受其他应用程序的影响,如果一个应用程序失败了, 只会影响自己的进程,其他进程中的应用程序可以继续运行。进程是操作系统为我们提供的一种保护应用程序的一种机制。
线程是进程中基本执行单元, 一个进程中可以包含多个线程,在进程入口执行的第一个线程是一个进程的主线程,在.Net应用程序中,都是以Main()方法作为程序的入口的, 所以在程序运行过程中调用这个方法时,系统就会自动创建一个主线程。(他们之间的关系简单说:线程是进程的执行单元,进程是线程的一个容器了)。
二、线程调度和优先级
Windows之所以被称为抢占式多线程操作系统,是因为线程可以在任意时间被抢占,并调度另一个线程。每个线程都分配了从0~31的一个优先级。系统首先把高优先级的线程分配给CPU执行。Windows 支持7个相对线程优先级:Idle,Lowest,Below Normal,Normal,Above Normal,Highest和Time-Critical,Normal是默认的线程优先级,然而在程序中可以通过设置Thread的Priority属性来改变线程的优先级,它的类型为ThreadPriority枚举类型,包含枚举有:Lowest,BelowNormal,Normal,AboveNormal和Highest,CLR为自己保留了 Idle和Time-Critical优先级。具体每个枚举值含义如下表:
| 成员名称 | 说明 |
|---|---|
| Lowest | 可以将 Thread何其他优先级的线程之后。 |
| BelowNormal | 可以将 Thread Normal 优先级的线程之后,在具有 Lowest 优先级的线程之前。 |
| Normal |
可以将 Thread AboveNormal 优先级的线程之后,在具有 BelowNormal 优先级的线程之前。 默认情况下,线程具有 Normal 优先级。 |
| AboveNormal | 可以将 Thread Highest 优先级的线程之后,在具有 Normal 优先级的线程之前。 |
| Highest | 可以将 Thread 其他优先级的线程之前。 |
三、前台线程和后台线程
在.net中线程分为前台线程和后台线程,在一个进程中,当所有前台线程停止运行时,CLR会强制结束仍在运行的任何后台线程,这些后台线程直接被终止,不会抛出异常。
所以我们应该在前台线程中执行我们确实要完成的事情,另外, 应该把非关键的任务使用后台线程,我们用Thread创建的是线程为前台线程。让我们通过下面的一段代码来看看前台线程和后台线成的区别:
using System; using System.Threading; class Program { static void Main(string[] args) { // 创建一个新线程(默认为前台线程) Thread backthread = new Thread(Worker); // 使线程成为一个后台线程 backthread.IsBackground = true; // 通过Start方法启动线程 backthread.Start(); // 如果backthread是前台线程,则应用程序大约5秒后才终止 // 如果backthread是后台线程,则应用程序立即终止 Console.WriteLine("Return from Main Thread"); } private static void Worker() { // 模拟做10秒 Thread.Sleep(5000); // 下面语句,只有由一个前台线程执行时,才会显示出来 Console.WriteLine("Return from Worker Thread"); } }
运行上面代码可以发现:控制台中显示字符串: Return form Main Thread 后就退出了, 字符串 Return from Worker Thread字符串根本就没有显示,这是因为此时的backthread线程为后台线程,当主线程(执行Main方法的线程,主线程当然也是前台线程了)结束运行后,CLR会强制终止后台线程的运行,整个进程就被销毁了,并不会等待后台线程运行完后才销毁。如果把 backthread.IsBackground = true; 注释掉后, 就可以看到控制台过5秒后就输出 Return from Worker Thread。再在Worker方法最后加一句 代码:Console.Read(); 就可以看到这样的结果了:

注意:有些人可能会问我不想把 backthread.IsBackground = true;注释掉, 又想把Worker()方法中的字符串输出在控制太上怎么做呢? 其实是有解决的办法的, 我们可以调用thread.Join()方法来实现,Join()方法能保证主线程(前台线程)在异步线程thread(后台线程)运行结束后才会运行。
实现代码如下:
using System; using System.Threading; class Program { static void Main(string[] args) { // 创建一个新线程(默认为前台线程) Thread backthread = new Thread(Worker); // 使线程成为一个后台线程 backthread.IsBackground = true; // 通过Start方法启动线程 backthread.Start(); backthread.Join(); // 模拟主线程的输出 Thread.Sleep(2000); Console.WriteLine("Return from Main Thread"); Console.Read(); } private static void Worker() { // 模拟做3秒 Thread.Sleep(3000); // 下面语句,只有由一个前台线程执行时,才会显示出来 Console.WriteLine("Return from Worker Thread"); } }
运行结果(与上面得到的结果一样):

四、简单线程的使用
其实在上面介绍前台线程和后台线程的时候已经通过ThreadStart委托创建了一个线程了,此时已经实现了一个多线程的一个过程,为此系列中将多线程也是做一个铺垫吧。下面通过ParameterizedThreadStart委托的方式来实现多线程。
以ParameterizedThreadStart委托的方式来实现多线程:
using System; using System.Threading; class Program { static void Main(string[] args) { // 创建一个新线程(默认为前台线程) Thread backthread = new Thread(new ParameterizedThreadStart(Worker)); // 通过Start方法启动线程 backthread.Start("123"); // 如果backthread是前台线程,则应用程序大约5秒后才终止 // 如果backthread是后台线程,则应用程序立即终止 Console.WriteLine("Return from Main Thread"); } private static void Worker(object data) { // 模拟做5秒 Thread.Sleep(5000); // 下面语句,只有由一个前台线程执行时,才会显示出来 Console.WriteLine(data + " Return from Worker Thread"); Console.Read(); } }
注意:此时Worker方法传入了一个参数,并且Start方法也传递了一个字符传参数。 对比与之前创建Thread的不同,
运行结果为:

写到这里, 本系列的第一篇差不多讲完了,在后续的文章将会介绍Thread方法的使用以及通过一些例子来展示他们的不同之处(像Abort()方法Interrupt方法等)对于线程的一些高级使用(如线程池,并行编程和PLINQ、线程同步和计时器)都会在后续中讲到。希望本系列可以给初学线程的人有所帮助。
[.Net线程处理系列]专题二:线程池中的工作者线程
目录:
一、上节补充
二、CLR线程池基础
三、通过线程池的工作者线程实现异步
四、使用委托实现异步
五、任务
一、上节补充
对于Thread类还有几个常用方法需要说明的。
1.1 Suspend和Resume方法
这两个方法在.net Framework 1.0的时候就支持的方法,他们分别可以挂起线程和恢复挂起的线程。但在.net Framework 2.0以后的版本中这两个方法都过时了,MSDN的解释是这样:
警告:
不要使用 Suspend 和 Resume 方法来同步线程的活动。您无法知道挂起线程时它正在执行什么代码。如果您在安全权限评估期间挂起持有锁的线程,则 AppDomain中的其他线程可能被阻止。如果您在线程正在执行类构造函数时挂起它,则 AppDomain中尝试使用该类的其他线程将被阻止。这样很容易发生死锁。
对于这个解释可能有点抽象吧,让我们来看看一段代码可能会清晰点:
class Program { static void Main(string[] args) { // 创建一个线程来测试 Thread thread1 = new Thread(TestMethod); thread1.Name = "Thread1"; thread1.Start(); Thread.Sleep(2000); Console.WriteLine("Main Thread is running"); ////int b = 0; ////int a = 3 / b; ////Console.WriteLine(a); thread1.Resume(); Console.Read(); } private static void TestMethod() { Console.WriteLine("Thread: {0} has been suspended!", Thread.CurrentThread.Name); //将当前线程挂起 Thread.CurrentThread.Suspend(); Console.WriteLine("Thread: {0} has been resumed!", Thread.CurrentThread.Name); } }
在上面这段代码中thread1线程是在主线程中恢复的,但当主线程发生异常时,这时候就thread1一直处于挂起状态,此时thread1所使用的资源就不能释放(除非强制终止进程),当另外线程需要使用这快资源的时候, 这时候就很可能发生死锁现象。
上面一段代码还存在一个隐患,请看下面一小段代码:
class Program { static void Main(string[] args) { // 创建一个线程来测试 Thread thread1 = new Thread(TestMethod); thread1.Name = "Thread1"; thread1.Start(); Console.WriteLine("Main Thread is running"); thread1.Resume(); Console.Read(); } private static void TestMethod() { Console.WriteLine("Thread: {0} has been suspended!", Thread.CurrentThread.Name); Thread.Sleep(1000); //将当前线程挂起 Thread.CurrentThread.Suspend(); Console.WriteLine("Thread: {0} has been resumed!", Thread.CurrentThread.Name); } }
当主线程跑(运行)的太快,做完自己的事情去唤醒thread1时,此时thread1还没有挂起而起唤醒thread1,此时就会出现异常了。并且上面使用的Suspend和Resume方法,编译器已经出现警告了,提示这两个方法已经过时, 所以在我们平时使用中应该尽量避免。
1.2 Abort和 Interrupt方法
Abort方法和Interrupt都是用来终止线程的,但是两者还是有区别的。
1、他们抛出的异常不一样,Abort 方法抛出的异常是ThreadAbortException, Interrupt抛出的异常为ThreadInterruptedException
2、调用interrupt方法的线程之后可以被唤醒,然而调用Abort方法的线程就直接被终止不能被唤醒的。
下面一段代码是掩饰Abort方法的使用
using System; using System.Threading; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { Thread abortThread = new Thread(AbortMethod); abortThread.Name = "Abort Thread"; abortThread.Start(); Thread.Sleep(1000); try { abortThread.Abort(); } catch { Console.WriteLine("{0} Exception happen in Main Thread", Thread.CurrentThread.Name); Console.WriteLine("{0} Status is:{1} In Main Thread ", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState); } finally { Console.WriteLine("{0} Status is:{1} In Main Thread ", abortThread.Name, abortThread.ThreadState); } abortThread.Join(); Console.WriteLine("{0} Status is:{1} ", abortThread.Name, abortThread.ThreadState); Console.Read(); } private static void AbortMethod() { try { Thread.Sleep(5000); } catch(Exception e) { Console.WriteLine(e.GetType().Name); Console.WriteLine("{0} Exception happen In Abort Thread", Thread.CurrentThread.Name); Console.WriteLine("{0} Status is:{1} In Abort Thread ", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState); } finally { Console.WriteLine("{0} Status is:{1} In Abort Thread", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState); } } }
运行结果:

从运行结果可以看出,调用Abort方法的线程引发的异常类型为ThreadAbortException, 以及异常只会在 调用Abort方法的线程中发生,而不会在主线程中抛出,并且调用Abort方法后线程的状态不是立即改变为Aborted状态,而是从AbortRequested->Aborted。
Interrupt方法:
using System; using System.Threading; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { Thread interruptThread = new Thread(AbortMethod); interruptThread.Name = "Interrupt Thread"; interruptThread.Start(); interruptThread.Interrupt(); interruptThread.Join(); Console.WriteLine("{0} Status is:{1} ", interruptThread.Name, interruptThread.ThreadState); Console.Read(); } private static void AbortMethod() { try { Thread.Sleep(5000); } catch(Exception e) { Console.WriteLine(e.GetType().Name); Console.WriteLine("{0} Exception happen In Interrupt Thread", Thread.CurrentThread.Name); Console.WriteLine("{0} Status is:{1} In Interrupt Thread ", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState); } finally { Console.WriteLine("{0} Status is:{1} In Interrupt Thread", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState); } } } }
运行结果:

从结果中可以得到,调用Interrupt方法抛出的异常为:ThreadInterruptException, 以及当调用Interrupt方法后线程的状态应该是中断的, 但是从运行结果看此时的线程因为了Join,Sleep方法而唤醒了线程,为了进一步解释调用Interrupt方法的线程可以被唤醒, 我们可以在线程执行的方法中运用循环,如果线程可以唤醒,则输出结果中就一定会有循环的部分,然而调用Abort方法线程就直接终止,就不会有循环的部分,下面代码相信大家看后肯定会更加理解两个方法的区别的:
using System; using System.Threading; namespace ConsoleApplication2 { class Program { static void Main(string[] args) { Thread thread1 = new Thread(TestMethod); thread1.Start(); Thread.Sleep(100); thread1.Interrupt(); Thread.Sleep(3000); Console.WriteLine("after finnally block, the Thread1 status is:{0}", thread1.ThreadState); Console.Read(); } private static void TestMethod() { for (int i = 0; i < 4; i++) { try { Thread.Sleep(2000); Console.WriteLine("Thread is Running"); } catch (Exception e) { if (e != null) { Console.WriteLine("Exception {0} throw ", e.GetType().Name); } } finally { Console.WriteLine("Current Thread status is:{0} ", Thread.CurrentThread.ThreadState); } } } } }
运行结果为:

如果把上面的 thread1.Interrupt();改为 thread1.Abort(); 运行结果为:

二、线程池基础
首先,创建和销毁线程是一个要耗费大量时间的过程,另外,太多的线程也会浪费内存资源,所以通过Thread类来创建过多的线程反而有损于性能,为了改善这样的问题 ,.net中就引入了线程池。
线程池形象的表示就是存放应用程序中使用的线程的一个集合(就是放线程的地方,这样线程都放在一个地方就好管理了)。CLR初始化时,线程池中是没有线程的,在内部, 线程池维护了一个操作请求队列,当应用程序想执行一个异步操作时,就调用一个方法,就将一个任务放到线程池的队列中,线程池中代码从队列中提取任务,将这个任务委派给一个线程池线程去执行,当线程池线程完成任务时,线程不会被销毁,而是返回到线程池中,等待响应另一个请求。由于线程不被销毁, 这样就可以避免因为创建线程所产生的性能损失。
注意:通过线程池创建的线程默认为后台线程,优先级默认为Normal.
三、通过线程池的工作者线程实现异步
3.1 创建工作者线程的方法
public static bool QueueUserWorkItem (WaitCallback callBack);
public static bool QueueUserWorkItem(WaitCallback callback, Object state);
这两个方法向线程池的队列添加一个工作项(work item)以及一个可选的状态数据。然后,这两个方法就会立即返回。
工作项其实就是由callback参数标识的一个方法,该方法将由线程池线程执行。同时写的回调方法必须匹配System.Threading.WaitCallback委托类型,定义为:
public delegate void WaitCallback(Object state);
下面演示如何通过线程池线程来实现异步调用:
using System; using System.Threading; namespace ThreadPoolUse { class Program { static void Main(string[] args) { // 设置线程池中处于活动的线程的最大数目 // 设置线程池中工作者线程数量为1000,I/O线程数量为1000 ThreadPool.SetMaxThreads(1000, 1000); Console.WriteLine("Main Thread: queue an asynchronous method"); PrintMessage("Main Thread Start"); // 把工作项添加到队列中,此时线程池会用工作者线程去执行回调方法 ThreadPool.QueueUserWorkItem(asyncMethod); Console.Read(); } // 方法必须匹配WaitCallback委托 private static void asyncMethod(object state) { Thread.Sleep(1000); PrintMessage("Asynchoronous Method"); Console.WriteLine("Asynchoronous thread has worked "); } // 打印线程池信息 private static void PrintMessage(String data) { int workthreadnumber; int iothreadnumber; // 获得线程池中可用的线程,把获得的可用工作者线程数量赋给workthreadnumber变量 // 获得的可用I/O线程数量给iothreadnumber变量 ThreadPool.GetAvailableThreads(out workthreadnumber, out iothreadnumber); Console.WriteLine("{0}\n CurrentThreadId is {1}\n CurrentThread is background :{2}\n WorkerThreadNumber is:{3}\n IOThreadNumbers is: {4}\n", data, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsBackground.ToString(), workthreadnumber.ToString(), iothreadnumber.ToString()); } } }
运行结果:

从结果中可以看出,线程池中的可用的工作者线程少了一个,用去执行回调方法了。
ThreadPool.QueueUserWorkItem(WaitCallback callback,Object state) 方法可以把object对象作为参数传送到回调函数中,使用和ThreadPool.QueueUserWorkItem(WaitCallback callback)的使用和类似,这里就不列出了。
3.2 协作式取消
.net Framework提供了取消操作的模式, 这个模式是协作式的。为了取消一个操作,首先必须创建一个System.Threading.CancellationTokenSource对象。
下面代码演示了协作式取消的使用,主要实现当用户在控制台敲下回车键后就停止数数方法。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; namespace ConsoleApplication3 { class Program { static void Main(string[] args) { ThreadPool.SetMaxThreads(1000, 1000); Console.WriteLine("Main thread run"); PrintMessage("Start"); Run(); Console.ReadKey(); } private static void Run() { CancellationTokenSource cts = new CancellationTokenSource(); // 这里用Lambda表达式的方式和使用委托的效果一样的,只是用了Lambda后可以少定义一个方法。 // 这在这里就是让大家明白怎么lambda表达式如何由委托转变的 ////ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 1000)); ThreadPool.QueueUserWorkItem(callback, cts.Token); Console.WriteLine("Press Enter key to cancel the operation\n"); Console.ReadLine(); // 传达取消请求 cts.Cancel(); } private static void callback(object state) { Thread.Sleep(1000); PrintMessage("Asynchoronous Method Start"); CancellationToken token =(CancellationToken)state; Count(token, 1000); } // 执行的操作,当受到取消请求时停止数数 private static void Count(CancellationToken token,int countto) { for (int i = 0; i < countto; i++) { if (token.IsCancellationRequested) { Console.WriteLine("Count is canceled"); break; } Console.WriteLine(i); Thread.Sleep(300); } Console.WriteLine("Cout has done"); } // 打印线程池信息 private static void PrintMessage(String data) { int workthreadnumber; int iothreadnumber; // 获得线程池中可用的线程,把获得的可用工作者线程数量赋给workthreadnumber变量 // 获得的可用I/O线程数量给iothreadnumber变量 ThreadPool.GetAvailableThreads(out workthreadnumber, out iothreadnumber); Console.WriteLine("{0}\n CurrentThreadId is {1}\n CurrentThread is background :{2}\n WorkerThreadNumber is:{3}\n IOThreadNumbers is: {4}\n", data, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsBackground.ToString(), workthreadnumber.ToString(), iothreadnumber.ToString()); } } }
运行结果:

四、使用委托实现异步
通过调用ThreadPool的QueueUserWorkItem方法来来启动工作者线程非常方便,但委托WaitCallback指向的是带有一个参数的无返回值的方法,如果我们实际操作中需要有返回值,或者需要带有多个参数, 这时通过这样的方式就难以实现, 为了解决这样的问题,我们可以通过委托来建立工作这线程,
下面代码演示了使用委托如何实现异步:
using System; using System.Threading; namespace Delegate { class Program { // 使用委托的实现的方式是使用了异步变成模型APM(Asynchronous Programming Model) // 自定义委托 private delegate string MyTestdelegate(); static void Main(string[] args) { ThreadPool.SetMaxThreads(1000, 1000); PrintMessage("Main Thread Start"); //实例化委托 MyTestdelegate testdelegate = new MyTestdelegate(asyncMethod); // 异步调用委托 IAsyncResult result = testdelegate.BeginInvoke(null, null); // 获取结果并打印出来 string returndata = testdelegate.EndInvoke(result); Console.WriteLine(returndata); Console.ReadLine(); } private static string asyncMethod() { Thread.Sleep(1000); PrintMessage("Asynchoronous Method"); return "Method has completed"; } // 打印线程池信息 private static void PrintMessage(String data) { int workthreadnumber; int iothreadnumber; // 获得线程池中可用的线程,把获得的可用工作者线程数量赋给workthreadnumber变量 // 获得的可用I/O线程数量给iothreadnumber变量 ThreadPool.GetAvailableThreads(out workthreadnumber, out iothreadnumber); Console.WriteLine("{0}\n CurrentThreadId is {1}\n CurrentThread is background :{2}\n WorkerThreadNumber is:{3}\n IOThreadNumbers is: {4}\n", data, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsBackground.ToString(), workthreadnumber.ToString(), iothreadnumber.ToString()); } } }
运行结果:

五、任务
同样 任务的引入也是为了解决通过ThreadPool.QueueUserWorkItem中限制的问题,
下面代码演示通过任务来实现异步:
5.1 使用任务来实现异步
using System; using System.Threading; using System.Threading.Tasks; namespace TaskUse { class Program { static void Main(string[] args) { ThreadPool.SetMaxThreads(1000, 1000); PrintMessage("Main Thread Start"); // 调用构造函数创建Task对象, Task<int> task = new Task<int>(n => asyncMethod((int)n), 10); // 启动任务 task.Start(); // 等待任务完成 task.Wait(); Console.WriteLine("The Method result is: "+task.Result); Console.ReadLine(); } private static int asyncMethod(int n) { Thread.Sleep(1000); PrintMessage("Asynchoronous Method"); int sum = 0; for (int i = 1; i < n; i++) { // 如果n太大,使用checked使下面代码抛出异常 checked { sum += i; } } return sum; } // 打印线程池信息 private static void PrintMessage(String data) { int workthreadnumber; int iothreadnumber; // 获得线程池中可用的线程,把获得的可用工作者线程数量赋给workthreadnumber变量 // 获得的可用I/O线程数量给iothreadnumber变量 ThreadPool.GetAvailableThreads(out workthreadnumber, out iothreadnumber); Console.WriteLine("{0}\n CurrentThreadId is {1}\n CurrentThread is background :{2}\n WorkerThreadNumber is:{3}\n IOThreadNumbers is: {4}\n", data, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsBackground.ToString(), workthreadnumber.ToString(), iothreadnumber.ToString()); } } }
运行结果:

5.2 取消任务
如果要取消任务, 同样可以使用一个CancellationTokenSource对象来取消一个Task.
下面代码演示了如何来取消一个任务:
using System; using System.Threading; using System.Threading.Tasks; namespace TaskUse { class Program { static void Main(string[] args) { ThreadPool.SetMaxThreads(1000, 1000); PrintMessage("Main Thread Start"); CancellationTokenSource cts = new CancellationTokenSource(); // 调用构造函数创建Task对象,将一个CancellationToken传给Task构造器从而使Task和CancellationToken关联起来 Task<int> task = new Task<int>(n => asyncMethod(cts.Token, (int)n), 10); // 启动任务 task.Start(); // 延迟取消任务 Thread.Sleep(3000); // 取消任务 cts.Cancel(); Console.WriteLine("The Method result is: " + task.Result); Console.ReadLine(); } private static int asyncMethod(CancellationToken ct, int n) { Thread.Sleep(1000); PrintMessage("Asynchoronous Method"); int sum = 0; try { for (int i = 1; i < n; i++) { // 当CancellationTokenSource对象调用Cancel方法时, // 就会引起OperationCanceledException异常 // 通过调用CancellationToken的ThrowIfCancellationRequested方法来定时检查操作是否已经取消, // 这个方法和CancellationToken的IsCancellationRequested属性类似 ct.ThrowIfCancellationRequested(); Thread.Sleep(500); // 如果n太大,使用checked使下面代码抛出异常 checked { sum += i; } } } catch (Exception e) { Console.WriteLine("Exception is:" + e.GetType().Name); Console.WriteLine("Operation is Canceled"); } return sum; } // 打印线程池信息 private static void PrintMessage(String data) { int workthreadnumber; int iothreadnumber; // 获得线程池中可用的线程,把获得的可用工作者线程数量赋给workthreadnumber变量 // 获得的可用I/O线程数量给iothreadnumber变量 ThreadPool.GetAvailableThreads(out workthreadnumber, out iothreadnumber); Console.WriteLine("{0}\n CurrentThreadId is {1}\n CurrentThread is background :{2}\n WorkerThreadNumber is:{3}\n IOThreadNumbers is: {4}\n", data, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsBackground.ToString(), workthreadnumber.ToString(), iothreadnumber.ToString()); } } }
运行结果:

5.3 任务工厂
同样可以通过任务工厂TaskFactory类型来实现异步操作。
using System; using System.Threading; using System.Threading.Tasks; namespace TaskFactory { class Program { static void Main(string[] args) { ThreadPool.SetMaxThreads(1000, 1000); Task.Factory.StartNew(() => PrintMessage("Main Thread")); Console.Read(); } // 打印线程池信息 private static void PrintMessage(String data) { int workthreadnumber; int iothreadnumber; // 获得线程池中可用的线程,把获得的可用工作者线程数量赋给workthreadnumber变量 // 获得的可用I/O线程数量给iothreadnumber变量 ThreadPool.GetAvailableThreads(out workthreadnumber, out iothreadnumber); Console.WriteLine("{0}\n CurrentThreadId is {1}\n CurrentThread is background :{2}\n WorkerThreadNumber is:{3}\n IOThreadNumbers is: {4}\n", data, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsBackground.ToString(), workthreadnumber.ToString(), iothreadnumber.ToString()); } } }
运行结果:

讲到这里CLR的工作者线程大致讲完了,希望也篇文章可以让大家对线程又有进一步的理解。在后面的一篇线程系列将谈谈CLR线程池的I/O线程。
[.Net线程处理系列]专题三:线程池中的I/O线程
上一篇文章主要介绍了如何利用线程池中的工作者线程来实现多线程,使多个线程可以并发地工作,从而高效率地使用系统资源。在这篇文章中将介绍如何用线程池中的I/O线程来执行I/O操作,希望对大家有所帮助。
目录:
一、I/O线程实现对文件的异步
二、I/O线程实现对请求的异步
三、总结
一、I/O线程实现对文件的异步
1.1 I/O线程介绍:
对于线程所执行的任务来说,可以把线程分为两种类型:工作者线程和I/O线程。
工作者线程用来完成一些计算的任务,在任务执行的过程中,需要CPU不间断地处理,所以,在工作者线程的执行过程中,CPU和线程的资源是充分利用的。
I/O线程主要用来完成输入和输出的工作的,在这种情况下, 计算机需要I/O设备完成输入和输出的任务,在处理过程中,CPU是不需要参与处理过程的,此时正在运行的线程将处于等待状态,只有等任务完成后才会有事可做, 这样就造成线程资源浪费的问题。为了解决这样的问题,可以通过线程池来解决这样的问题,让线程池来管理线程,前面已经介绍过线程池了, 在这里就不讲了。
对于I/O线程,我们可以将输入输出操作分成三个步骤:启动、实际输入输出、处理结果。用于实际输入输出可由硬件完成,并不需要CPU的参与,而启动和处理结果也可以不在同一个线程上,这样就可以充分利用线程资源。在.Net中通过以Begin开头的方法来完成启动,以End开头的方法来处理结果,这两个方法可以运行在不同的线程,这样我们就实现了异步编程了。
1.2 .Net中如何使用异步
注意:
其实当我们调用Begin开头的方法就是将一个I/O线程排入到线程池中(调用Begin开头的方法就把I/O线程加入到线程池中管理都是.Net机制帮我们实现的)。
(因为有些人会问什么地方用到了线程池了,工作者线程由线程池管理很好看出来,因为创建工作者线程直接调用ThreadPool.QueueUserWorkItem方法来把工作者线程排入到线程池中)。
在.net Framework中的FCL中有许多类型能够对异步操作提供支持,其中在FileStream类中就提供了对文件的异步操作的方法。
FileStream类要调用I/O线程要实现异步操作,首先要建立一个FileStream对象。
通过下面的构造函数来初始化FileStream对象实现异步操作(异步读取和异步写入):
public FileStream (string path, FileMode mode, FileAccess access, FileShare share,int bufferSize,bool useAsync)
其中path代表文件的相对路径或绝对路径,mode代表如何打开或创建文件,access代表访问文件的方式,share代表文件如何由进程共享,buffersize代表缓冲区的大小,useAsync代表使用异步I/O还是同步I/O,设置为true时,说明使用异步I/O.
下面通过代码来学习下异步写入文件:
using System; using System.IO; using System.Text; using System.Threading; namespace AsyncFile { class Program { static void Main(string[] args) { const int maxsize = 100000; ThreadPool.SetMaxThreads(1000,1000); PrintMessage("Main Thread start"); // 初始化FileStream对象 FileStream filestream = new FileStream("test.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, 100, true); //打印文件流打开的方式 Console.WriteLine("filestream is {0} opened Asynchronously", filestream.IsAsync ? "" : "not"); byte[] writebytes =new byte[maxsize]; string writemessage = "An operation Use asynchronous method to write message......................."; writebytes = Encoding.Unicode.GetBytes(writemessage); Console.WriteLine("message size is: {0} byte\n", writebytes.Length); // 调用异步写入方法比信息写入到文件中 filestream.BeginWrite(writebytes, 0, writebytes.Length, new AsyncCallback(EndWriteCallback), filestream); filestream.Flush(); Console.Read(); } // 当把数据写入文件完成后调用此方法来结束异步写操作 private static void EndWriteCallback(IAsyncResult asyncResult) { Thread.Sleep(500); PrintMessage("Asynchronous Method start"); FileStream filestream = asyncResult.AsyncState as FileStream; // 结束异步写入数据 filestream.EndWrite(asyncResult); filestream.Close(); } // 打印线程池信息 private static void PrintMessage(String data) { int workthreadnumber; int iothreadnumber; // 获得线程池中可用的线程,把获得的可用工作者线程数量赋给workthreadnumber变量 // 获得的可用I/O线程数量给iothreadnumber变量 ThreadPool.GetAvailableThreads(out workthreadnumber, out iothreadnumber); Console.WriteLine("{0}\n CurrentThreadId is {1}\n CurrentThread is background :{2}\n WorkerThreadNumber is:{3}\n IOThreadNumbers is: {4}\n", data, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsBackground.ToString(), workthreadnumber.ToString(), iothreadnumber.ToString()); } } }
运行结果:

从运行结果可以看出,此时是调用线程池中的I/O线程去执行回调函数的,同时在工程所的的bin\Debug文件目录下有生成一个text.txt文件,打开文件可以知道里面的内容正是你写入的。
下面演示如何从刚才的文件中异步读取我们写入的内容:
using System; using System.IO; using System.Text; using System.Threading; namespace AsyncFileRead { class Program { const int maxsize = 1024; static byte[] readbytes = new byte[maxsize]; static void Main(string[] args) { ThreadPool.SetMaxThreads(1000, 1000); PrintMessage("Main Thread start"); // 初始化FileStream对象 FileStream filestream = new FileStream("test.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, 100, false); // 异步读取文件内容 filestream.BeginRead(readbytes, 0, readbytes.Length, new AsyncCallback(EndReadCallback), filestream); Console.Read(); } private static void EndReadCallback(IAsyncResult asyncResult) { Thread.Sleep(1000); PrintMessage("Asynchronous Method start"); // 把AsyncResult.AsyncState转换为State对象 FileStream readstream = (FileStream)asyncResult.AsyncState; int readlength = readstream.EndRead(asyncResult); if (readlength <=0) { Console.WriteLine("Read error"); return; } string readmessage = Encoding.Unicode.GetString(readbytes, 0, readlength); Console.WriteLine("Read Message is :" + readmessage); readstream.Close(); } // 打印线程池信息 private static void PrintMessage(String data) { int workthreadnumber; int iothreadnumber; // 获得线程池中可用的线程,把获得的可用工作者线程数量赋给workthreadnumber变量 // 获得的可用I/O线程数量给iothreadnumber变量 ThreadPool.GetAvailableThreads(out workthreadnumber, out iothreadnumber); Console.WriteLine("{0}\n CurrentThreadId is {1}\n CurrentThread is background :{2}\n WorkerThreadNumber is:{3}\n IOThreadNumbers is: {4}\n", data, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsBackground.ToString(), workthreadnumber.ToString(), iothreadnumber.ToString()); } } }
运行结果:

这里有个需要注意的问题:如果大家测试的时候, 应该把开始生成的text.txt文件放到该工程下bin\debug\目录下, 我刚开始的做的时候就忘记拷过去的, 读出来的数据长度一直为0(这里我犯的错误写下了,希望大家可以注意,也是警惕自己要小心。)
二、I/O线程实现对请求的异步
我们同样可以利用I/O线程来模拟对浏览器对服务器请求的异步操作,在.net类库中的WebRequest类提供了异步请求的支持,
下面就来演示下如何实现请求异步:
using System; using System.Net; using System.Threading; namespace RequestSample { class Program { static void Main(string[] args) { ThreadPool.SetMaxThreads(1000, 1000); PrintMessage("Main Thread start"); // 发出一个异步Web请求 WebRequest webrequest =WebRequest.Create("http://www.cnblogs.com/"); webrequest.BeginGetResponse(ProcessWebResponse, webrequest); Console.Read(); } // 回调方法 private static void ProcessWebResponse(IAsyncResult result) { Thread.Sleep(500); PrintMessage("Asynchronous Method start"); WebRequest webrequest = (WebRequest)result.AsyncState; using (WebResponse webresponse = webrequest.EndGetResponse(result)) { Console.WriteLine("Content Length is : "+webresponse.ContentLength); } } // 打印线程池信息 private static void PrintMessage(String data) { int workthreadnumber; int iothreadnumber; // 获得线程池中可用的线程,把获得的可用工作者线程数量赋给workthreadnumber变量 // 获得的可用I/O线程数量给iothreadnumber变量 ThreadPool.GetAvailableThreads(out workthreadnumber, out iothreadnumber); Console.WriteLine("{0}\n CurrentThreadId is {1}\n CurrentThread is background :{2}\n WorkerThreadNumber is:{3}\n IOThreadNumbers is: {4}\n", data, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsBackground.ToString(), workthreadnumber.ToString(), iothreadnumber.ToString()); } } }
运行结果为:

写到这里这篇关于I/O线程的文章也差不多写完了, 其实I/O线程还可以做很多事情,在网络(Socket)编程,web开发中都会用I/O线程,本来想写个Demo来展示多线程在实际的工作中都有那些应用的地方的, 但是后面觉得还是等多线程系列都讲完后再把知识一起串联起来做个Demo会好点,至于后面文章中将介绍下线程同步的问题。
[.Net线程处理系列]专题四:线程同步
目录:
一、线程同步概述
二、线程同步的使用
三 、总结
一、线程同步概述
前面的文章都是讲创建多线程来实现让我们能够更好的响应应用程序,然而当我们创建了多个线程时,就存在多个线程同时访问一个共享的资源的情况,在这种情况下,就需要我们用到线程同步,线程同步可以防止数据(共享资源)的损坏。
然而我们在设计应用程序还是要尽量避免使用线程同步, 因为线程同步会产生一些问题:
1. 它的使用比较繁琐。因为我们要用额外的代码把多个线程同时访问的数据包围起来,并获取和释放一个线程同步锁,如果我们在一个代码块忘记获取锁,就有可能造成数据损坏。
2. 使用线程同步会影响性能,获取和释放一个锁肯定是需要时间的吧,因为我们在决定哪个线程先获取锁时候, CPU必须进行协调,进行这些额外的工作就会对性能造成影响
3. 因为线程同步一次只允许一个线程访问资源,这样就会阻塞线程,阻塞线程会造成更多的线程被创建,这样CPU就有可能要调度更多的线程,同样也对性能造成了影响。
所以在实际的设计中还是要尽量避免使用线程同步,因此我们要避免使用一些共享数据,例如静态字段。
二、线程同步的使用
2.1 对于使用锁性能的影响
上面已经说过使用锁将会对性能产生影响, 下面通过比较使用锁和不使用锁时消耗的时间来说明这点
using System; using System.Diagnostics; using System.Threading; namespace InterlockedSample { // 比较使用锁和不使用锁锁消耗的时间 // 通过时间来说明使用锁性能的影响 class Program { static void Main(string[] args) { int x = 0; // 迭代次数为500万 const int iterationNumber = 5000000; // 不采用锁的情况 // StartNew方法 对新的 Stopwatch 实例进行初始化,将运行时间属性设置为零,然后开始测量运行时间。 Stopwatch sw = Stopwatch.StartNew(); for (int i = 0; i < iterationNumber; i++) { x++; } Console.WriteLine("Use the all time is :{0} ms", sw.ElapsedMilliseconds); sw.Restart(); // 使用锁的情况 for (int i = 0; i < iterationNumber; i++) { Interlocked.Increment(ref x); } Console.WriteLine("Use the all time is :{0} ms", sw.ElapsedMilliseconds); Console.Read(); } } }
运行结果(这是在我电脑上运行的结果)从结果中可以看出加了锁的运行速度慢了好多(慢了11倍 197/18 ):

2.2 Interlocked实现线程同步
Interlocked类提供了为多个线程共享的变量提供原子操作,当我们在多线程中对一个整数进行递增操作时,就需要实现线程同步。
因为增加变量操作(++运算符)不是一个原子操作,需要执行下列步骤:
1)将实例变量中的值加载到寄存器中。
2)增加或减少该值。
3)在实例变量中存储该值。
如果不使用 Interlocked.Increment方法,线程可能会在执行完前两个步骤后被抢先。然后由另一个线程执行所有三个步骤,此时第一个线程还没有把变量的值存储到实例变量中去,而另一个线程就可以把实例变量加载到寄存器里面读取了(此时加载的值并没有改变),所以会导致出现的结果不是我们预期的,相信这样的解释可以帮助大家更好的理解Interlocked.Increment方法和 原子性操作,
下面通过一段代码来演示下加锁和不加锁的区别(开始讲过加锁会对性能产生影响,这里将介绍加锁来解决线程同步的问题,得到我们预期的结果):
不加锁的情况:
class Program { static void Main(string[] args) { for (int i = 0; i < 10; i++) { Thread testthread = new Thread(Add); testthread.Start(); } Console.Read(); } // 共享资源 public static int number = 1; public static void Add() { Thread.Sleep(1000); Console.WriteLine("the current value of number is:{0}", ++number); } }
运行结果(不同电脑上可能运行的结果和我的不一样,但是都是得到不是预期的结果的):

为了解决这样的问题,我们可以通过使用 Interlocked.Increment方法来实现原子的自增操作。
代码很简单,只需要把++number改成Interlocked.Increment(ref number)就可以得到我们预期的结果了,在这里代码和运行结果就不贴了。
总之Interlocked类中的方法都是执行一次原子读取以及写入的操作的。
2.3 Monitor实现线程同步
对于上面那个情况也可以通过Monitor.Enter和Monitor.Exit方法来实现线程同步。C#中通过lock关键字来提供简化的语法(lock可以理解为Monitor.Enter和Monitor.Exit方法的语法糖),代码也很简单:
using System; using System.Threading; namespace MonitorSample { class Program { static void Main(string[] args) { for (int i = 0; i < 10; i++) { Thread testthread = new Thread(Add); testthread.Start(); } Console.Read(); } // 共享资源 public static int number = 1; public static void Add() { Thread.Sleep(1000); //获得排他锁 Monitor.Enter(number); Console.WriteLine("the current value of number is:{0}", number++); // 释放指定对象上的排他锁。 Monitor.Exit(number); } } }
运行结果当然是我们所期望的:

在 Monitor类中还有其他几个方法在这里也介绍,只是让大家引起注意下,一个Wait方法,很明显Wait方法的作用是:释放某个对象上的锁以便允许其他线程锁定和访问这个对象。第二个就是TryEnter方法,这个方法与Enter方法主要的区别在于是否阻塞当前线程,当一个对象通过Enter方法获取锁,而没有执行Exit方法释放锁,当另一个线程想通过Enter获得锁时,此时该线程将会阻塞,直到另一个线程释放锁为止,而TryEnter不会阻塞线程。具体代码就不不写出来了。
2.4 ReaderWriterLock实现线程同步
如果我们需要对一个共享资源执行多次读取时,然而用前面所讲的类实现的同步锁都只允许一个线程允许,所有线程将阻塞,但是这种情况下肯本没必要堵塞其他线程, 应该让它们并发的执行,因为我们此时只是进行读取操作,此时通过ReaderWriterLock类可以很好的实现读取并行。
演示代码为:
using System; using System.Collections.Generic; using System.Threading; namespace ReaderWriterLockSample { class Program { public static List<int> lists = new List<int>(); // 创建一个对象 public static ReaderWriterLock readerwritelock = new ReaderWriterLock(); static void Main(string[] args) { //创建一个线程读取数据 Thread t1 = new Thread(Write); t1.Start(); // 创建10个线程读取数据 for (int i = 0; i < 10; i++) { Thread t = new Thread(Read); t.Start(); } Console.Read(); } // 写入方法 public static void Write() { // 获取写入锁,以10毫秒为超时。 readerwritelock.AcquireWriterLock(10); Random ran = new Random(); int count = ran.Next(1, 10); lists.Add(count); Console.WriteLine("Write the data is:" + count); // 释放写入锁 readerwritelock.ReleaseWriterLock(); } // 读取方法 public static void Read() { // 获取读取锁 readerwritelock.AcquireReaderLock(10); foreach (int li in lists) { // 输出读取的数据 Console.WriteLine(li); } // 释放读取锁 readerwritelock.ReleaseReaderLock(); } } }
运行结果:

三、总结
本文中主要介绍如何实现多线程同步的问题, 通过线程同步可以防止共享数据的损坏,但是由于获取锁的过程会有性能损失,所以在设计应用过程中尽量减少线程同步的使用。本来还要介绍互斥(Mutex), 信号量(Semaphore), 事件构造的, 由于篇幅的原因怕影响大家的阅读,所以这剩下的内容放在后面介绍的。


浙公网安备 33010602011771号