Heim >Backend-Entwicklung >C#.Net-Tutorial >Umfassende Geheimnisse von C# – Multithreading im Detail

Umfassende Geheimnisse von C# – Multithreading im Detail

高洛峰
高洛峰Original
2016-12-12 15:06:311128Durchsuche

In diesem Artikel wird hauptsächlich die Entwicklung von Multithreading vorgestellt, ausgehend von der grundlegenden Verwendung von Threads, der Entwicklung von Arbeitsthreads und E/A-Threads im CLR-Thread-Pool und dem parallelen Betrieb von PLINQ.
Unter diesen werden die delegierte BeginInvoke-Methode und die Rückruffunktion am häufigsten verwendet.
Der E/A-Thread kann von jedem leicht ignoriert werden. Tatsächlich sollten Sie bei der Entwicklung eines Multithread-Systems der Funktionsweise des E/A-Threads mehr Aufmerksamkeit schenken. Insbesondere in der ASP.NET-Entwicklung achten möglicherweise immer mehr Menschen darauf, Ajax auf der Clientseite oder UpdatePanel auf der Serverseite zu verwenden. Tatsächlich kann die rationelle Verwendung von E/A-Threads den Druck auf IIS bei der Kommunikation von Projekten oder Dateidownloads so weit wie möglich reduzieren.
Parallele Programmierung ist eine asynchrone Betriebsmethode, die in Framework 4.0 stark gefördert wird und es lohnt sich, sie tiefer zu erlernen.
Ich hoffe, dass dieser Artikel für Ihr Studium und Ihre Forschung hilfreich sein kann. Bitte kommentieren Sie etwaige Fehler oder Auslassungen darin.

1. Definition von Thread

1. 1 Die Beziehung zwischen Prozess, Anwendungsdomäne und Thread

Prozess ist ein Grundkonzept im Windows-System, das eine erforderliche Ressource enthält um das Programm auszuführen. Prozesse sind relativ unabhängig. Ein Prozess kann nicht auf die Daten eines anderen Prozesses zugreifen (es sei denn, der Ausfall eines Prozesses hat Auswirkungen auf den Betrieb anderer Prozesse). unabhängige Region. Der Prozess kann als grundlegende Grenze eines Programms verstanden werden.

Die Anwendungsdomäne (AppDomain) ist ein logischer Bereich, in dem ein Programm ausgeführt wird. Sie kann als einfacher Prozess betrachtet werden, der in der Anwendungsdomäne ausgeführt wird Eine Anwendungsdomäne kann auch mehrere Assemblys enthalten. Eine Anwendungsdomäne enthält einen oder mehrere Kontexte, und die Kontext-CLR kann verwendet werden, um den Status bestimmter spezieller Objekte in verschiedenen Containern zu platzieren.

Thread ist die grundlegende Ausführungseinheit im Prozess. Der erste Thread, der am Prozesseintrag ausgeführt wird, wird als Haupt-Thread des Prozesses betrachtet. In .NET-Anwendungen wird die Main()-Methode als Einstiegspunkt verwendet. Wenn diese Methode aufgerufen wird, erstellt das System automatisch einen Hauptthread. Threads bestehen hauptsächlich aus CPU-Registern, Aufrufstapeln und lokalem Thread-Speicher (Thread Local Storage, TLS). Die CPU-Register zeichnen hauptsächlich den Status des aktuell ausgeführten Threads auf, der Aufrufstapel wird hauptsächlich zum Verwalten des vom Thread aufgerufenen Speichers und der Daten verwendet und TLS wird hauptsächlich zum Speichern der Statusinformationen des Threads verwendet.

Die Beziehung zwischen Prozessen, Anwendungsdomänen und Threads ist wie folgt: Ein Prozess kann mehrere Anwendungsdomänen und mehrere Threads umfassen, und Threads können auch zwischen mehreren Anwendungsdomänen wechseln. Gleichzeitig befindet sich der Thread jedoch nur in einer Anwendungsdomäne.

Umfassende Geheimnisse von C# – Multithreading im Detail

1.2 Multi-Threading

In einer Zeiteinheit (Zeitscheibe) eines einzelnen CPU-Systems kann die CPU nur einen einzelnen Thread ausführen, und der Die Ausführungsreihenfolge hängt von der Thread-Prioritätsstufe ab. Wenn der Thread die Ausführung nicht innerhalb der Zeiteinheit abschließen kann, speichert das System die Statusinformationen des Threads im lokalen Speicher (TLS) des Threads, sodass die Ausführung beim nächsten Mal wieder aufgenommen werden kann. Multithreading ist nur eine Illusion, die das System hervorruft. Es wechselt mehrere Threads in mehreren Zeiteinheiten. Da häufig gewechselt wird und die Zeiteinheit sehr kurz ist, können mehrere Threads als gleichzeitig ausgeführt angesehen werden.

