1. 程式人生 > 實用技巧 >C#非同步程式設計:多執行緒基礎Thread類

C#非同步程式設計:多執行緒基礎Thread類

Thrad類提供了:在不同執行緒上執行方法的能力

Thread類位於System.Threading名稱空間下,學會使用一下幾點技能,便可基本掌握最簡單的多執行緒操作

知識點1:建立並啟動執行緒

class Program
{
    static void Main(string[] args)
    {
        Thread thread1 = new Thread(new ThreadStart(CommonService.GetConnection));
        Console.WriteLine("thread1 is ready to running....");
        thread1.Start();

        Console.ReadLine();
    }
}

class CommonService
{
    public static void GetConnection()
    {
        Console.WriteLine("connect success....");
    }
}

說明:1、new一個Thread物件,便建立了一個新的執行緒;2、呼叫Start方法,將建立的執行緒的狀態設定為running狀態;作業系統會將該執行緒視為執行,然後尋找空閒時間,在該執行緒上執行GetConnection方法。

知識點2(不常用):暫停(掛起)某個執行緒一段時間

class Program
{
    static void Main(string[] args)
    {
        Thread.Sleep(new TimeSpan(0,0,5));//將當前執行緒暫停五秒(暫停的是,Main方法所在的主執行緒)
        Thread thread1 = new Thread(new ThreadStart(CommonService.GetConnection));
        Console.WriteLine("thread1 is ready to running....");
        thread1.Start();

        Console.ReadLine();
    }
}

class CommonService
{
    public static void GetConnection()
    {
        Thread.Sleep(TimeSpan.FromSeconds(3));//暫停的是GetConnection方法所在的執行緒
        Console.WriteLine("connect success....");
    }
}

說明:使用Thread類的靜態方法--Sleep(),系統會自動判斷執行的程式碼所處在哪一個執行緒"。1、在主執行緒中使用了Sleep,那麼整個應用會停留在該位置"假死"到指定的時間,才繼續向下執行;2、在子執行緒中使用Sleep,那麼子執行緒會暫停操作,到指定時間才繼續執行。開發工作中,不建議使用Sleep()方法。

知識點3(不常用):在一個執行緒中等待另個執行緒執行完成再繼續

當子執行緒建立並啟動後,主執行緒同一時刻也在執行;子執行緒何時執行完畢是不確定的;如果我們希望等待某個執行緒執行完後,才繼續執行另一個執行緒的程式碼,就需要使用Join()方法。

class Program
{
    static void Main(string[] args)
    {
        Thread.Sleep(new TimeSpan(0,0,5));//將Main方法所在的主執行緒暫停五秒
        Thread thread1 = new Thread(new ThreadStart(CommonService.GetConnection));
        Console.WriteLine("thread1 is ready to running....");
        thread1.Start();
        thread1.Join();//join方法有兩個過載,這裡使用無引數形式的Join()方法來等待執行緒執行結束。
        Console.WriteLine("main is ready to completed ....");
        Console.ReadLine();
    }
}

class CommonService
{
    public static void GetConnection()
    {
        Thread.Sleep(TimeSpan.FromSeconds(3));//暫停的是GetConnection方法所在的執行緒
        Console.WriteLine("connect success....");
    }
}

說明:從執行結果可以看出,使用Join()方法後,執行緒之間便存在了一種同步關係--即:你先幹完,我再幹。

知識點4:前臺執行緒與後臺執行緒

class Program
{
    static void Main(string[] args)
    {
        Thread thread = new Thread(new ThreadStart(CommonService.GetConnection));
        /**
         * 可以通過解除下面註釋來觀察前臺執行緒與後臺執行緒的區別:
         * 未解除註釋--Main方法所在的主執行緒率先退出;GetConnection方法所在的子執行緒是前臺執行緒,應用程式會等待子執行緒中的程式碼執行完畢後才結束程序。
         * 解除註釋--Main方法所在的主執行緒率先退出;GetConnection方法所在的子執行緒是後臺執行緒,應用程式檢測程序中沒有正在執行的前臺執行緒後,直接結束程序。
         */
        //thread.IsBackground = true;
        thread.Start();
    }
}

class CommonService
{
    public static void GetConnection()
    {
        Console.WriteLine($"當前正在執行方法所在的執行緒是否為後臺執行緒:{Thread.CurrentThread.IsBackground}");
        Thread.Sleep(TimeSpan.FromSeconds(3));
        Console.WriteLine("connect success....");
    }
}

以控制檯程式為例:1、手動建立的執行緒,預設也是一個"前臺執行緒";3、如果指定IsBackground=true,那麼執行緒被指定為後臺執行緒;4、應用程式會等待所有的前臺執行緒執行完畢後再結束工作(如果程式中只剩下後臺執行緒的話,應用程式會直接結束工作)

知識點5:給執行緒傳遞引數

使用Thread物件的Start方法的過載形式:通過給Start()方法傳遞引數,達到給執行緒傳遞引數

錯誤內容:System.InvalidOperationException:“該執行緒是用不接受引數的 ThreadStart 委託建立的。ThreadStart不接受引數,所以需要一個能夠接收引數的委託:ParameterizedThreadStart

方式1:使用ParameterizedThreadStart建立執行緒、並通過給Start()方法傳遞引數,來給執行緒傳遞引數

