第二十一篇 .NET高級技術之使用多線程(三)

1.  單元模式和 Windows Forms

       單元模式線程是一個自動線程安全機制, 很是貼近於COM——Microsoft的遺留下的組件對象模型。儘管.NET最大地放棄擺脫了遺留下的模型,但不少時候它也會忽然出現,這是由於有必要 與舊的API 進行通訊。單元模式線程與Windows Forms最相關,由於大多Windows Forms使用或包裝了長期存在的Win32 API——連同它的單元傳統。php

       單元是多線程的邏輯上的「容器」,單元產生兩種容量——「單的」和「多的」。單線 程單元只包含一個線程;多線程單元能夠包含任何數量的線程。單線程模式更廣泛 而且能與二者有互操做性。web

       就像包含線程同樣,單元也包含對象,當對象在一個單元內被建立後,在它的生命週期中它將一直存在在那,永遠也「居家不出」地與那些駐留線程在一塊兒。這相似 於被包含在.NET 同步環境中 ,除了同步環境中沒有本身的或包含線程。任何線程能夠訪問在任何同步環境中的對象 ——在排它鎖的控制中。可是單元內的對象只有單元內的線程才能夠訪問。緩存

       想象一個圖書館,每本書都象徵着一個對象;借出書是不被容許的,書都在圖書館 建立並直到它壽終正寢。此外,咱們用一我的來象徵一個線程。安全

       一個同步內容的圖書館容許任何人進入,同時同一時刻只容許一我的進入,在圖書館外會造成隊列。服務器

       單元模式的圖書館有常駐維護人員——對於單線程模式的圖書館有一個圖書管理員, 對於多線程模式的圖書館則有一個團隊的管理員。沒人被容許除了隸屬與維護人員的人 ——資助人想要完成研究就必須給圖書管理員發信號,而後告訴管理員去作工做!給管理員發信號被稱爲調度編組——資助人經過調度把方法依次讀出給一個隸屬管 理員的人(或,某個隸屬管理員的人!)。 調度編組是自動的,在Windows Forms經過信息泵被實如今庫結尾。這就是操做系統常常檢查鍵盤和鼠標的機制。若是信息到達的太快了,以至不能被處理,它們將造成消息隊列,因此它們可 以以它們到達的順序被處理。cookie

 

1.1  定義單元模式多線程

 

        .NET線程在進入單元核心Win32或舊的COM代碼前自動地給單元賦值,它被默認地指定爲多線程單元模式,除非須要一個單線程單元模式,就像下面的同樣:併發

1
2
Thread t = new  Thread (...);
t.SetApartmentState (ApartmentState.STA);

        你也能夠用STAThread特性標在主線程上來讓它與單線程單元相結合:dom

1
2
3
4
class  Program {
   [STAThread]
static  void  Main() {
   ...

        線程單元設置對純.NET代碼沒有效果,換言之,即便兩個線程都有STA 的單元狀態,也能夠被相同的對象同時調用相同的方法,就沒有自動的信號編組或鎖定發生了, 只有在執行非託管的代碼時,這纔會發生。異步

在System.Windows.Forms名稱空間下的類型,普遍地調用Win32代碼, 在單線程單元下工做。因爲這個緣由,一個Windos Forms程序應該在它的主方法上貼上 [STAThread]特性,除非在執行Win32 UI代碼以前如下兩者之一發生了:

  • 它將調度編組成一個單線程單元
  • 它將崩潰

 

1.2  Control.Invoke

 

在多線程的Windows Forms程序中,經過非建立控件的線程調用控件的的屬性和方法是非法的。全部跨進程的調用必須被明確地排列至建立控件的線程中(一般爲主線程),利用 Control.Invoke 或 Control.BeginInvoke方法。你不能依賴自動調度編組由於它發生的太晚了,僅當執行恰好進入了非託管的代碼它才發生,而.NET已有足夠 的時間來運行「錯誤的」線程代碼,那些非線程安全的代碼。

一個優秀的管理Windows Forms程序的方案是使用BackgroundWorker, 這個類包裝了須要報道進度和完成度的工做線程,並自動地調用Control.Invoke方法做爲須要。

 

1.3  BackgroundWorker

 

BackgroundWorker是一個在System.ComponentModel命名空間 下幫助類,它管理着工做線程。它提供瞭如下特性:

  • "cancel" 標記,對於給工做線程打信號讓它結束而沒有使用 Abort的狀況
  • 提供報道進度,完成度和退出的標準方案
  • 實現了IComponent接口,容許它參與Visual Studio設計器
  • 在工做線程之上作異常處理
  • 更新Windows Forms控件以應答工做進度或完成度的能力

     最後兩個特性是至關地有用:意味着你再也不須要將try/catch語句塊放到 你的工做線程中了,而且更新Windows Forms控件不須要調用 Control.Invoke了。BackgroundWorker使用線程池工做, 對於每一個新任務,它循環使用避免線程們獲得休息。這意味着你不能在 BackgroundWorker線程上調用 Abort了。

     下面是使用BackgroundWorker最少的步驟:

  • 實例化 BackgroundWorker,爲DoWork事件增長委託。
  • 調用RunWorkerAsync方法,使用一個隨便的object參數。

     這就設置好了它,任何被傳入RunWorkerAsync的參數將經過事件參數的Argument屬性,傳到DoWork事件委託的方法中,下面是例子:

1
2
3
4
5
6
7
8
9
10
11
12
class  Program {
s   tatic BackgroundWorker bw = new  BackgroundWorker();
static  void  Main() {
         bw.DoWork += bw_DoWork;
         bw.RunWorkerAsync ( "Message to worker" );    
     Console.ReadLine();
   }
static  void  bw_DoWork ( object  sender, DoWorkEventArgs e) {
// 這被工做線程調用
     Console.WriteLine (e.Argument);        // 寫"Message to worker"
     // 執行耗時的任務...
   }

      BackgroundWorker也提供了RunWorkerCompleted事件,它在DoWork事件完成後觸發,處理 RunWorkerCompleted事件並非強制的,可是爲了查詢到DoWork中的異常,你一般會這麼作的。RunWorkerCompleted 中的代碼能夠更新Windows Forms 控件,而不用顯示的信號編組,而DoWork中就能夠這麼作。

添加進程報告支持:

  • 設置WorkerReportsProgress屬性爲true
  • 在DoWork中使用「完成百分比」週期地調用ReportProgress方法,以及可選用戶狀態對象
  • 處理ProgressChanged事件,查詢它的事件參數的 ProgressPercentage屬性

      ProgressChanged中的代碼就像RunWorkerCompleted同樣能夠自由地與UI控件進行交互,這在更性進度欄尤其有用。

添加退出報告支持:

  • 設置WorkerSupportsCancellation屬性爲true
  • 在DoWork中週期地檢查CancellationPending屬性:若是爲true,就設置事件參數的Cancel屬性爲true,而後返 回。(工做線程可能會設置Cancel爲true,而且不經過CancellationPending進行提示——若是斷定工做太過困難而且它不能繼續運 行)
  • 調用CancelAsync來請求退出

下面的例子實現了上面描述的特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using  System;
using  System.Threading;
using  System.ComponentModel;
  
class  Program {
   static  BackgroundWorker bw;
   static  void  Main() {
     bw = new  BackgroundWorker();
     bw.WorkerReportsProgress = true ;
     bw.WorkerSupportsCancellation = true ;
 
     bw.DoWork += bw_DoWork;
     bw.ProgressChanged += bw_ProgressChanged;
     bw.RunWorkerCompleted += bw_RunWorkerCompleted;
  
     bw.RunWorkerAsync ( "Hello to worker" );
     
     Console.WriteLine ( "Press Enter in the next 5 seconds to cancel" );
     Console.ReadLine();
     if  (bw.IsBusy) bw.CancelAsync();
     Console.ReadLine();
   }
  
   static  void  bw_DoWork ( object  sender, DoWorkEventArgs e) {
     for  ( int  i = 0; i <= 100; i += 20) {
       if  (bw.CancellationPending) {
         e.Cancel = true ;
         return ;
       }
       bw.ReportProgress (i);
       Thread.Sleep (1000);
     }
     e.Result = 123;    // This gets passed to RunWorkerCompleted
   }
  
   static  void  bw_RunWorkerCompleted ( object  sender,
   RunWorkerCompletedEventArgs e) {
     if  (e.Cancelled)
       Console.WriteLine ( "You cancelled!" );
     else  if  (e.Error != null )
       Console.WriteLine ( "Worker exception: "  + e.Error.ToString());
     else
       Console.WriteLine ( "Complete - "  + e.Result);      // from DoWork
   }
  
   static  void  bw_ProgressChanged ( object  sender,
   ProgressChangedEventArgs e) {
     Console.WriteLine ( "Reached "  + e.ProgressPercentage + "%" );
   }
}

image

 

1.4  BackgroundWorker的子類

  

       BackgroundWorker不是密封類,它提供OnDoWork爲虛方法,暗示着另外一個模式能夠它。 當寫一個可能耗時的方法,你能夠或最好寫個返回BackgroundWorker子類的等方法,預配置完成異步的工做。使用者只要處理 RunWorkerCompleted事件和ProgressChanged事件。好比,設想咱們寫一個耗時 的方法叫作GetFinancialTotals:

1
2
3
4
5
public  class  Client {
   Dictionary < string , int > GetFinancialTotals ( int  foo, int  bar) { ... }
   ...
 
}

      咱們能夠如此來實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public  class  Client {
   public  FinancialWorker GetFinancialTotalsBackground ( int  foo, int  bar) {
     return  new  FinancialWorker (foo, bar);
   }
}
  
public  class  FinancialWorker : BackgroundWorker {
   public  Dictionary < string , int > Result;   // We can add typed fields.
   public  volatile  int  Foo, Bar;            // We could even expose them
                                            // via properties with locks!
   public  FinancialWorker() {
     WorkerReportsProgress = true ;
     WorkerSupportsCancellation = true ;
   }
  
   public  FinancialWorker ( int  foo, int  bar) : this () {
     this .Foo = foo; this .Bar = bar;
   }
  
   protected  override  void  OnDoWork (DoWorkEventArgs e) {
     ReportProgress (0, "Working hard on this report..." );
     Initialize financial report data
  
     while  (!finished report ) {
       if  (CancellationPending) {
         e.Cancel = true ;
         return ;
       }
       Perform another calculation step
       ReportProgress (percentCompleteCalc, "Getting there..." );
     }     
     ReportProgress (100, "Done!" );
     e.Result = Result = completed report data;
   }
}

   

      不管誰調用GetFinancialTotalsBackground都會獲得一個FinancialWorker——一個用真實地可用地包裝了管理後臺 操做。它能夠報告進度,被取消,與Windows Forms交互而不用使用Control.Invoke。它也有異常句柄,而且使用了標準的協議(與使用BackgroundWorker沒任何區別!)

     這種BackgroundWorker的用法有效地迴避了舊有的「基於事件的異步模式」。

 

2  ReaderWriterLockSlim

 

     //注意還有一個老的ReaderWriterLock類,Slim類爲.net 3.5新增,提升了性能。

     一般來說,一個類型的實例對於並行的讀操做是線程安全的,可是並行地更新操做則不是(並行地讀與更新也不是)。 這對於資源(好比一個文件)也是同樣的。使用一個簡單的獨佔鎖來鎖定全部可能的訪問可以解決實例的線程安全爲問題,可是當有不少的讀操做而只是偶然的更新 操做的時候,這就很不合理的限制了併發。一個例子就是這在一個業務程序服務器中,爲了快速查找把數據緩存到靜態字段中。在這樣的狀況 下,ReaderWriterLockSlim類被設計成提供最大可能的鎖定。

     ReaderWriterLockSlim有兩種基本的Lock方法:一個獨佔的Wirte Lock ,和一個與其餘Read lock相容的讀鎖定。

     因此,當一個線程擁有一個Write Lock的時候,會阻塞全部其餘線程得到讀寫鎖。可是當沒有線程得到WriteLock時,能夠有多個線程同時得到ReadLock,進行讀操做。

     ReaderWriterLockSlim提供了下面四個方法來獲得和釋放讀寫鎖:

1
2
3
4
public  void  EnterReadLock();
public  void  ExitReadLock();
public  void  EnterWriteLock();
public  void  ExitWriteLock();

 

     另外對於全部的EnterXXX方法,還有」Try」版本的方法,它們接收timeOut參數,就像Monitor.TryEnter同樣(在資源爭用嚴重的時候超時發生至關容易)。另外ReaderWriterLock提供了其餘相似的AcquireXXX 和 ReleaseXXX方法,它們超時退出的時候拋出異常而不是返回false。

       下面的程序展現了ReaderWriterLockSlim——三個線程循環地枚舉一個List,同時另外兩個線程每一秒鐘添加一個隨機數到List中。一個read lock保護List的讀取線程,同時一個write lock保護寫線程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class  SlimDemo
{
   static  ReaderWriterLockSlim rw = new  ReaderWriterLockSlim();
   static  List< int > items = new  List< int >();
   static  Random rand = new  Random();
 
   static  void  Main()
   {
     new  Thread (Read).Start();
     new  Thread (Read).Start();
     new  Thread (Read).Start();
 
     new  Thread (Write).Start ( "A" );
     new  Thread (Write).Start ( "B" );
   }
 
   static  void  Read()
   {
     while  ( true )
     {
       rw.EnterReadLock();
       foreach  ( int  i in  items) Thread.Sleep (10);
       rw.ExitReadLock();
     }
   }
 
   static  void  Write ( object  threadID)
   {
     while  ( true )
     {              
       int  newNumber = GetRandNum (100);
       rw.EnterWriteLock();
       items.Add (newNumber);
       rw.ExitWriteLock();
       Console.WriteLine ( "Thread "  + threadID + " added "  + newNumber);
       Thread.Sleep (100);
     }
   }
 
   static  int  GetRandNum ( int  max) { lock  (rand) return  rand.Next (max); }
}
<em><span style= "font-family: YaHei Consolas Hybrid;" > //在實際的代碼中添加try/finally,保證異常狀況寫lock也會被釋放。</span></em>

結果爲:

Thread B added 61 Thread A added 83 Thread B added 55 Thread A added 33 ...

      ReaderWriterLockSlim比簡單的Lock容許更大的併發讀能力。咱們可以添加一行代碼到Write方法,在While循環的開始:

1
Console.WriteLine (rw.CurrentReadCount + " concurrent readers" );

       基本上老是會返回「3 concurrent readers」(讀方法花費了更多的時間在Foreach循環),ReaderWriterLockSlim還提供了許多與CurrentReadCount屬性相似的屬性來監視lock的狀況:

1
2
3
4
5
6
7
8
9
10
11
public  bool  IsReadLockHeld            { get ; }
public  bool  IsUpgradeableReadLockHeld { get ; }
public  bool  IsWriteLockHeld           { get ; }
 
public  int   WaitingReadCount          { get ; }
public  int   WaitingUpgradeCount       { get ; }
public  int   WaitingWriteCount         { get ; }
 
public  int   RecursiveReadCount        { get ; }
public  int   RecursiveUpgradeCount     { get ; }
public  int   RecursiveWriteCount       { get ; }

      有時候,在一個原子操做裏面交換讀寫鎖是很是有用的,好比,當某個item不在list中的時候,添加此item進去。最好的狀況是,最小化寫如鎖的時間,例如像下面這樣處理:

    1 得到一個讀取鎖

    2 測試list是否包含item,若是是,則返回

    3 釋放讀取鎖

    4 得到一個寫入鎖

    5 寫入item到list中,釋放寫入鎖。

     但 是在步驟三、4之間,當另一個線程可能偷偷修改List(好比說添加一樣一個Item),ReaderWriterLockSlim經過提供第三種鎖來 解決這個問題,這就是upgradeable lock。一個可升級鎖和read lock 相似,只是它可以經過一個原子操做,被提高爲write lock。使用方法以下:

  1.  
    1. 調用 EnterUpgradeableReadLock
    2. 讀操做(e.g. test if item already present in list)
    3. 調用 EnterWriteLock (this converts the upgradeable lock to a write lock)
    4. 寫操做(e.g. add item to list)
    5. 調用ExitWriteLock (this converts the write lock back to an upgradeable lock)
    6. 其餘讀取的過程
    7. 調用ExitUpgradeableReadLock

      從調用者的角度,這很是想遞歸(嵌套)鎖。實際上第三步的時候,經過一個原子操做,釋放了read lock 並得到了一個新的write lock.

      upgradeable locks 和read locks之間另外還有一個重要的區別,儘管一個upgradeable locks 可以和任意多個read locks共存,可是一個時刻,只能有一個upgradeable lock本身被使用。這防止了死鎖。這和SQL Server的Update lock相似

image

      咱們能夠改變前面例子的Write方法來展現upgradeable lock:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while  ( true )
{
   int  newNumber = GetRandNum (100);
   rw.EnterUpgradeableReadLock();
   if  (!items.Contains (newNumber))
   {
     rw.EnterWriteLock();
     items.Add (newNumber);
     rw.ExitWriteLock();
     Console.WriteLine ( "Thread "  + threadID + " added "  + newNumber);
   }
   rw.ExitUpgradeableReadLock();
   Thread.Sleep (100);
}

ReaderWriterLock 沒有提供upgradeable locks的功能。

 

2.1  遞歸鎖 Lock recursion

Ordinarily, nested or recursive locking is prohibited with ReaderWriterLockSlim. Hence, the following throws an exception:

默認狀況下,遞歸(嵌入)鎖被ReaderWriterLockSlim禁止,由於下面的代碼可能拋出異常。

1
2
3
4
5
var  rw = new  ReaderWriterLockSlim();
rw.EnterReadLock();
rw.EnterReadLock();
rw.ExitReadLock();
rw.ExitReadLock();

可是顯示地聲明容許嵌套的話,就能正常工做,不過這帶來了沒必要要的複雜性。

1
var  rw = new  ReaderWriterLockSlim (LockRecursionPolicy.SupportsRecursion);

 

1
2
3
4
5
6
rw.EnterWriteLock();
rw.EnterReadLock();
Console.WriteLine (rw.IsReadLockHeld);     // True
Console.WriteLine (rw.IsWriteLockHeld);    // True
rw.ExitReadLock();
rw.ExitWriteLock();

   使用鎖的順序大體爲:Read Lock  -->  Upgradeable Lock  -->  Write Lock

 

3   線程池

 

        若是你的程序有不少線程,致使花費了大多時間在等待句柄的阻止上,你能夠經過 線程池來削減負擔。線程池經過合併不少等待句柄在不多的線程上來節省時間。

        使用線程池,你須要註冊一個連同將被執行的委託的Wait Handle,在Wait Handle發信號時。這個工做經過調用ThreadPool.RegisterWaitForSingleObject來完成,以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class  Test {
   static  ManualResetEvent starter = new  ManualResetEvent ( false );
  
   public  static  void  Main() {
     ThreadPool.RegisterWaitForSingleObject (starter, Go, "hello" , -1, true );
     Thread.Sleep (5000);
     Console.WriteLine ( "Signaling worker..." );
     starter.Set();
     Console.ReadLine();
   }
  
   public  static  void  Go ( object  data, bool  timedOut) {
     Console.WriteLine ( "Started "  + data);
     // Perform task...
   }
}

image

      除了等待句柄和委託以外,RegisterWaitForSingleObject也接收一個「黑盒」對象,它被傳遞到你的委託方法中( 就像用ParameterizedThreadStart同樣),擁有一個毫秒級的超時參數(-1意味着沒有超時)和布爾標誌來指明請求是一次性的仍是循環的。

       全部進入線程池的線程都是後臺的線程,這意味着 它們在程序的前臺線程終止後將自動的被終止。但你若是想等待進入線程池的線程都完成它們的重要工做在退出程序以前,在它們上調用Join是不行的,由於進 入線程池的線程歷來不會結束!意思是說,它們被改成循環,直到父進程終止後才結束。因此爲知道運行在線程池中的線程是否完成,你必須發信號——好比用另外一 個Wait Handle。

      在線程池中的線程上調用Abort 是一個壞主意,線程須要在程序域的生命週期中循環。

      你也能夠用QueueUserWorkItem方法而不用等待句柄來使用線程池,它定義了一個當即執行的委託。你沒必要在多個任務中節省共享線程,但有一個 慣例:線程池保持一個線程總數的封頂(默認爲25),在任務數達到這個頂值後將自動排隊。這就像程序範圍的有25個消費者的生產者/消費者隊列。在下面的例子中,100個任務入列到線程池中,而一次只執行 25個,主線程使用Wait 和 Pulse來等待全部的任務完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class  Test {
   static  object  workerLocker = new  object  ();
   static  int  runningWorkers = 100;
  
   public  static  void  Main() {
     for  ( int  i = 0; i < 100; i++) {
       ThreadPool.QueueUserWorkItem (Go, i);
     }
     Console.WriteLine ( "Waiting for threads to complete..." );
     lock  (workerLocker) {
       while  (runningWorkers > 0) Monitor.Wait (workerLocker);
     }
     Console.WriteLine ( "Complete!" );
     Console.ReadLine();
   }
  
   public  static  void  Go ( object  instance) {
     Console.WriteLine ( "Started: "  + instance);
     Thread.Sleep (1000);
     Console.WriteLine ( "Ended: "  + instance);
     lock  (workerLocker) {
       runningWorkers--; Monitor.Pulse (workerLocker);
     }
   }
}

     爲了傳遞多個對象給目標方法,你能夠定義個擁有全部須要屬性的自定義對象,或者調用一個匿名方法。好比若是Go方法接收兩個整型參數,會像下面這樣:

1
ThreadPool.QueueUserWorkItem ( delegate  ( object  notUsed) { Go (23,34); });

另外一個進入線程池的方式是經過異步委託。

 

4.   異步委託

 

     在第一部分咱們描述如何使用 ParameterizedThreadStart把數據傳入線程中。有時候 你須要經過另外一種方式,來從線程中獲得它完成後的返回值。異步委託提供了一個便利的機制,容許許多參數在兩個方向上傳遞 。此外,未處理的異常在異步委託中在原始線程上被從新拋出,所以在工做線程上不須要明確的處理了。異步委託也提供了計入 線程池的另外一種方式。

     對此你必須付出的代價是要跟從異步模型。爲了看看這意味着什麼,咱們首先討論更常見的同步模型。咱們假設咱們想比較 兩個web頁面,咱們按順序取得它們,而後像下面這樣比較它們的輸出:

1
2
3
4
5
6
static  void  ComparePages() {
   WebClient wc = new  WebClient ();
   string  s1 = wc.DownloadString ( "http://www.oreilly.com" );
   string  s2 = wc.DownloadString ( "http://oreilly.com" );
   Console.WriteLine (s1 == s2 ? "Same"  : "Different" );
}

    若是兩個頁面同時下載固然會更快了。問題在於當頁面正在下載時DownloadString阻止了繼續調用方法。若是咱們能 調用 DownloadString在一個非阻止的異步方式中會變的更好,換言之:

1. 咱們告訴 DownloadString 開始執行

2. 在它執行時咱們執行其它任務,好比說下載另外一個頁面

3. 咱們詢問DownloadString的全部結果

    WebClient類實際上提供一個被稱爲DownloadStringAsync的內建方法 ,它提供了就像異步函數的功能。而眼下,咱們忽略這個問題,集中精力在任何方法均可以被異步調用的機制上。

    第三步使異步委託變的有用。調用者聚集了工做線程獲得結果和容許任何異常被從新拋出。沒有這步,咱們只有普通多線程。雖然也可能不用聚集方式使用異步委託,你能夠用ThreadPool.QueueWorkerItem 或 BackgroundWorker。

    下面咱們用異步委託來下載兩個web頁面,同時實現一個計算:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
delegate  string  DownloadString ( string  uri);
  
static  void  ComparePages() {
  
   // Instantiate delegates with DownloadString's signature:
   DownloadString download1 = new  WebClient().DownloadString;
   DownloadString download2 = new  WebClient().DownloadString;
   
   // Start the downloads:
   IAsyncResult cookie1 = download1.BeginInvoke (uri1, null , null );
   IAsyncResult cookie2 = download2.BeginInvoke (uri2, null , null );
   
   // Perform some random calculation:
   double  seed = 1.23;
   for  ( int  i = 0; i < 1000000; i++) seed = Math.Sqrt (seed + 1000);
   
   // Get the results of the downloads, waiting for completion if necessary.
相關文章
相關標籤/搜索