Eine angemessene Verwendung von Multithreading kann die Leistung des Systems verbessern. Beispiel: Verwenden Sie Multithreading, wenn das System Daten mit großer Kapazität anfordert, und übergeben Sie die Datenausgabearbeit an asynchrone Threads, damit die Der Hauptthread kann seine Stabilität beibehalten, um andere Probleme zu bewältigen. Beachten Sie jedoch, dass eine übermäßige Verwendung von Multithreads zu einer Leistungseinbuße führt, da die CPU viel Zeit mit dem Wechseln der Threads verbringen muss.

2. Grundkenntnisse über Threads

2.1 System.Threading.Thread-Klasse

System.Threading.Thread ist die Basisklasse, mit der Threads den Strom steuern können Erstellen, Anhalten, Stoppen und Zerstören von Threads in der Anwendungsdomäne.

Es enthält die folgenden allgemeinen öffentlichen Eigenschaften:

Umfassende Geheimnisse von C# – Multithreading im Detail

2.1.1 Thread-ID

ManagedThreadId ist die eindeutige ID des bestätigten Threads. In den meisten Fällen identifizieren Programme Threads über Thread.ManagedThreadId. Name ist ein variabler Wert. Entwickler können den Namen des Threads über das Programm festlegen, dies ist jedoch nur eine Hilfsfunktion.

2.1.2 Prioritätsstufe von Threads

.NET legt das Priority-Attribut für Threads fest, um die Prioritätsstufe der Thread-Ausführung zu definieren, die 5 Optionen enthält, darunter „Normal“. ist der Standardwert. Sofern das System keine besonderen Anforderungen stellt, sollte die Thread-Priorität nicht zufällig festgelegt werden.

Umfassende Geheimnisse von C# – Multithreading im Detail

2.1.3 Thread-Status

ThreadState kann verwendet werden, um zu erkennen, ob sich der Thread im Zustand „Nicht gestartet“, „Schlafend“, „Läuft“ usw. befindet, was weitere Informationen liefern kann Informationen als das IsAlive-Attribut.

Wie bereits erwähnt, kann eine Anwendungsdomäne mehrere Kontexte enthalten, und der aktuelle Kontext des Threads kann über CurrentContext abgerufen werden.

CurrentThread ist das am häufigsten verwendete Attribut, mit dem der aktuell laufende Thread abgerufen wird.

2.1.4 Methoden von System.Threading.Thread

Thread umfasst mehrere Methoden zur Steuerung der Erstellung, Unterbrechung, des Stopps und der Zerstörung von Threads, die später besprochen werden wird oft in Beispielen verwendet.

Umfassende Geheimnisse von C# – Multithreading im Detail

2.1.5 Entwicklungsbeispiel

Das folgende Beispiel zeigt die aktuellen Thread-Informationen über Thread an

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();
        }

Laufergebnisse

Umfassende Geheimnisse von C# – Multithreading im Detail

2.2 System.Threading-Namespace

Stellt mehrere Methoden im System.Threading-Namespace zum Erstellen von Multithread-Anwendungen bereit, wobei ThreadPool und Thread am häufigsten Multithreading-Anwendungen sind In der Entwicklung wird ein CLR-Thread-Pool speziell in .NET eingerichtet, um die Ausführung von Threads zu verwalten. Dieser CLR-Thread-Pool wird über die ThreadPool-Klasse verwaltet. Thread ist die direkteste Möglichkeit, Threads zu verwalten. In den folgenden Abschnitten werden die relevanten Inhalte ausführlich vorgestellt.

Umfassende Geheimnisse von C# – Multithreading im Detail

System.Threading enthält in der folgenden Tabelle mehrere gemeinsame Delegaten, unter denen ThreadStart und ParameterizedThreadStart die am häufigsten verwendeten Delegaten sind.
Der von ThreadStart generierte Thread ist der direkteste Weg, aber der von ThreadStart generierte Thread wird nicht vom Thread-Pool verwaltet.
ParameterizedThreadStart ist für das asynchrone Auslösen von Methoden mit Parametern konzipiert, was im nächsten Abschnitt ausführlich erläutert wird.

Umfassende Geheimnisse von C# – Multithreading im Detail

2.3 Thread-Verwaltungsmethode

Das Erstellen eines neuen Threads über ThreadStart ist die direkteste Methode, aber der auf diese Weise erstellte Thread ist schwierig zu verwalten Das Erstellen zu vieler Threads verringert die Systemleistung. Aus diesem Grund hat .NET speziell einen CLR-Thread-Pool für die Thread-Verwaltung eingerichtet. Durch die Verwendung des CLR-Thread-Pool-Systems kann die Verwendung von Threads sinnvoller verwaltet werden. Alle angeforderten Dienste können im Thread-Pool ausgeführt werden, und der Thread kehrt nach Abschluss des Vorgangs zum Thread-Pool zurück. Durch Einstellungen kann die maximale Anzahl von Threads im Thread-Pool gesteuert werden. Wenn die Anforderung die maximale Anzahl von Threads überschreitet, kann der Thread-Pool entsprechend der Prioritätsstufe des Vorgangs ausgeführt werden, sodass einige Vorgänge im Wartezustand verbleiben Führen Sie die Operation aus, wenn ein Thread zurückkehrt.