private static void TestThreadBase()
{
    Thread thread = new Thread(new ParameterizedThreadStart(CommonService.ConnectionWithTarget));
    thread.Start(12);
}
class CommonService
{
    public static void ConnectionWithTarget(object targetName)
    {
        Console.WriteLine($"執行緒 {targetName} 連線成功....");
        Thread.Sleep(TimeSpan.FromSeconds(5));
        Console.WriteLine($"執行緒 {targetName} 退出 ...");
    }
}
/*
執行緒 12 連線成功....
執行緒 12 退出 ...
*/

使用ParameterizedThreadStart委託的方式給執行緒傳遞引數,那麼執行執行緒的方法的引數列表就需要和ParameterizedThreadStart委託的引數列表匹配--即:需要改方法定義一個Object型別的形參;假如我希望執行執行緒的方法的引數是Student型別的(如下),那麼使用ParameterizedThreadStart委託,還能奏效麼?
準備一個入參為Student型別的方法

class CommonService
{
    public static void ShowTargetInfo(Student student) 
    {
        Console.WriteLine($"{student.StudentName}的性別是{student.StudentSex},身高是{student.StudentHeight}");
    }
}
class Student
{
    public int StudentId { get; set; }
    public string StudentName { get; set; }
    public double StudentHeight { get; set; }
    public char StudentSex { get; set; }
}

嘗試給ParameterizedThreadStart委託新增ShowTargetInfo方法

上面的報錯告訴我們,ShowTargetInfo方法不匹配ParameterizedThreadStart委託,所以我們需要轉換思路:使用Lambda表示式來例項化ParameterizedThreadStart委託;然後再在Lambda表示式中呼叫ShowTargetInfo方法,這樣ShowTargetInfo也算是在該執行緒上運行了。

方式2:使用Lambda表示式例項化委託型別,然後在Lambda表示式中呼叫方法,達到給執行緒傳遞引數的目的

private static void TestThreadBase()
{
    Student student = new Student() { StudentId = 11, StudentName = "小哇", StudentSex = '男', StudentHeight = 170.5 };
    //方式1
    Thread thread1 = new Thread(new ThreadStart(()=> 
    {
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
        CommonService.ShowTargetInfo(student);
    }));
    thread1.Start();

    //方式2
    Thread thread2 = new Thread(new ParameterizedThreadStart(item=> 
    {
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
        CommonService.ShowTargetInfo(student);
    }));
    thread1.Start();

    //方式3
    Thread thread3 = new Thread(new ParameterizedThreadStart(item =>
    {
        var stu = item as Student;
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
        CommonService.ShowTargetInfo(stu);
    }));
    thread3.Start(student);

    //方式1 簡寫形式 Lambda表示式隱式建立了ThreadStart委託(也是我最常用的給執行緒傳遞引數的形式)
    Thread thread4 = new Thread(()=> 
    {
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
        CommonService.ShowTargetInfo(student);
    });
    thread4.Start();

    //方式2 簡寫形式 Lambda表示式隱式建立了ParameterizedThreadStart委託
    Thread thread5 = new Thread(item =>
    {
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
        CommonService.ShowTargetInfo(student);
    });
    thread5.Start();

    //方式3 簡寫形式 Lambda表示式隱式建立了ParameterizedThreadStart委託
    Thread thread6 = new Thread(item =>
    {
        var stu = item as Student;
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
        CommonService.ShowTargetInfo(stu);
    });
    thread5.Start(student);
}

向執行緒傳遞引數,本質上是向執行緒所執行的方法傳入實參;利用Lambda表示式呼叫目標方法的形式,使目標方法引數列表的靈活度高,不受限制。

知識點6:在多執行緒中處理異常

當我們建立了一個執行緒並啟動該執行緒,那麼在該執行緒中一旦發生了異常,會發生什麼後果呢?

在工作執行緒中(即:非主執行緒)發生了異常,會導致程式崩潰

private static void TestThreadException() 
{
    new Thread(()=> 
    {
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} is running .....");
        Thread.Sleep(3000);
        throw new Exception($"從執行緒{Thread.CurrentThread.ManagedThreadId}中丟擲異常....");
    }).Start();
}
static void Main(string[] args)
{
    TestThreadException();
    Console.ReadLine();
}

分析:Lambda表示式中的程式碼片段是執行在子執行緒中的,三秒後該執行緒中丟擲異常;由於沒有處理該異常控制檯程式直接崩潰。

在子執行緒的程式碼外部試圖捕獲異常也是徒勞的

static void Main(string[] args)
{
    try
    {
        TestThreadException();
    }
    catch (Exception ex)
    {
        Console.WriteLine("捕捉到了子執行緒的異常{0}", ex.Message);
    }
    Console.ReadLine();
} 

分析:本來我們以為在Main()方法中使用try..catch塊兒來試圖捕獲子執行緒中的異常,但實際卻並未能捕捉到該異常,程式照常崩潰

在子執行緒中處理異常,才是多執行緒中處理異常的正確方式

private static void TestThreadException()
{
    new Thread(() =>
    {
        try
        {
            Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} is running .....");
            Thread.Sleep(3000);
            throw new Exception($"從執行緒{Thread.CurrentThread.ManagedThreadId}中丟擲異常....");
        }
        catch (Exception ex)
        {
            Console.WriteLine("捕捉到了子執行緒的異常{0}", ex.Message);
        }
    }).Start();
}
/*
3 is running .....
捕捉到了子執行緒的異常從執行緒3中丟擲異常....
*/

多執行緒程式設計處理異常:不要線上程中丟擲異常,而是使用try..catch塊兒。

以上便是對多執行緒基礎的知識點的總結,記錄下來以便以後查閱。