多執行緒

      在〈多執行緒〉中尚無留言

行程

行程是一個執行中的程式, 現行分時系統中, 行程是基本的工作單元. 行程包括程式碼(本文區)、程式計數器數值、暫存器內容、行程堆疊、堆積、變數的資料區間。

行程有五種狀態 : 新產生、執行、等待、就緒、結束, 任何時候只能一個行程在執行狀態, 但有許多行程在等待和就緒的狀態。

每一個行程在作業系統中都對應一個行程控制表(PCB), PCB記載所代表的行程相關資訊。

分時系統可將CPU在不同行程間不斷切換, 以便讓使用者可以在自己的行程執行時與它互動。

一個新的行程最初置於就緒佇列中, 此行程被CPU執行時, 可觸發如下事件 :

發出I/O要求,然後置於一個I/O佇列中。
產生一個子行程,並等待其結束。
強行移離CPU(和中斷的結果一樣),然後放回就緒佇列中。

執行緒

執行緒是CPU使用時的一個基本單位, 由執行緒ID、程式計數器、一組暫存器及一個堆疊空間組成。

一個行程可擁有多個執行緒, 每個執行緒共用行程的資源(Ram, File, Signal)

行程的產生對系統來說是重擔,如果新行程和現存行程執行相同的工作,比較有效率的做法是讓一個行程產生新的執行緒來達到相同的目的。

多執行緒程式好處如下 :

應答 : 如一個執行緒載入影像時, 另一執行緒可以跟使用者互動
資源共享 : 執行緒共享行程內的Ram, 但也容易造成Dead Lock
經濟 : 執行緒消耗的資源比行程少

正規寫法

正規啟動執行緒, 必需使用 new Thread(ThreadStart物件).Start();

麻煩難懂的地方是, 什麼是ThreadStart物件.
我們可以把ThreadStart想成是一個物件, 把要作的任務塞在裏面. 所以更進一步的完整代碼如下
ThreadStart s=new ThreadStart(某個方法);
new Thread(s).Start();

現在怪異的地方又來了, 把某個方法塞到ThreadStart()這個建構子. 方法之內不是都只是塞資料型態嗎, 怎麼又塞方法呢?

原來他是使用Delegate(委派)的方式, ThreadStart是系統就寫好的, 原始碼如下
public delegate void ThreadStart();
我們只要產生ThreadStart物件, 將方法塞到裏面, 這個方法就會包在物件裏面, 然後這個方法就可以供Thread使用了。

using System;
using System.Threading;
namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            ThreadStart s = new ThreadStart(task);
            Thread t = new Thread(s);
            t.Name = "New Thread 0";
            t.Start();
            Console.WriteLine("Main 結束了");
        }

        static void task(){
            for (int i = 0; i < 20; i++)
            {
                Thread.Sleep(100);
                Console.WriteLine("{0} : {1}", Thread.CurrentThread.Name, i);
            }
        }
    }
}

請注意一下, Main主執行緒結束後, 新的執行緒還是一直在背景努力的執行喔

Simple Thread

將任務包含在一個方法內, 再建立新執行緒, 然後把任務傳入, 最後使用Start()啟動

但為何可以直接使用 new thread(task).Start()呢, 因為編譯器會自動啟動如下委派
public delegate void ThreadStart();

        static void Main(string[] args)
        {
            new Thread(task).Start();
            for(int i = 0; i < 1000; i++)
            {
                Thread t = Thread.CurrentThread;
                Console.WriteLine("Thread_{0}:{1}", t.ManagedThreadId, i);
            }
        }
        static void task()
        {
            for(int i = 0; i < 1000; i++)
            {
                Thread t = Thread.CurrentThread;
                Console.WriteLine("Thread_{0}:{1}", t.ManagedThreadId, i);
            }
        }

 執行結果如下, 主執行緒及新執行緒交互執行, 完全無法預測

Thread_1:8
Thread_1:9
Thread_3:0
Thread_3:1
Thread_3:2
Thread_3:3
Thread_3:4
Thread_3:5
Thread_3:6
Thread_3:7
Thread_3:8
Thread_3:9
Thread_3:10
Thread_3:11
Thread_3:12
Thread_3:13
Thread_3:14
Thread_3:15
Thread_3:16
Thread_3:17
Thread_3:18
Thread_3:19
Thread_3:20
Thread_3:21
Thread_3:22
Thread_3:23
Thread_3:24
Thread_1:10
Thread_1:11
Thread_1:12
Thread_1:13
Thread_1:14

