C#多線程和異步(一)——基本概念和使用方法

1、多線程相關的基本概念

進程(Process):是系統中的一個基本概念。 一個正在運行的應用程序在操做系統中被視爲一個進程,包含着一個運行程序所須要的資源,進程能夠包括一個或多個線程 。進程之間是相對獨立的,一個進程沒法訪問另外一個進程的數據(除非利用分佈式計算方式),一個進程運行的失敗也不會影響其餘進程的運行,Windows系統就是利用進程把工做劃分爲多個獨立的區域的。進程能夠理解爲一個程序的基本邊界。html

線程(Thread):是 進程中的基本執行單元,是操做系統分配CPU時間的基本單位 ,在進程入口執行的第一個線程被視爲這個進程的 主線程 。c#

多線程能實現的基礎:安全

  一、CPU運行速度太快,硬件處理速度跟不上,因此操做系統進行分時間片管理。這樣,宏觀角度來講是多線程併發 ,看起來是同一時刻執行了不一樣的操做。可是從微觀角度來說,同一時刻只能有一個線程在處理。多線程

  二、目前電腦都是多核多CPU的,一個CPU在同一時刻只能運行一個線程,可是 多個CPU在同一時刻就能夠運行多個線程 。併發

多線程的優勢:分佈式

  能夠同時完成多個任務;可讓佔用大量處理時間的任務或當前沒有進行處理的任務按期將處理時間讓給別的任務;能夠隨時中止任務;能夠設置每一個任務的優先級以優化程序性能。ide

多線程的缺點:性能

  一、 內存佔用  線程也是程序,因此線程須要佔用內存,線程越多,佔用內存也越多(每一個線程都須要開闢堆棧空間,多線程時有時須要切換時間片)。測試

  二、 管理協調 多線程須要協調和管理,因此須要佔用CPU時間以便跟蹤線程,線程太多會致使控制太複雜。優化

  三、 資源共享   線程之間對共享資源的訪問會相互影響,必須解決爭用共享資源的問題。

2、C#中的線程使用

2.1  基本使用

2.1.1  無參時

 1      class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             ThreadTest test = new ThreadTest();
 6             //無參調用實例方法
 7             Thread thread1 = new Thread(test.Func2);
 8             thread1.Start();
 9             Console.ReadKey();
10         }
11     }
12 
13     class ThreadTest
14     {
15         public void Func2()
16         {
17             Console.WriteLine("這是實例方法");
18         }
19     }

2.1.2  有參數時

 1      class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             ThreadTest test = new ThreadTest();
 6             //有參調用實例方法,ParameterizedThreadStart是一個委託,input爲object,返回值爲void
 7             Thread thread1 = new Thread(new ParameterizedThreadStart(test.Func1));
 8             thread1.Start("有參的實例方法");
 9             Console.ReadKey();
10         }
11     }
12     class ThreadTest
13     {
14         public void Func1(object o)
15         {
16             Console.WriteLine(o);
17         }
18     }

2.2  經常使用的屬性和方法

屬性名稱 說明
CurrentThread 獲取當前正在運行的線程。
ExecutionContext 獲取一個 ExecutionContext 對象,該對象包含有關當前線程的各類上下文的信息。
IsBackground bool,指示某個線程是否爲後臺線程。
IsThreadPoolThread bool,指示線程是否屬於託管線程池。
ManagedThreadId int,獲取當前託管線程的惟一標識符。
Name string,獲取或設置線程的名稱。
Priority

獲取或設置一個值,該值指示線程的調度優先級 。

Lowest<BelowNormal<Normal<AboveNormal<Highest

ThreadState

獲取一個值,該值包含當前線程的狀態。

Unstarted、Sleeping、Running 等

 

方法名稱 說明
GetDomain() 返回當前線程正在其中運行的當前域。
GetDomainId() 返回當前線程正在其中運行的當前域Id。
Start()   執行本線程。(不必定當即執行,只是標記爲能夠執行)
Suspend() 掛起當前線程,若是當前線程已屬於掛起狀態則此不起做用
Resume() 繼續運行已掛起的線程。
Interrupt() 中斷處於 WaitSleepJoin 線程狀態的線程。
Abort() 終結線程
Join() 阻塞調用線程,直到某個線程終止。
Sleep()   把正在運行的線程掛起一段時間。

 看一個簡單的演示線程方法的栗子:

namespace ThreadForm
{
    public partial class Form1 : Form
    {
        
        Thread thread;
        int index = 0;
        public Form1()
        {
            InitializeComponent();
        }

        //啓動按鈕
        private void startBtn_Click(object sender, EventArgs e)
        {
            //建立一個線程,每秒在textbox中追加一下執行次數
            if (thread==null)
            {
                thread = new Thread(() =>
                {
                    while (true)
                    {
                        index++;
                        try
                        {
                            Thread.Sleep(1000);
                            textBox1.Invoke(new Action(() =>
                            {
                                textBox1.AppendText($"第{index}次,");
                            }));
                        }
                        catch (Exception ex) { MessageBox.Show(ex.ToString()); }
                    }
                });
                //啓動線程
                thread.Start();
            }
        }
       
        //掛起按鈕
        private void suspendBtn_Click(object sender, EventArgs e)
        {
            if (thread != null && thread.ThreadState==ThreadState.Running || thread.ThreadState==ThreadState.WaitSleepJoin)
            {
                thread.Suspend();
            }
        }
       
        //繼續運行掛起的線程
        private void ResumeBtn_Click(object sender, EventArgs e)
        {
            if (thread!=null && thread.ThreadState==ThreadState.Suspended)
            {
                thread.Resume();
            }
        }

