多线程笔记
自从接触代码以来,多线程一直困扰着小弟。不理解什么是多线程,不理解怎么用多线程,不理解什么时候用多线程???这里仅仅是学习多线程的笔记,以供自己闲暇时间翻看。
在网上随便看了篇文章,发现里面对关于线程的概念解释的不错。贴过来:
顺序执行:程序中的所有事物在任意时刻都只能执行一个步骤;
并发:在同一时间段内,需要处理多个任务,而在每个时间点又只能处理一个,这就是并发。
假设我们要把多个任务分配给处理机,如果这台机器有多个处理器,显然可以同时执行这些任务,这就是并行。
不同于并行,并发的目的旨在最大限度的提高程序在单处理器上的效率。前者是在物理上的同时发生,而并发是在逻辑上的同时发生。
1)首先列举学习多线程所必须明白的基本概念:
进程(Process)是Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。而一个进程又是由多个线程所组成的。
线程(Thread)是进程中的基本执行单元,在进程入口执行的第一个线程被视为这个进程的主线程。在.NET应用程序中,都是以Main()方法作为入口的,当调用此方法时系统就会自动创建一个主线程。线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的。CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。线程,有时被称为轻量级进程,是程序执行流的最小单元。
应用程序域(AppDomain)是一个程序运行的逻辑区域,它可以视为一个轻量级的进程,.NET的程序集正是在应用程序域中运行的,一个进程可以包含有多个应用程序域,一个应用程序域也可以包含多个程序集。在一个应用程序域中包含了一个或多个上下文context,使用上下文CLR就能够把某些特殊对象的状态放置在不同容器当中。
多线程:线程是程序中一个单一的顺序控制流程。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。
2)基础知识:
1>公共属性:
CurrentContext | 获取线程正在其中执行的当前上下文。 |
CurrentThread | 获取当前正在运行的线程。 |
ExecutionContext | 获取一个 ExecutionContext 对象,该对象包含有关当前线程的各种上下文的信息。 |
IsAlive | 获取一个值,该值指示当前线程的执行状态。 |
IsBackground | 获取或设置一个值,该值指示某个线程是否为后台线程。 |
IsThreadPoolThread | 获取一个值,该值指示线程是否属于托管线程池。 |
ManagedThreadId | 获取当前托管线程的唯一标识符。 |
Name | 获取或设置线程的名称。 |
Priority | 获取或设置一个值,该值指示线程的调度优先级。 |
ThreadState | 获取一个值,该值包含当前线程的状态。 |
2>多线程的优先级:
成员名称 | 说明 |
---|---|
Lowest | 可以将 Thread 安排在具有任何其他优先级的线程之后。 |
BelowNormal | 可以将 Thread 安排在具有 Normal 优先级的线程之后,在具有 Lowest 优先级的线程之前。 |
Normal | 默认选择。可以将 Thread 安排在具有 AboveNormal 优先级的线程之后,在具有BelowNormal 优先级的线程之前。 |
AboveNormal | 可以将 Thread 安排在具有 Highest 优先级的线程之后,在具有 Normal 优先级的线程之前。 |
Highest | 可以将 Thread 安排在具有任何其他优先级的线程之前。 |
3>System.Threading.Thread的方法
方法名称 | 说明 |
---|---|
Abort() | 终止本线程。 |
GetDomain() | 返回当前线程正在其中运行的当前域。 |
GetDomainId() | 返回当前线程正在其中运行的当前域Id。 |
Interrupt() | 中断处于 WaitSleepJoin 线程状态的线程。 |
Join() | 已重载。 阻塞调用线程,直到某个线程终止时为止。 |
Resume() | 继续运行已挂起的线程。 |
Start() | 执行本线程。 |
Suspend() | 挂起当前线程,如果当前线程已属于挂起状态则此不起作用 |
Sleep() | 把正在运行的线程挂起一段时间。 |
3)简单示例:
(1)委托绑定方法不带参数:
public class Message { public void ShowMessage() { string message = string.Format("Async threadId is :{0}", Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); for (int n = 0; n < 20; n++) { Thread.Sleep(300); Console.WriteLine("The number is:" + n.ToString()); } } } class Program { static void Main(string[] args) { Console.WriteLine("Main threadId is:" + Thread.CurrentThread.ManagedThreadId); Message message = new Message(); Thread thread = new Thread(new ThreadStart(message.ShowMessage)); thread.IsBackground = true;//设置为后台线程 thread.Start(); Console.WriteLine("Do something ..........!"); Console.WriteLine("Main thread working is complete!"); } }
示例分析:
1>在主线程内创建一个显示信息的线程thread。定义线程,使用ThreadStart委托绑定ShowMessage方法,最后完成整个进程。
2>IsBackground:设置线程是否为后台线程。本例中设置为true,则系统不会执行thread线程而直接结束进程。
3>使用Thread.Start()启动的线程默认为前台线程,而系统必须等待所有前台线程运行结束后,应用程序域才会自动卸载。曾经介绍过线程Thread有一个属性IsBackground,通过把此属性设置为true,就可以把线程设置为后台线程!这时应用程序域将在主线程完成时就被卸载,而不会等待异步线程的运行。
(2)委托绑定方法带参数:
public class Person { public string Name { get; set; } public int Age { get; set; } } public class Message { public void ShowMessage(object person) { if (person != null) { Person _person = (Person)person; string message = string.Format("\n{0}'s age is {1}!\nAsync threadId is:{2}",_person.Name, _person.Age, Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); } for (int n = 0; n < 10; n++) { Thread.Sleep(300); Console.WriteLine("The number is:" + n.ToString()); } } } class Program { static void Main(string[] args) { Console.WriteLine("Main threadId is:" + Thread.CurrentThread.ManagedThreadId); Message message = new Message(); //绑定带参数的异步方法 Thread thread = new Thread(new ParameterizedThreadStart(message.ShowMessage)); Person person = new Person(); person.Name = "Jack"; person.Age = 21; thread.IsBackground = true; thread.Start(person); //启动异步线程 Console.WriteLine("Do something ..........!"); Console.WriteLine("Main thread working is complete!"); //Thread.Sleep(5000);//为了等待其他后台线程完成后再结束主线程,就可以使用Thread.Sleep()方法。 thread.Join();//thread.Join() 就能保证主线程在异步线程thread运行结束后才会终止 } }
示例分析:
1>ParameterizedThreadStart支持绑定带参数的方法。
2>本例将thread设置为后台线程,thread.Join();//thread.Join() 就能保证主线程在异步线程thread运行结束后才会终止。
(3)启动和终止线程
class Program { static void Main(string[] args) { Console.WriteLine("Main threadId is:" + Thread.CurrentThread.ManagedThreadId); Thread thread = new Thread(new ThreadStart(AsyncThread)); thread.IsBackground = true; thread.Start(); thread.Join(); } //以异步方式调用 static void AsyncThread() { try { string message = string.Format("\nAsync threadId is:{0}",Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); for (int n = 0; n < 10; n++) { //当n等于4时,终止线程 if (n >= 4) { Thread.CurrentThread.Abort(n); } Thread.Sleep(300); Console.WriteLine("The number is:" + n.ToString()); } } catch (ThreadAbortException ex) { //输出终止线程时n的值 if (ex.ExceptionState != null) Console.WriteLine(string.Format("Thread abort when the number is: {0}!", ex.ExceptionState.ToString())); //取消终止,继续执行线程 Thread.ResetAbort(); Console.WriteLine("Thread ResetAbort!"); } Console.WriteLine("Thread Close!"); } }
示例分析:
1>若想终止正在运行的线程,可以使用Abort()方法。在使用Abort()的时候,将引发一个特殊异常 ThreadAbortException 。若想在线程终止前恢复线程的执行,可以在捕获异常后 ,catch(ThreadAbortException ex){...} 中调用Thread.ResetAbort()取消终止。而使用Thread.Join()可以保证应用程序域等待异步线程结束后才终止运行。
2>Thread.Suspend()与 Thread.Resume()是在Framework1.0 就已经存在的老方法了,它们分别可以挂起、恢复线程。但在Framework2.0中就已经明确排斥这两个方法。这是因为一旦某个线程占用了已有的资源,再使用Suspend()使线程长期处于挂起状态,当在其他线程调用这些资源的时候就会引起死锁!所以在没有必要的情况下应该避免使用这两个方法。
4)线程池
使用ThreadStart与ParameterizedThreadStart建立新线程非常简单,但通过此方法建立的线程难于管理,若建立过多的线程反而会影响系统的性能。所以.NET引入CLR线程池这个概念。CLR线程池并不会在CLR初始化的时候立刻建立线程,而是在应用程序要创建线程来执行任务时,线程池才初始化一个线程。线程的初始化与其他的线程一样。在完成任务以后,该线程不会自行销毁,而是以挂起的状态返回到线程池。直到应用程序再次向线程池发出请求时,线程池里挂起的线程就会再度激活执行任务。这样既节省了建立线程所造成的性能损耗,也可以让多个任务反复重用同一线程,从而在应用程序生存期内节约大量开销。
不带参数:
class Program { static void Main(string[] args) { //把CLR线程池的最大值设置为1000 ThreadPool.SetMaxThreads(1000, 1000); //显示主线程启动时线程池信息 //启动工作者线程 ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncCallback)); Console.ReadKey(); } static void AsyncCallback(object state) { Thread.Sleep(200); ThreadMessage("AsyncCallback"); Console.WriteLine("Async thread do work!"); } }
带参数:
class Program { static void Main(string[] args) { //把线程池的最大值设置为1000 ThreadPool.SetMaxThreads(1000, 1000); ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncCallback), "Hello Elva"); Console.ReadKey(); } static void AsyncCallback(object state) { Thread.Sleep(200); ThreadMessage("AsyncCallback"); string data = (string)state; Console.WriteLine("Async thread do work!\n" + data); } }
5)利用BeginInvoke和EndInvoke完成异步委托
class Program { delegate string MyDelegate(string name); static void Main(string[] args) { MyDelegate mDelegate = new MyDelegate(Method); IAsyncResult result = mDelegate.BeginInvoke("lesli", null, null);//异步调用委托,获取计算结果 while (!result.IsCompleted) { Thread.Sleep(200); Console.WriteLine("Main thread do work"); } string data = mDelegate.EndInvoke(result);//等待异步方法完成,调用EndInvoke(IAsyncResult)获取运行结果 Console.WriteLine(data); Console.Read(); } static string Method(string name) { ThreadMessage("Async Thread"); Thread.Sleep(2000); return "Method " + name; } }
示例分析:
1>如果主线只有简单的操作,只需要0.1秒就能完成操作,而异步线程是一个IO数据操作,需要2秒才能完成。这样如果在主线程直接调用myDelegate.EndInvoke方法,主线程就必须等待IO数据操作完成后才能有返回结果,在这2秒时间内主线程就处于一种阻塞状态。这令多线程操作失去其价值。
所以下一个例子才会使用轮询方式进行处理,在异步线程未完成操作时,先进行其他操作。到asyncResult.IsCompleted为true时,才使用myDelegate.EndInvoke获取异步操作的结果.
2>建立一个委托对象,通过IAsyncResult BeginInvoke(string name,AsyncCallback callback,object state) 异步调用委托方法,BeginInvoke 方法除最后的两个参数外,其它参数都是与方法参数相对应的。通过 BeginInvoke 方法将返回一个实现了 System.IAsyncResult 接口的对象,之后就可以利用EndInvoke(IAsyncResult ) 方法就可以结束异步操作,获取委托的运行结果。
6)多线程中的回调函数:
不传递参数:
class Program { public delegate string MyDelegate(string name); static void Main(string[] args) { ThreadMessage("Main Thread"); MyDelegate mDelegate = new MyDelegate(Method); mDelegate.BeginInvoke("lili", new AsyncCallback(Completed), null); for (int i = 0; i < 10; i++) { Console.WriteLine("Main thread do work {0}", i); } Console.Read(); } static void Completed(IAsyncResult ar) { ThreadMessage("Async Completed"); AsyncResult result = (AsyncResult)ar; MyDelegate mDelegate2 = (MyDelegate)result.AsyncDelegate; string data = mDelegate2.EndInvoke(result); Console.WriteLine(data); } static string Method(string name) { ThreadMessage("Async Thread"); Thread.Sleep(2000); return "Method " + name; } //显示线程现状 static void ThreadMessage(string data) { string message = string.Format("{0}\n CurrentThreadId is {1}", data, Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); } }
传递参数:
class Program { public class Person { public string Name; public int Age; } delegate string MyDelegate(string name); static void Main(string[] args) { ThreadMessage("Main Thread"); //建立委托 MyDelegate myDelegate = new MyDelegate(Method); //建立Person对象 Person person = new Person(); person.Name = "Elva"; person.Age = 27; //异步调用委托,输入参数对象person, 获取计算结果 myDelegate.BeginInvoke("Leslie", new AsyncCallback(Completed), person); //在启动异步线程后,主线程可以继续工作而不需要等待 for (int n = 0; n < 6; n++) Console.WriteLine(" Main thread do work!"); Console.WriteLine(""); Console.ReadKey(); } static string Method(string name) { ThreadMessage("Async Thread"); Thread.Sleep(2000); return "\nHello " + name; } static void Completed(IAsyncResult result) { ThreadMessage("Async Completed"); //获取委托对象,调用EndInvoke方法获取运行结果 AsyncResult _result = (AsyncResult)result; MyDelegate myDelegate = (MyDelegate)_result.AsyncDelegate; string data = myDelegate.EndInvoke(_result); //获取Person对象 Person person = (Person)result.AsyncState; string message = person.Name + "'s age is " + person.Age.ToString(); Console.WriteLine(data + "\n" + message); } static void ThreadMessage(string data) { string message = string.Format("{0}\n ThreadId is:{1}", data, Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); } }
7)示例:socket多线程中的异步操作
server端:
class Program { static void Main(string[] args) { ThreadPool.SetMaxThreads(1000, 1000); IPAddress ip = IPAddress.Parse("127.0.0.1"); TcpListener tcpListener = new TcpListener(ip, 5000); tcpListener.Start(); while (true) { ChatClient chatClient = new ChatClient(tcpListener.AcceptTcpClient()); } } } public class ChatClient { static TcpClient tcpClient; static byte[] byteMessage; static string clientEndPoint; public ChatClient(TcpClient tcpClient1) { tcpClient = tcpClient1; byteMessage = new byte[tcpClient.ReceiveBufferSize]; clientEndPoint = tcpClient.Client.RemoteEndPoint.ToString(); Console.WriteLine("Client's endpoint is " + clientEndPoint); NetworkStream networkStream = tcpClient.GetStream(); networkStream.BeginRead(byteMessage, 0, tcpClient.ReceiveBufferSize, new AsyncCallback(ReceiveCallback), null); } public void ReceiveCallback(IAsyncResult ar) { Thread.Sleep(100); ThreadPoolMessage("\n Message is receiving"); NetworkStream networkStream2 = tcpClient.GetStream(); int length = networkStream2.EndRead(ar); if (length < 1) { tcpClient.GetStream().Close(); throw new Exception("Disconnection"); } string message = Encoding.UTF8.GetString(byteMessage, 0, length); Console.WriteLine("Message:" + message); byte[] sendMessage = Encoding.UTF8.GetBytes("Message is received"); NetworkStream networkStreamWrite = tcpClient.GetStream(); networkStreamWrite.BeginWrite(sendMessage, 0, sendMessage.Length, new AsyncCallback(SendCallback), null); } public void SendCallback(IAsyncResult ar) { Thread.Sleep(100); ThreadPoolMessage("\n Message is sending"); tcpClient.GetStream().EndWrite(ar); tcpClient.GetStream().BeginRead(byteMessage, 0, tcpClient.ReceiveBufferSize, new AsyncCallback(ReceiveCallback), null); } //显示线程池现状 static void ThreadPoolMessage(string data) { int a, b; ThreadPool.GetAvailableThreads(out a, out b); string message = string.Format("{0}\n CurrentThreadId is {1}\n " + "WorkerThreads is:{2} CompletionPortThreads is :{3}\n", data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString()); Console.WriteLine(message); } }
client端:
class Program { static void Main(string[] args) { TcpClient tcpClient = new TcpClient("127.0.0.1", 5000); NetworkStream networkStream = tcpClient.GetStream(); byte[] sendMessage = Encoding.UTF8.GetBytes("Client request connection"); networkStream.Write(sendMessage, 0, sendMessage.Length); networkStream.Flush(); byte[] receiveMessage = new byte[1024]; int count = networkStream.Read(receiveMessage, 0, 1024); Console.WriteLine(Encoding.UTF8.GetString(receiveMessage)); Console.ReadKey(); } }
8)优缺点:
优点:多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。线程是在同一时间需要完成多项任务的时候实现的。使用线程可以把耗时比较长的任务放到后台单独开一个线程,使程序运行得更快。同时使用多线程可以开发出更人性化的界面,例如当我们提交某项数据的时候通过使用多线程显示处理进度等效果。最简单的比喻多线程就像一个工厂的工人,而进程则是工厂的某个车间。工人离开车间就无法生产,同理车间也不可能只有一个员工。多线程的出现就是为了提高效率。
缺点:更过的线程意味着更多的内存消耗;线程的退出可能会对程序带来麻烦;处理不当造成更多的死锁;过多的线程会影响性能(因为操作系统需要在各个线程间切换)。
说到死锁,这里简单介绍下:
什么叫做死锁?
多个线程在执行过程中因争夺资源而造成的一种僵局,若无外力作用,将无法向前推进。
产生死锁的原因
1> 竞争资源。当系统中供多个线程共享的资源如打印机等,其数目不足以满足诸线程的需要时,会引起诸线程对资源的竞争而产生死锁;
2> 线程间推进顺序非法,线程在运行过程中,请求和释放资源的顺序不当,也同样会导致产生进程死锁。
产生死锁的必要条件
1> 互斥条件,即一段时间某资源只由一个线程占用;
2> 请求和保持条件,指进程已经保持了至少一个资源,但又提出了新的资源请求新的资源求情,而该资源又已经被其他线程占有,此时请求进程序阻塞,但又对自己已经获得的其他资源保持不放;
3> 不剥夺条件,指线程已经获得的资源,在未使用完之前,不能被剥夺,只能在使用完时自己释放;
4> 环路等待,指在发生死锁时,必然存在一个进程—资源的环形链。如P1等待一个P2占用的资源,P2正在等待P3占用的资源,P3正在等待P1占用的资源。
死锁的解除方式
1>剥夺资源,从其他进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
2>撤销进程,最简单的撤销进程的方法是使全部死锁进程都夭折掉,稍微温和一点的方法是按照某种顺序逐个撤销进程,直至有足够的资源可用,使死锁状态消除为止。
多线程与异步的区别:
多线程和异步操作两者都可以达到避免调用线程阻塞的目的,从而提高软件的可响应性。甚至有些时候我们就认为多线程和异步操作是等同的概念。但是,多线程和异步操作还是有一些区别的。而这些区别造成了使用多线程和异步操作的时机的区别。
异步操作的本质
所有的程序最终都会由计算机硬件来执行,所以为了更好的理解异步操作的本质,我们有必要了解一下它的硬件基础。 熟悉电脑硬件的朋友肯定对DMA这个词不陌生,硬盘、光驱的技术规格中都有明确DMA的模式指标,其实网卡、声卡、显卡也是有DMA功能的。DMA就是直接内存访问的意思,也就是说,拥有DMA功能的硬件在和内存进行数据交换的时候可以不消耗CPU资源。只要CPU在发起数据传输时发送一个指令,硬件就开始自己和内存交换数据,在传输完成之后硬件会触发一个中断来通知操作完成。这些无须消耗CPU时间的I/O操作正是异步操作的硬件基础。所以即使在DOS这样的单进程(而且无线程概念)系统中也同样可以发起异步的DMA操作。
异步操作的优缺点:
因为异步操作无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码可以减少共享变量的数量),减少了死锁的可能。当然异步操作也并非完美无暇。编写异步操作的复杂程度较高,程序主要使用回调方式进行处理,与普通人的思维方式有些初入,而且难以调试。
线程的本质:线程不是一个计算机硬件的功能,而是操作系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码,所以线程需要操作系统投入CPU资源来运行和调度。
多线程的优缺点:
多线程的优点很明显,线程中的处理程序依然是顺序执行,符合普通人的思维习惯,所以编程简单。但是多线程的缺点也同样明显,线程的使用(滥用)会给系统带来上下文切换的额外负担。并且线程间的共享变量可能造成死锁的出现。
适用范围:
当需要执行I/O操作时,使用异步操作比使用线程+同步I/O操作更合适。I/O操作不仅包括了直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及.Net Remoting等跨进程的调用。
而线程的适用范围则是那种需要长时间CPU运算的场合,例如耗时较长的图形处理和算法执行。但是往往由于使用线程编程的简单和符合习惯,所以很多朋友往往会使用线程来执行耗时较长的I/O操作。这样在只有少数几个并发操作的时候还无伤大雅,如果需要处理大量的并发操作时就不合适了。
线程池:
构建服务器应用程序的一个过于简单的模型是:每当一个请求到达就创建一个新的服务对象,然后在新的服务对象中为请求服务。但当有大量请求并发访问时,服务器不断的创建和销毁对象的开销很大。所以提高服务器效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这样就引入了“池”的概念,“池”的概念使得人们可以定制一定量的资源,然后对这些资源进行复用,而不是频繁的创建和销毁。
线程池是预先创建线程的一种技术。线程池在还没有任务到来之前,创建一定数量的线程,放入空闲队列中。这些线程都是处于睡眠状态,即均为启动,不消耗CPU,而只是占用较小的内存空间。当请求到来之后,缓冲池给这次请求分配一个空闲线程,把请求传入此线程中运行,进行处理。当预先创建的线程都处于运行状态,即预制线程不够,线程池可以自由创建一定数量的新线程,用于处理更多的请求。当系统比较闲的时候,也可以通过移除一部分一直处于停用状态的线程。
一个线程的周期分为:创建、运行、销毁三个阶段。处理一个任务时,首先创建一个任务线程,然后执行任务,完了,销毁线程。而线程处于运行状态的时候,才是真的在处理我们交给它的任务,这个阶段才是有效运行时间。所以,我们希望花在创建和销毁线程的资源越少越好。如果不销毁线程,而这个线程又不能被其他的任务调用,那么就会出现资源的浪费。为了提高效率,减少创建和销毁线程带来时间和空间上的浪费,出现了线程池技术。这种技术是在开始就创建一定量的线程,批量处理一类任务,等待任务的到来。任务执行完毕后,线程又可以执行其他的任务。等不再需要线程的时候,就销毁。这样就省去了频繁创建和销毁线程的麻烦。
引用: