天天看點

淺談.NET下的多線程和并行計算(七)基于多線程的基本元件

在多線程應用中我們有一些很常見的需求,比如定時去做計劃任務,或者是在執行一個長時間的任務,在執行這個任務的過程中能有進度顯示(能想到要實作這個需求需要新開一個線程,避免阻塞UI的更新)。對于這些應用.NET提供了現成的元件。

首先來看一下System.Threading的Timer元件,它提供了定時執行某個任務的方法:

ThreadPool.SetMinThreads(2, 2);
ThreadPool.SetMaxThreads(4, 4);

Timer timer = new Timer((state) =>
{
    int a, b;
    ThreadPool.GetAvailableThreads(out a, out b);
    Console.WriteLine(string.Format("({0}/{1}) #{2} : {3}", a, b, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("mm:ss")));
}, null, 2000, 1000);

Console.WriteLine(DateTime.Now.ToString("mm:ss"));
Thread.Sleep(5000);
Console.WriteLine("Change()");
timer.Change(3000, 500);
Thread.Sleep(5000);
Console.WriteLine("Dispose()");
timer.Dispose();
Thread.Sleep(5000);
Console.WriteLine(DateTime.Now.ToString("mm:ss"));      

這段代碼的運作結果如下:

淺談.NET下的多線程和并行計算(七)基于多線程的基本元件

我們可以看到:

1) Timer構造方法中第一個參數就是要定時執行的方法,這個方法接受一個狀态參數,第二個參數是狀态參數,第三個參數是首次調用回調方法前的延遲毫秒,第四個參數就是執行方法的間隔毫秒

2) 從結果中我們可以看到,第一次回調方法在2秒後執行,然後每一秒執行一次,之後我們調用了Change()方法把延遲時間設定為3秒,把間隔設定為500毫秒,看到Timer在完成了上次回調之後3秒後執行了新的回調,之後間隔500毫秒執行一次。

3) 最後,我們執行了Dispose()方法,在結束最後一次回調之後Timer就再也沒有調用回調方法。

4) 在回調方法中我們輸出了線程池的可用線程,可以看到Timer基于線程池,也就是Timer基于背景線程。

.NET中還提供了System.Timers.Timer,它封裝并增強了System.Threading.Timer:

System.Timers.Timer timer2 = new System.Timers.Timer();
timer2.Elapsed += new System.Timers.ElapsedEventHandler(timer2_Elapsed);
timer2.Interval = 1000;
Console.WriteLine("Start()");
timer2.Start();
Console.WriteLine(DateTime.Now.ToString("mm:ss"));
Thread.Sleep(5000);
Console.WriteLine("Stop()");
timer2.Stop();
Thread.Sleep(5000);
Console.WriteLine("Change Interval and Start()");
timer2.Interval = 500;
timer2.Start();
Thread.Sleep(5000);
Console.WriteLine("Dispose()");
timer2.Dispose();
Thread.Sleep(5000);
Console.WriteLine(DateTime.Now.ToString("mm:ss"));      
static void timer2_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
    int a, b;
    ThreadPool.GetAvailableThreads(out a, out b);
    Console.WriteLine(string.Format("({0}/{1}) #{2} : {3}", a, b, Thread.CurrentThread.ManagedThreadId, e.SignalTime.ToString("mm:ss")));
}      

(假設我們還是設定線程池最小2個線程最大4個線程)

這段代碼的結果如下:

淺談.NET下的多線程和并行計算(七)基于多線程的基本元件

從運作結果中我們可以看到:

1) 由于System.Timers.Timer封裝了System.Threading.Timer,是以還是基于線程池

2) 預設Timer是停止的,啟動後需要等待一個Interval再執行回調方法

最後,再來看看BackgroundWorker,它提供了對于前面說的執行一個任務,在UI上更新進度這種應用的封裝,首先定義一個static的BackgroundWorker:

static BackgroundWorker bw = new BackgroundWorker();      

然後寫如下測試代碼:

bw.WorkerReportsProgress = true;
bw.WorkerSupportsCancellation = true;
bw.ProgressChanged += new ProgressChangedEventHandler(bw_ProgressChanged);
bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
bw.DoWork += new DoWorkEventHandler(bw_DoWork);
bw.Disposed += new EventHandler(bw_Disposed);
AutoResetEvent are = new AutoResetEvent(false);
Console.WriteLine(DateTime.Now.ToString("mm:ss"));
bw.RunWorkerAsync(are);
Thread.Sleep(2000);
are.Set();
Thread.Sleep(2000);
Console.WriteLine("CancelAsync()");
bw.CancelAsync();
while (bw.IsBusy)
    Thread.Sleep(10);
bw.Dispose();      

這段代碼中我們:

1) 設定BackgroundWorker可以彙報進度(通過ProgressChanged事件)

2) 設定BackgroundWorker支援任務取消

3) 定義了進度更新的處理事件:

static void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    Console.WriteLine(string.Format("{0} : {1}% completed", DateTime.Now.ToString("mm:ss"), e.ProgressPercentage));
}      

4) 定義了任務完成的處理事件:

static void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    if (e.Cancelled)
        Console.WriteLine("Cancelled");
    else if (e.Error != null)
        Console.WriteLine(e.Error.ToString());
    else
        Console.WriteLine(e.Result);
    Console.WriteLine(DateTime.Now.ToString("mm:ss"));
}      

5) 定義了任務的主體方法:

static void bw_DoWork(object sender, DoWorkEventArgs e)
{
    (e.Argument as AutoResetEvent).WaitOne();

    for (int i = 0; i <= 100; i += 10)
    {
        //if (bw.CancellationPending)
        //{
        //    Console.WriteLine("Cancelling...");
        //    for (int j = 0; j <= 100; j += 20)
        //    {
        //        bw.ReportProgress(j);
        //        Thread.Sleep(500);
        //    }
        //    e.Cancel = true;
        //    return;
        //}
        bw.ReportProgress(i);
        Thread.Sleep(500);
    }
    e.Result = 100;
}      

6) 定義了Dispose BackgroundWorker後的事件:

static void bw_Disposed(object sender, EventArgs e)
{
    Console.WriteLine("disposed");
}      

7) 使用信号量作為事件的狀态參數讓任務延遲2秒執行

8) 主線程通過IsBusy判斷任務是否在執行,輪詢等待

9) 最後Dispose這個元件

程式執行結果如下:

淺談.NET下的多線程和并行計算(七)基于多線程的基本元件

可以看到:

1) 任務延遲2秒執行,任務分為10個階段執行,每執行一個階段彙報一下進度。每個階段需要500毫秒

2) 任務執行完成之後可以設定Result屬性的值,這個值在bw_RunWorkerCompleted中可以擷取到

我們還原注釋的那些代碼來看看取消任務的情況:

淺談.NET下的多線程和并行計算(七)基于多線程的基本元件

我們看到任務在2秒後取消,要注意這種異步取消任務的方式,我們調用了CancelAsync()其實不能從實質上取消任務的執行,要真正取消任務需要在任務的主體方法中不斷檢測CancellationPending屬性,如果為true表示使用者希望取消任務,然後去執行一些取消任務的行為,在完成後設定Cancel屬性為true,并結束任務主體,這麼做的目的是因為對于長時間的任務取消復原的過程可能也是長時間的,我們同樣可以在主體方法中對取消的行為進行進度彙報。您可以自己做相關實驗,可以發現在控制台程式中,BackgroundWorker的DoWork可ProgressReport基于兩個獨立的線程,這兩個線程都是基于線程池的,在Winform中 BackgroundWorker的DoWork基于線程池中的獨立線程而ProgressReport執行于UI線程。

作者:

lovecindywang

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。