        //interrupt會報一個異常,並中斷處於WaitSleepJoin狀態的線程
        private void InterruptBtn_Click(object sender, EventArgs e)
        {
            if (thread != null && thread.ThreadState==ThreadState.WaitSleepJoin)
            {
                thread.Interrupt(); 
            }
        }
        //abort會報一個異常,並銷燬線程
        private void AbortBtn_Click(object sender, EventArgs e)
        {
            if (thread != null)
            {
                thread.Abort();
            }
        }


        //定時器,刷新顯示線程狀態
        private void timer1_Tick(object sender, EventArgs e)
        {
            if (thread!=null)
            {
                txtStatus.Text = thread.ThreadState.ToString();
            }
        }
        //窗體加載
        private void Form1_Load(object sender, EventArgs e)
        {
            timer1.Interval = 100;
            timer1.Enabled = true;
        }
        //窗口關閉時,關閉進程
        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            System.Diagnostics.Process[] processes = System.Diagnostics.Process.GetProcessesByName("ThreadForm");
            foreach (var item in processes)
            {
                item.Kill();
            }
        }
    }
}
View Code

   當點擊Start按鈕,線程啓動文本框會開始追加【第x次】字符串;點擊Suspend按鈕,線程掛起,中止追加字符串;點擊Resume按鈕會讓掛起線程繼續運行;點擊Interrupt按鈕彈出一個異常信息,線程狀態從WaitSleepJoin變爲Running,線程繼續運行;點擊Abort按鈕會彈出一個異常信息並銷燬線程。

一點補充:Suspend、Resume方法已不建議使用,推薦使用AutoResetEvent和ManualResetEvent來控制線程的暫停和繼續,用法也十分簡單,這裏不詳細介紹,有興趣的小夥伴能夠研究下。

2.3  線程同步

  所謂同步: 是指在某一時刻只有一個線程能夠訪問變量 。
  c#爲同步訪問變量提供了一個很是簡單的方式,即便用c#語言的關鍵字Lock,它能夠把一段代碼定義爲互斥段,互斥段在一個時刻內只容許一個線程進入執行,其實是Monitor.Enter(obj),Monitor.Exit(obj)的語法糖。在c#中,lock的用法以下:

 lock (obj) { dosomething... }

obj表明你但願鎖定的對象,注意一下幾點:

  1. lock不能鎖定空值 ,由於Null是不須要被釋放的。 2. 不能鎖定string類型 ,雖然它也是引用類型的。由於字符串類型被CLR「暫留」,這意味着整個程序中任何給定字符串都只有一個實例,具備相同內容的字符串上放置了鎖,就將鎖定應用程序中該字符串的全部實例。 3. 值類型不能被lock ,每次裝箱後的對象都不同 ,鎖定時會報錯 4  避免鎖定public類型 若是該實例能夠被公開訪問,則 lock(this) 可能會有問題,由於不受控制的代碼也可能會鎖定該對象。

         推薦使用 private static readonly類型的對象,readonly是爲了不lock的代碼塊中修改對象,形成對象改變後鎖失效。

以書店賣書爲例

 1      class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             BookShop book = new BookShop();
 6             //建立兩個線程同時訪問Sale方法
 7             Thread t1 = new Thread(book.Sale);
 8             Thread t2 = new Thread(book.Sale);
 9             //啓動線程
10             t1.Start();
11             t2.Start();
12             Console.ReadKey();
13         }
14     }
15     class BookShop
16     {
17         //剩餘圖書數量
18         public int num = 1;
19         private static readonly object locker = new object();
20         public void Sale()
21         {
22 
23             lock (locker)
24             {
25                 int tmp = num;
26                 if (tmp > 0)//判斷是否有書,若是有就能夠賣
27                 {
28                     Thread.Sleep(1000);
29                     num -= 1;
30                     Console.WriteLine("售出一本圖書,還剩餘{0}本", num);
31                 }
32                 else
33                 {
34                     Console.WriteLine("沒有了");
35                 }
36             }
37         }
38     }

 代碼執行結果時:

若是不添加lock則執行的結果時:

2.4  跨線程訪問

例子:點擊測試按鈕,給文本框賦值

代碼以下:

 1     private void myBtn_Click(object sender, EventArgs e)
 2         {
 3             Thread thread1 = new Thread(SetValue);
 4             thread1.Start();
 5 
 6         }
 7         private void SetValue()
 8         {
 9             for (int i = 0; i < 10000; i++)
10             {
11                 this.myTxtBox.Text = i.ToString();
12             }
13         }

 執行代碼會出現以下錯誤:

出現該錯誤的緣由是:myTxtBox是由主線程建立的,thread1線程是另一個線程,在.NET上執行的是託管代碼, C#強制要求這些代碼必須是線程安全的,即不容許跨線程訪問Windows窗體的控件 

 解決的方法:

        public Form1()
        {
            InitializeComponent();
        }
        //點擊按鈕開啓一個新線程
        private void myBtn_Click(object sender, EventArgs e)
        {
            Thread thread1 = new Thread(SetValues);
            thread1.IsBackground = true;
            thread1.Start();
        }

        //新線程給文本框賦值
        private void SetValues()
        {
            Action<int> setVal = (i) => { this.myTxtBox.Text = i.ToString(); };
            for (int i = 0; i < 10000; i++)
            {
                this.myTxtBox.Invoke(setVal, i);
            }
        }

  Invoke:在「擁有控件的基礎窗口句柄的線程」  即在本例的主線程上執行委託,這樣就不存在跨線程訪問了 ,所以仍是線程安全的。

參考文章:

[1] http://www.javashuo.com/article/p-yxegyarl-ds.html

[2] http://www.javashuo.com/article/p-qfpnbdiy-md.html

相關文章
相關標籤/搜索