Hier werden die Grundkenntnisse vorgestellt. Im Folgenden wird die Entwicklung von Multithreading im Detail vorgestellt.

3. Multithreading mit ThreadStart implementieren

3.1 ThreadStart-Delegat verwenden

Hier ist ein Beispiel, um die Vorteile von Multithreading zu demonstrieren. Erstellen Sie zunächst einen Thread im Nachrichtenklasse Eine Methode ShowMessage(), die die ID des aktuell laufenden Threads anzeigt und die Methode Thread.Sleep (int) verwendet, um einen Teil der Arbeit zu simulieren. Binden Sie in main() die ShowMessage()-Methode des Message-Objekts über den ThreadStart-Delegaten und führen Sie dann die asynchrone Methode über Thread.Start() aus.

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!");
              
          }
      }

Bitte achten Sie auf die laufenden Ergebnisse. Nach dem Aufruf der Thread.Start()-Methode führt das System Message.ShowMessage() asynchron aus, während der Vorgang des Hauptthreads weiterhin ausgeführt wird Message.ShowMessage() ist abgeschlossen, der Hauptthread hat alle Vorgänge abgeschlossen.

Umfassende Geheimnisse von C# – Multithreading im Detail

3.2 Verwendung des ParameterizedThreadStart-Delegaten

Der ParameterizedThreadStart-Delegat ist dem ThreadStart-Delegaten sehr ähnlich, der ParameterizedThreadStart-Delegat ist jedoch für Methoden mit Parametern vorgesehen. Beachten Sie, dass der Parameter der entsprechenden Methode von ParameterizedThreadStart ein Objekt ist. Dieser Parameter kann ein Wertobjekt oder ein benutzerdefiniertes Objekt sein.

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}&#39;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!");
             
        }
    }


Laufendes Ergebnis:

Umfassende Geheimnisse von C# – Multithreading im Detail

3.3 Vordergrund-Thread und Hintergrund-Thread

Beachten Sie, dass keines der beiden oben genannten Beispiele werden Console.ReadKey() verwendet, aber das System wartet immer noch auf den Abschluss des asynchronen Threads, bevor er endet. Dies liegt daran, dass Threads, die mit Thread.Start() gestartet wurden, standardmäßig Vordergrund-Threads verwenden und das System warten muss, bis die Ausführung aller Vordergrund-Threads abgeschlossen ist, bevor die Anwendungsdomäne automatisch entladen wird.

Im zweiten Abschnitt haben wir eingeführt, dass Thread ein Attribut IsBackground hat. Indem Sie dieses Attribut auf true setzen, können Sie den Thread als Hintergrundthread festlegen! Zu diesem Zeitpunkt wird die Anwendungsdomäne entladen, wenn der Hauptthread abgeschlossen ist, ohne auf die Ausführung des asynchronen Threads zu warten.

3.4 Threads anhalten

Um zu warten, bis andere Hintergrundthreads abgeschlossen sind, bevor der Hauptthread beendet wird, können Sie die Methode Thread.Sleep() verwenden.

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);
        }
    }

Die Ausführungsergebnisse sind wie folgt. Zu diesem Zeitpunkt wird die Anwendungsdomäne automatisch beendet, nachdem der Hauptthread 5 Sekunden lang ausgeführt wurde

Umfassende Geheimnisse von C# – Multithreading im Detail

但系统无法预知异步线程需要运行的时间,所以用通过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!");
         }

运行结果如下

Umfassende Geheimnisse von C# – Multithreading im Detail

四、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);
        }
    }

运行结果

Umfassende Geheimnisse von C# – Multithreading im Detail

使用 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);
        }
    }

运行结果

Umfassende Geheimnisse von C# – Multithreading im Detail

通过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();
         }
     }

委托类包括以下几个重要方法

Umfassende Geheimnisse von C# – Multithreading im Detail

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);
        }
    }

运行结果

Umfassende Geheimnisse von C# – Multithreading im Detail

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);
        }
    }

运行结果:

Umfassende Geheimnisse von C# – Multithreading im Detail

除此以外,也可以使用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方法获取计算结果。
运行结果如下:

Umfassende Geheimnisse von C# – Multithreading im Detail

如果想为回调函数传送一些外部信息,就可以利用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 + "&#39;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);
        }
    }

运行结果:

Umfassende Geheimnisse von C# – Multithreading im Detail

关于I/O线程、SqlCommand多线程查询、PLINQ、定时器与锁的内容将在C#综合揭秘——细说多线程(下)中详细介绍。

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn