집 >백엔드 개발 >C#.Net 튜토리얼 >C#의 포괄적인 비밀 - 멀티스레딩 자세히 알아보기
이 글에서는 스레드의 기본 사용법부터 멀티스레딩 개발, CLR 스레드 풀의 작업자 스레드 및 I/O 스레드 개발, PLINQ의 병렬 연산을 주로 소개합니다.
그 중 위임된 BeginInvoke 메서드와 콜백 함수가 가장 일반적으로 사용됩니다.
I/O 스레드는 누구나 쉽게 무시할 수 있습니다. 사실 멀티 스레드 시스템을 개발할 때는 I/O 스레드의 작동에 더 주의를 기울여야 합니다. 특히 ASP.NET 개발에서는 클라이언트 측에서 Ajax를 사용하거나 서버 측에서 UpdatePanel을 사용하는 것에만 관심을 두는 사람들이 많아졌습니다. 실제로 I/O 스레드를 합리적으로 사용하면 프로젝트 통신이나 파일 다운로드 시 IIS에 대한 부담을 최대한 줄일 수 있습니다.
병렬 프로그래밍은 Framework 4.0에서 강력하게 권장되는 비동기 작업 방법으로, 더 깊이 배울 가치가 있습니다.
이 기사가 귀하의 연구와 연구에 도움이 되기를 바랍니다. 기사에 오류나 누락이 있으면 지적해 주십시오.
1. 스레드 정의
1. 1 프로세스, 애플리케이션 도메인, 스레드의 관계
프로세스는 Windows 시스템의 기본 개념으로, 필요한 리소스를 보유합니다. 프로그램을 실행합니다. 프로세스는 상대적으로 독립적입니다. (분산 컴퓨팅을 사용하지 않는 한) 한 프로세스는 다른 프로세스의 작업에 영향을 주지 않습니다. 독립지역. 프로세스는 프로그램의 기본 경계로 이해될 수 있습니다.
애플리케이션 도메인(AppDomain)은 프로그램이 실행되는 논리적 영역입니다. 애플리케이션 도메인에서 실행되는 .NET 어셈블리는 여러 애플리케이션 도메인을 포함할 수 있습니다. 애플리케이션 도메인에는 여러 어셈블리가 포함될 수도 있습니다. 애플리케이션 도메인에는 하나 이상의 컨텍스트가 포함되어 있으며 컨텍스트 CLR을 사용하여 특정 특수 개체의 상태를 다른 컨테이너에 배치할 수 있습니다.
스레드는 프로세스의 기본 실행 단위입니다. 프로세스 항목에서 실행되는 첫 번째 스레드는 프로세스의 기본 스레드로 간주됩니다. .NET 애플리케이션에서는 Main() 메서드가 진입점으로 사용됩니다. 이 메서드가 호출되면 시스템이 자동으로 기본 스레드를 생성합니다. 스레드는 주로 CPU 레지스터, 호출 스택, 스레드 로컬 저장소(Thread Local Storage, TLS)로 구성됩니다. CPU 레지스터는 주로 현재 실행 중인 스레드의 상태를 기록하고, 호출 스택은 주로 스레드가 호출하는 메모리와 데이터를 유지하는 데 사용되며, TLS는 스레드의 상태 정보를 저장하는 데 주로 사용됩니다.
프로세스, 애플리케이션 도메인, 스레드 간의 관계는 아래와 같습니다. 프로세스에는 여러 애플리케이션 도메인과 여러 스레드가 포함될 수 있으며 스레드는 여러 애플리케이션 도메인 간에 이동할 수도 있습니다. 그러나 동시에 스레드는 하나의 애플리케이션 도메인에만 존재합니다.
1.2 멀티스레딩
단일 CPU 시스템의 단위 시간(타임 슬라이스)에서 CPU는 단일 스레드만 실행할 수 있으며, 실행 순서는 스레드 우선 순위 수준에 따라 다릅니다. 스레드가 단위 시간 내에 실행을 완료하지 못하면 시스템은 스레드의 상태 정보를 스레드의 로컬 저장소(TLS)에 저장하여 다음에 실행을 재개할 수 있도록 합니다. 멀티스레딩은 여러 시간 단위로 여러 스레드를 전환하는 시스템에 의한 환상일 뿐입니다. 전환이 빈번하고 단위 시간이 매우 짧기 때문에 여러 스레드가 동시에 실행되는 것으로 간주할 수 있습니다.
멀티스레딩을 적절하게 사용하면 시스템 성능이 향상될 수 있습니다. 예를 들어 시스템이 대용량 데이터를 요청할 때 멀티스레딩을 사용하고 데이터 출력 작업을 비동기 스레드에 넘겨줍니다. 메인 스레드는 다른 문제를 처리하기 위해 안정성을 유지할 수 있습니다. 하지만 한 가지 주의할 점은 CPU가 스레드를 전환하는 데 많은 시간을 소비해야 하기 때문에 멀티 스레드를 과도하게 사용하면 성능이 저하된다는 점입니다.
2. 스레드 기본 지식
2.1 System.Threading.Thread 클래스
System.Threading.Thread는 스레드를 제어하는 데 사용되는 기본 클래스입니다. 애플리케이션 도메인에서 스레드의 생성, 일시 중단, 중지 및 소멸입니다.
여기에는 다음과 같은 공통 공용 속성이 포함됩니다.
2.1.1 스레드 식별자
ManagedThreadId는 확인된 스레드의 고유 식별자입니다. 대부분의 경우 프로그램은 Thread.ManagedThreadId를 통해 스레드를 식별합니다. Name은 변수 값입니다. 기본적으로 Name은 비어 있는 값입니다. 개발자는 프로그램을 통해 스레드 이름을 설정할 수 있지만 이는 보조 기능일 뿐입니다.
2.1.2 스레드의 우선 순위 수준
.NET은 스레드에 대한 우선 순위 속성을 설정하여 스레드 실행의 우선 순위 수준을 정의합니다. 여기에는 5가지 옵션이 포함됩니다. 기본값입니다. 시스템에 특별한 요구 사항이 없는 한 스레드 우선 순위를 임의로 설정해서는 안 됩니다.
2.1.3 스레드 상태
ThreadState는 스레드가 Unstarted, Sleeping, Running 등의 상태인지 감지하는 데 사용할 수 있습니다. IsAlive 속성보다 훨씬 구체적인 정보입니다.
앞서 언급했듯이 애플리케이션 도메인에는 여러 컨텍스트가 포함될 수 있으며 스레드의 현재 컨텍스트는 CurrentContext를 통해 얻을 수 있습니다.
CurrentThread는 현재 실행 중인 스레드를 가져오는 데 사용되는 가장 일반적으로 사용되는 속성입니다.
2.1.4 System.Threading.Thread 메서드
Thread에는 스레드의 생성, 일시 중지, 중지 및 소멸을 제어하는 여러 메서드가 포함되어 있습니다. 이에 대해서는 나중에 설명하겠습니다. 예문에서 자주 사용됩니다.
2.1.5 개발 예시
다음은 Thread를 통해 현재 Thread 정보를 출력하는 예시
static void Main(string[] args) { Thread thread = Thread.CurrentThread; thread.Name = "Main Thread"; string threadMessage = string.Format("Thread ID:{0}\n Current AppDomainId:{1}\n "+ "Current ContextId:{2}\n Thread Name:{3}\n "+ "Thread State:{4}\n Thread Priority:{5}\n", thread.ManagedThreadId, Thread.GetDomainID(), Thread.CurrentContext.ContextID, thread.Name, thread.ThreadState, thread.Priority); Console.WriteLine(threadMessage); Console.ReadKey(); }
실행 결과
2.2 System.Threading 네임스페이스
System.Threading 네임스페이스에서 다중 스레드 애플리케이션을 구축하기 위한 여러 메서드를 제공하며, 그중 ThreadPool 및 Thread가 다중 스레드입니다. 개발에 사용되는 CLR 스레드 풀은 스레드 실행을 관리하기 위해 .NET에서 특별히 설정됩니다. 이 CLR 스레드 풀은 ThreadPool 클래스를 통해 관리됩니다. 스레드는 스레드를 관리하는 가장 직접적인 방법입니다. 다음 섹션에서는 관련 내용을 자세히 소개합니다.
System.Threading에는 아래 표에 여러 개의 공통 대리자가 포함되어 있으며, 그중 ThreadStart 및 ParameterizedThreadStart가 가장 일반적으로 사용되는 대리자입니다.
ThreadStart에 의해 생성된 스레드가 가장 직접적인 방법이지만 ThreadStart에 의해 생성된 스레드는 스레드 풀에서 관리되지 않습니다.
ParameterizedThreadStart는 매개변수를 사용하여 메소드를 비동기적으로 트리거하도록 설계되었습니다. 이에 대해서는 다음 섹션에서 자세히 설명합니다.
2.3 스레드 관리 방법
ThreadStart를 통해 새로운 스레드를 생성하는 것이 가장 직접적인 방법이지만 이렇게 생성된 스레드는 관리하기 어렵다. 스레드를 너무 많이 생성하면 시스템 성능이 저하됩니다. 이를 고려하여 .NET에서는 스레드 관리를 위해 특별히 CLR 스레드 풀을 설정했습니다. CLR 스레드 풀 시스템을 사용하면 스레드 사용을 보다 합리적으로 관리할 수 있습니다. 요청된 모든 서비스는 스레드 풀에서 실행될 수 있으며, 작업이 완료되면 스레드는 스레드 풀로 반환됩니다. 설정을 통해 스레드 풀의 최대 스레드 수를 제어할 수 있습니다. 요청이 최대 스레드 수를 초과하면 스레드 풀은 작업의 우선 순위 수준에 따라 실행될 수 있으며 일부 작업은 대기 상태로 유지됩니다. 스레드가 반환될 때 작업을 실행합니다.
여기에서는 기본 지식을 소개합니다. 다음은 멀티스레딩의 개발을 자세히 소개합니다.
3. ThreadStart로 멀티스레딩 구현
3.1 ThreadStart 대리자 사용
다음은 멀티스레딩의 이점을 보여주는 예입니다. 메시지 클래스 현재 실행 중인 스레드의 ID를 표시하고 Thread.Sleep(int) 메서드를 사용하여 작업의 일부를 시뮬레이션하는 ShowMessage() 메서드입니다. main()에서 ThreadStart 대리자를 통해 Message 개체의 ShowMessage() 메서드를 바인딩한 다음 Thread.Start()를 통해 비동기 메서드를 실행합니다.
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 < 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 ThreadStart(message.ShowMessage)); thread.Start(); Console.WriteLine("Do something ..........!"); Console.WriteLine("Main thread working is complete!"); } }
Thread.Start() 메서드를 호출한 후 시스템은 Message.ShowMessage()에서 기본 스레드 작업을 계속하는 동안 비동기적으로 실행됩니다. )이 완료되면 메인 스레드가 모든 작업을 완료했습니다.
3.2 ParameterizedThreadStart 대리자 사용
ParameterizedThreadStart 대리자는 ThreadStart 대리자와 매우 유사하지만 ParameterizedThreadStart 대리자는 매개 변수가 있는 메서드용입니다. ParameterizedThreadStart의 해당 메소드 매개변수는 object입니다. 이 매개변수는 값 개체 또는 사용자 정의 개체일 수 있습니다.
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.Start(person); //启动异步线程 Console.WriteLine("Do something ..........!"); Console.WriteLine("Main thread working is complete!"); } }
실행 결과:
3.3 포그라운드 스레드 및 백그라운드 스레드
위의 두 가지 예는 모두 아닙니다. Console.ReadKey()가 사용되지만 시스템은 비동기 스레드가 끝나기 전에 완료될 때까지 기다립니다. Thread.Start()를 사용하여 시작된 스레드는 기본적으로 포그라운드 스레드로 설정되고 시스템은 애플리케이션 도메인이 자동으로 언로드되기 전에 모든 포그라운드 스레드의 실행이 완료될 때까지 기다려야 하기 때문입니다.
두 번째 섹션에서는 스레드에 IsBackground 속성이 있음을 소개했습니다. 이 속성을 true로 설정하면 스레드를 백그라운드 스레드로 설정할 수 있습니다. 이때 비동기 스레드가 실행될 때까지 기다리지 않고 메인 스레드가 완료되면 애플리케이션 도메인이 언로드됩니다.
3.4 스레드 일시 중단
메인 스레드를 종료하기 전에 다른 백그라운드 스레드가 완료될 때까지 기다리려면 Thread.Sleep() 메서드를 사용할 수 있습니다.
public class Message { public void ShowMessage() { string message = string.Format("\nAsync threadId is:{0}", 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 ThreadStart(message.ShowMessage)); thread.IsBackground = true; thread.Start(); Console.WriteLine("Do something ..........!"); Console.WriteLine("Main thread working is complete!"); Console.WriteLine("Main thread sleep!"); Thread.Sleep(5000); } }
이 때, 메인 스레드가 5초 동안 실행된 후 애플리케이션 도메인이 자동으로 종료됩니다.
但系统无法预知异步线程需要运行的时间,所以用通过Thread.Sleep(int)阻塞主线程并不是一个好的解决方法。有见及此,.NET专门为等待异步线程完成开发了另一个方法thread.Join()。把上面例子中的最后一行Thread.Sleep(5000)修改为 thread.Join() 就能保证主线程在异步线程thread运行结束后才会终止。
3.5 Suspend 与 Resume (慎用)
Thread.Suspend()与 Thread.Resume()是在Framework1.0 就已经存在的老方法了,它们分别可以挂起、恢复线程。但在Framework2.0中就已经明确排斥这两个方法。这是因为一旦某个线程占用了已有的资源,再使用Suspend()使线程长期处于挂起状态,当在其他线程调用这些资源的时候就会引起死锁!所以在没有必要的情况下应该避免使用这两个方法。
3.6 终止线程
若想终止正在运行的线程,可以使用Abort()方法。在使用Abort()的时候,将引发一个特殊异常 ThreadAbortException 。
若想在线程终止前恢复线程的执行,可以在捕获异常后 ,在catch(ThreadAbortException ex){...} 中调用Thread.ResetAbort()取消终止。
而使用Thread.Join()可以保证应用程序域等待异步线程结束后才终止运行。
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!"); }
运行结果如下
四、CLR线程池的工作者线程
4.1 关于CLR线程池
使用ThreadStart与ParameterizedThreadStart建立新线程非常简单,但通过此方法建立的线程难于管理,若建立过多的线程反而会影响系统的性能。
有见及此,.NET引入CLR线程池这个概念。CLR线程池并不会在CLR初始化的时候立刻建立线程,而是在应用程序要创建线程来执行任务时,线程池才初始化一个线程。线程的初始化与其他的线程一样。在完成任务以后,该线程不会自行销毁,而是以挂起的状态返回到线程池。直到应用程序再次向线程池发出请求时,线程池里挂起的线程就会再度激活执行任务。这样既节省了建立线程所造成的性能损耗,也可以让多个任务反复重用同一线程,从而在应用程序生存期内节约大量开销。
注意:通过CLR线程池所建立的线程总是默认为后台线程,优先级数为ThreadPriority.Normal。
4.2 工作者线程与I/O线程
CLR线程池分为工作者线程(workerThreads)与I/O线程 (completionPortThreads) 两种,工作者线程是主要用作管理CLR内部对象的运作,I/O(Input/Output) 线程顾名思义是用于与外部系统交换信息,IO线程的细节将在下一节详细说明。
通过ThreadPool.GetMax(out int workerThreads,out int completionPortThreads )和 ThreadPool.SetMax( int workerThreads, int completionPortThreads)两个方法可以分别读取和设置CLR线程池中工作者线程与I/O线程的最大线程数。在Framework2.0中最大线程默认为25*CPU数,在Framewok3.0、4.0中最大线程数默认为250*CPU数,在近年 I3,I5,I7 CPU出现后,线程池的最大值一般默认为1000、2000。
若想测试线程池中有多少的线程正在投入使用,可以通过ThreadPool.GetAvailableThreads( out intworkerThreads,out int completionPortThreads ) 方法。
使用CLR线程池的工作者线程一般有两种方式,一是直接通过 ThreadPool.QueueUserWorkItem() 方法,二是通过委托,下面将逐一细说。
4.3 通过QueueUserWorkItem启动工作者线程
ThreadPool线程池中包含有两个静态方法可以直接启动工作者线程:
一为 ThreadPool.QueueUserWorkItem(WaitCallback)
二为 ThreadPool.QueueUserWorkItem(WaitCallback,Object)
先把WaitCallback委托指向一个带有Object参数的无返回值方法,再使用 ThreadPool.QueueUserWorkItem(WaitCallback) 就可以异步启动此方法,此时异步方法的参数被视为null 。
class Program { static void Main(string[] args) { //把CLR线程池的最大值设置为1000 ThreadPool.SetMaxThreads(1000, 1000); //显示主线程启动时线程池信息 ThreadMessage("Start"); //启动工作者线程 ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncCallback)); Console.ReadKey(); } static void AsyncCallback(object state) { Thread.Sleep(200); ThreadMessage("AsyncCallback"); Console.WriteLine("Async thread do work!"); } //显示线程现状 static void ThreadMessage(string data) { string message = string.Format("{0}\n CurrentThreadId is {1}", data, Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); } }
运行结果
使用 ThreadPool.QueueUserWorkItem(WaitCallback,Object) 方法可以把object对象作为参数传送到回调函数中。
下面例子中就是把一个string对象作为参数发送到回调函数当中。
class Program { static void Main(string[] args) { //把线程池的最大值设置为1000 ThreadPool.SetMaxThreads(1000, 1000); ThreadMessage("Start"); 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); } //显示线程现状 static void ThreadMessage(string data) { string message = string.Format("{0}\n CurrentThreadId is {1}", data, Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); } }
运行结果
通过ThreadPool.QueueUserWorkItem启动工作者线程虽然是方便,但WaitCallback委托指向的必须是一个带有Object参数的无返回值方法,这无疑是一种限制。若方法需要有返回值,或者带有多个参数,这将多费周折。有见及此,.NET提供了另一种方式去建立工作者线程,那就是委托。
4.4 委托类
使用CLR线程池中的工作者线程,最灵活最常用的方式就是使用委托的异步方法,在此先简单介绍一下委托类。
当定义委托后,.NET就会自动创建一个代表该委托的类,下面可以用反射方式显示委托类的方法成员(对反射有兴趣的朋友可以先参考一下“.NET基础篇——反射的奥妙”)
class Program { delegate void MyDelegate(); static void Main(string[] args) { MyDelegate delegate1 = new MyDelegate(AsyncThread); //显示委托类的几个方法成员 var methods=delegate1.GetType().GetMethods(); if (methods != null) foreach (MethodInfo info in methods) Console.WriteLine(info.Name); Console.ReadKey(); } }
委托类包括以下几个重要方法
public class MyDelegate:MulticastDelegate { public MyDelegate(object target, int methodPtr); //调用委托方法 public virtual void Invoke(); //异步委托 public virtual IAsyncResult BeginInvoke(AsyncCallback callback,object state); public virtual void EndInvoke(IAsyncResult result); }
当调用Invoke()方法时,对应此委托的所有方法都会被执行。而BeginInvoke与EndInvoke则支持委托方法的异步调用,由BeginInvoke启动的线程都属于CLR线程池中的工作者线程,在下面将详细说明。
4.5 利用BeginInvoke与EndInvoke完成异步委托方法
首先建立一个委托对象,通过IAsyncResult BeginInvoke(string name,AsyncCallback callback,object state) 异步调用委托方法,BeginInvoke 方法除最后的两个参数外,其它参数都是与方法参数相对应的。通过 BeginInvoke 方法将返回一个实现了 System.IAsyncResult 接口的对象,之后就可以利用EndInvoke(IAsyncResult ) 方法就可以结束异步操作,获取委托的运行结果。
class Program { delegate string MyDelegate(string name); static void Main(string[] args) { ThreadMessage("Main Thread"); //建立委托 MyDelegate myDelegate = new MyDelegate(Hello); //异步调用委托,获取计算结果 IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null); //完成主线程其他工作 ............. //等待异步方法完成,调用EndInvoke(IAsyncResult)获取运行结果 string data=myDelegate.EndInvoke(result); Console.WriteLine(data); Console.ReadKey(); } static string Hello(string name) { ThreadMessage("Async Thread"); Thread.Sleep(2000); //虚拟异步工作 return "Hello " + name; } //显示当前线程 static void ThreadMessage(string data) { string message = string.Format("{0}\n ThreadId is:{1}", data,Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); } }
运行结果
4.6 善用IAsyncResult
在以上例子中可以看见,如果在使用myDelegate.BeginInvoke后立即调用myDelegate.EndInvoke,那在异步线程未完成工作以前主线程将处于阻塞状态,等到异步线程结束获取计算结果后,主线程才能继续工作,这明显无法展示出多线程的优势。此时可以好好利用IAsyncResult 提高主线程的工作性能,IAsyncResult有以下成员
public interface IAsyncResult { object AsyncState {get;} //获取用户定义的对象,它限定或包含关于异步操作的信息。 WailHandle AsyncWaitHandle {get;} //获取用于等待异步操作完成的 WaitHandle。 bool CompletedSynchronously {get;} //获取异步操作是否同步完成的指示。 bool IsCompleted {get;} //获取异步操作是否已完成的指示。 }
通过轮询方式,使用IsCompleted属性判断异步操作是否完成,这样在异步操作未完成前就可以让主线程执行另外的工作。
class Program { delegate string MyDelegate(string name); static void Main(string[] args) { ThreadMessage("Main Thread"); //建立委托 MyDelegate myDelegate = new MyDelegate(Hello); //异步调用委托,获取计算结果 IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null); //在异步线程未完成前执行其他工作 while (!result.IsCompleted) { Thread.Sleep(200); //虚拟操作 Console.WriteLine("Main thead do work!"); } string data=myDelegate.EndInvoke(result); Console.WriteLine(data); Console.ReadKey(); } static string Hello(string name) { ThreadMessage("Async Thread"); Thread.Sleep(2000); return "Hello " + name; } static void ThreadMessage(string data) { string message = string.Format("{0}\n ThreadId is:{1}", data,Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); } }
运行结果:
除此以外,也可以使用WailHandle完成同样的工作,WaitHandle里面包含有一个方法WaitOne(int timeout),它可以判断委托是否完成工作,在工作未完成前主线程可以继续其他工作。运行下面代码可得到与使用 IAsyncResult.IsCompleted 同样的结果,而且更简单方便 。
namespace Test { class Program { delegate string MyDelegate(string name); static void Main(string[] args) { ThreadMessage("Main Thread"); //建立委托 MyDelegate myDelegate = new MyDelegate(Hello); //异步调用委托,获取计算结果 IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null); while (!result.AsyncWaitHandle.WaitOne(200)) { Console.WriteLine("Main thead do work!"); } string data=myDelegate.EndInvoke(result); Console.WriteLine(data); Console.ReadKey(); } static string Hello(string name) { ThreadMessage("Async Thread"); Thread.Sleep(2000); return "Hello " + name; } static void ThreadMessage(string data) { string message = string.Format("{0}\n ThreadId is:{1}", data,Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); } }
当要监视多个运行对象的时候,使用IAsyncResult.WaitHandle.WaitOne可就派不上用场了。
幸好.NET为WaitHandle准备了另外两个静态方法:WaitAny(waitHandle[], int)与WaitAll (waitHandle[] , int)。
其中WaitAll在等待所有waitHandle完成后再返回一个bool值。
而WaitAny是等待其中一个waitHandle完成后就返回一个int,这个int是代表已完成waitHandle在waitHandle[]中的数组索引。
下面就是使用WaitAll的例子,运行结果与使用 IAsyncResult.IsCompleted 相同。
class Program { delegate string MyDelegate(string name); static void Main(string[] args) { ThreadMessage("Main Thread"); //建立委托 MyDelegate myDelegate = new MyDelegate(Hello); //异步调用委托,获取计算结果 IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null); //此处可加入多个检测对象 WaitHandle[] waitHandleList = new WaitHandle[] { result.AsyncWaitHandle,........ }; while (!WaitHandle.WaitAll(waitHandleList,200)) { Console.WriteLine("Main thead do work!"); } string data=myDelegate.EndInvoke(result); Console.WriteLine(data); Console.ReadKey(); } static string Hello(string name) { ThreadMessage("Async Thread"); Thread.Sleep(2000); return "Hello " + name; } static void ThreadMessage(string data) { string message = string.Format("{0}\n ThreadId is:{1}", data,Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); } }
4.7 回调函数
使用轮询方式来检测异步方法的状态非常麻烦,而且效率不高,有见及此,.NET为 IAsyncResult BeginInvoke(AsyncCallback , object)准备了一个回调函数。使用 AsyncCallback 就可以绑定一个方法作为回调函数,回调函数必须是带参数 IAsyncResult 且无返回值的方法: void AsycnCallbackMethod(IAsyncResult result) 。在BeginInvoke方法完成后,系统就会调用AsyncCallback所绑定的回调函数,最后回调函数中调用 XXX EndInvoke(IAsyncResult result) 就可以结束异步方法,它的返回值类型与委托的返回值一致。
class Program { delegate string MyDelegate(string name); static void Main(string[] args) { ThreadMessage("Main Thread"); //建立委托 MyDelegate myDelegate = new MyDelegate(Hello); //异步调用委托,获取计算结果 myDelegate.BeginInvoke("Leslie", new AsyncCallback(Completed), null); //在启动异步线程后,主线程可以继续工作而不需要等待 for (int n = 0; n < 6; n++) Console.WriteLine(" Main thread do work!"); Console.WriteLine(""); Console.ReadKey(); } static string Hello(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); Console.WriteLine(data); } static void ThreadMessage(string data) { string message = string.Format("{0}\n ThreadId is:{1}", data, Thread.CurrentThread.ManagedThreadId); Console.WriteLine(message); } }
可以看到,主线在调用BeginInvoke方法可以继续执行其他命令,而无需再等待了,这无疑比使用轮询方式判断异步方法是否完成更有优势。
在异步方法执行完成后将会调用AsyncCallback所绑定的回调函数,注意一点,回调函数依然是在异步线程中执行,这样就不会影响主线程的运行,这也使用回调函数最值得青昧的地方。
在回调函数中有一个既定的参数IAsyncResult,把IAsyncResult强制转换为AsyncResult后,就可以通过 AsyncResult.AsyncDelegate 获取原委托,再使用EndInvoke方法获取计算结果。
运行结果如下:
如果想为回调函数传送一些外部信息,就可以利用BeginInvoke(AsyncCallback,object)的最后一个参数object,它允许外部向回调函数输入任何类型的参数。只需要在回调函数中利用 AsyncResult.AsyncState 就可以获取object对象。
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(Hello); //建立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 Hello(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); } }
运行结果:
关于I/O线程、SqlCommand多线程查询、PLINQ、定时器与锁的内容将在C#综合揭秘——细说多线程(下)中详细介绍。