ParameterizedThreadStart

若要將參數傳入任務方法, 可使用如下程式碼

static void Main(string[] args)
        {
            new Thread(task).Start(10);
            for (int i = 0; i < 100; i++)
            {
                Thread t = Thread.CurrentThread;
                Console.WriteLine("thread_{0}:{1}", t.ManagedThreadId, i);
            }
        }
        static void task(object number)
        {
            for (int i = 0; i < (int)number; i++)
            {
                Thread t = Thread.CurrentThread;
                Console.WriteLine("thread_{0}:{1}", t.ManagedThreadId, i);
            }
        }

上述紅色部份, 將參數放入Start(10)方法之內, 此時系統自動啟動
public delegate void ParameterizedThreadStart(Object obj);
所以也可以寫成如下
new Thread(new ParameterizedThreadStart(task)).Start(10);

等待

如果希望執行緒完成後, 才開始調用者的流程, 可以使用Thread物件的Join()方法, 如下程式碼

class Program
    {
        static void Main(string[] args)
        {
            Thread t1 = new Thread(task);
            Thread t2 = new Thread(task);
            Thread t3 = new Thread(task);
            t1.Start("a");
            t2.Start("b");
            t3.Start("c");
            t1.Join();
            t2.Join();
            t3.Join();
            for (int i = 0; i < 1000; i++)
            {
                Console.Write("+");
            }
        }
        static void task(object param)
        {
            for(int i = 0; i < 1000; i++)
            {
                Console.Write(param);
            }
        }
    }

上述程式碼, 主執行緒會停止, 等待t1, t2, t3三個執行緒完成後, 才會開始執行主執行緒. 而在主執行緒停止期間, t1, t2, t3處於同室操戈的狀態, 誰先誰後, 只有神知道

執行結果如下

aaaaaaaaaaaaaaaaaaaaaaaaaaaacccccccbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

ThreadPool

養一堆執行緒等待工作的到來, 工作一來, 都放在Queue裏, 再由執行緒集區一個一個消化掉

ThreadPool.QueueUserWorkItem(WaitCallback)

將工作排入執行緒佇列中,由 ThreadPool 對其做管理

WaitCallback

為要執行的方法, 需先產生WaitCallback物件, 並將方法傳入建構子之中, 如下之代碼 new WaitCallback(Doworker);

static void Main(string[] args)
    {
        for (int i = 0; i < 10; i++)
        {
            ThreadPool.QueueUserWorkItem(new WaitCallback(Doworker));
        }
        Console.WriteLine("Main thread exit");
        Console.ReadKey();
    }
    static void Doworker(object number)
    {
        Thread t = Thread.CurrentThread;
        Console.WriteLine("No.{0}-Thread[{1}]:{2}", number, t.ManagedThreadId, t.ThreadState);
        Thread.Sleep(1000);
    }

/*
以上可簡化成
ThreadPool.QueueUserWorkItem(callback =>
{
    Thread t = Thread.CurrentThread;
    Console.WriteLine("No.{0}-Thread[{1}]:{2}", number, t.ManagedThreadId, t.ThreadState);
    Thread.Sleep(1000);
});
*/

執行結果如下

Main thread exit
No.-Thread[7]:Background
No.-Thread[5]:Background
No.-Thread[3]:Background
No.-Thread[8]:Background
No.-Thread[4]:Background
No.-Thread[6]:Background
No.-Thread[10]:Background
No.-Thread[9]:Background
No.-Thread[11]:Background
No.-Thread[3]:Background
.請按任意鍵繼續 . . .

以上代碼, 主執行緒交付了10個任務, 由執行緒集區執行, 集區啟動10條執行緒工作. 請注意, 主執行緒一結束, 就算集區還沒完成, 集區還是跟著毀滅. 所以在主執行緒要加入ReadKey()等待使用者按任何鍵.

Lambda

執行緒也可以使用 Lambda簡易寫法, 如下

new Thread(()=>{
    //要執行的任務
}).Start()

跨線程

在C#中, 若要更新UI畫面時, 只能使用UI主執行緒進行更新. 也就是說, 新開啟的執行緒是無法操作UI控制項.

那麼, 在新的執行緒執行到某一時間點, 想要更改控制項的話, 那該怎麼辦呢! 這時就必需由新執行緒跨到UI主執行緒, 把更新的工作交給UI主執行緒去執行. 此時稱為跨線程.

事實上, 跨線程要求UI主執行緒更新畫面時, 並不是馬上就執行. 必需等到UI主執行緒有空閒的時候,才會去處理畫面的事。

BackgroundWorker

BackgroundWorker這名字太長了, 所以我們就簡稱為bg

UI的變更, 只能由UI主執行緒更改, 其他新生的執行緒無法更改UI, 否則會發生Exception. 此設計與Android 一模一樣.

在背景執行的執行緒, 若想控制UI, 必需傳回相關資料給UI主執行緒, 待UI主執行緒有空時才進行更改. 最常用也是最方便的就是bg物件.

BackgroundWorker 可以指定如下委派

DoWork : 背景要執行的任務, 由DoWorkEventHandler處理
ProgressChanged : 進度發生變化時會被執行的方法, 此方法是由UI主執行緒執行, 所以可以在此控制 一般會在DoWork中下達bg.ReportProgress(int) 來觸發此方法
RunWorkerCompleted : 執行緒結束後會被執行的方法, 也是由UI主執行緒控制

在DoWork裏, 若不小心操作了UI的控制項, 整個執行緒將立即中斷Crash並跳出, 但不會出現任何的錯誤訊息及Exception, 此Issue非常難除錯, 請注意

如下代碼, 用於顯示進度條, 請注意, CancelAsync()並不能真正的停止Thread

public partial class MainWindow : Window
    {
        BackgroundWorker bg;
        bool stopFlag = false;
        public MainWindow()
        {
            InitializeComponent();
        }
        private void btnOk_Click(object sender, RoutedEventArgs e)
        {
            bg = new BackgroundWorker();

            //背景執行的任務
            bg.DoWork += new DoWorkEventHandler(bgDoWork);

            //進度變化時, 由背景任務的ReportProgress(i)觸發
            bg.ProgressChanged += new ProgressChangedEventHandler(bgProgressChanged);

            //任務完成時要執行的工作
            bg.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bgRunWorkerCompleted);
            bg.WorkerReportsProgress = true;

            //允許CancelAsync(), 但此指令是無法中止執行緒的, 日後可能會被拿掉
            //所以, 底下加了也是白加的
            bg.WorkerSupportsCancellation = true;

            //開始執行執行緒
            bg.RunWorkerAsync();
        }
        private void btnCancel_Click(object sender, RoutedEventArgs e)
        {
            stopFlag = true;
        }
        private void bgDoWork(object sender, DoWorkEventArgs e)
        {
            for(int i = 0; i <= 100; i++)
            {
                bg.ReportProgress(i);
                Thread.Sleep(50);
                if (stopFlag)
                {
                    //bg.CancelAsync();沒啥用處
                    break;
                }
            }
        }
        private void bgProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            psBar.Value = e.ProgressPercentage;
        }
        private void bgRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            MessageBox.Show("complete");
        }
    }

執行結果如下

thread1

最後要注意一件事, BackgroundWorker雖是很直覺化的設計, 但如果使用多個bg物件, 會造成阻塞. 也就是說, 明明就下達了
bg.RunWorkerAsync();
但就是要等很久才會執行. 這可能是VS設計上的bug, 所以本人最初的專案都使用bg解決UI與Thread的問題. 到了後期, 全改為下面的方式了.

Dispatcher

dispatcher是調度員的意思. VS使用這個類別, 來分配時間及通知UI主執行緒執行UI更新的任務. dispatcher只適用在WPF. 如果是Windows Form, 必需使用Invoke. 在此有個建議, Windows Form早就該拋棄了, 而且Invoke也被列入即將棄用的項目.

Windows Form

在Windows Form之, 要執到跨線程的工作, 可以使用Windows Form的物件方法Invoke(delegate, parameter), 或是InvokeBegin().

WPF

但在WPF中, 似乎沒有Invoke這個方法了. 此時就必需使用Dispatcher, 如下

Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => {

    //UI控製元件代碼寫在這裏

}));

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *