關於C#多線程、易失域、鎖的分享

  • 2020 年 3 月 12 日
  • 筆記

一、多線程

  windows系統是一個多線程的操作系統。一個程序至少有一個進程,一個進程至少有一個線程。進程是線程的容器,一個C#客戶端程序開始於一個單獨的線程,CLR(公共語言運行庫)為該進程創建了一個線程,該線程稱為主線程。例如當我們創建一個C#控制台程序,程序的入口是Main()函數,Main()函數是始於一個主線程的。它的功能主要 是產生新的線程和執行程序。

  在軟件中,如果有一種操作可以被多人同時調用,我們就可以創建多個線程同時處理,以提高任務執行效率。這時,操作就被分配到各個線程中分別執行。

在C#中我們可以使用Thread類和ThreadStart委託,他們都定義在System.Threading命名空間中。

  ThreadStart委託類型用於定義在線程中的工作,就像我們在使用其他的委託類型一樣,可以使用方法名來創建此委託類型對象,如「new ThreadStart(test)」

多線程優點: (1)多線程技術使程序的響應速度更快 ,因為用戶界面可以在進行其它工作的同時一直處於活動狀態; (2)多線程可以提高CPU的利用率,因為當一個線程處於等待狀態的時候,CPU會去執行另外的線程; (3)佔用大量處理時間的任務可以定期將處理器時間讓給其它任務; (4)可以隨時停止任務; (5)可以分別設置各個任務的優先級以優化性能。

多線程缺點: (1)等候使用共享資源時造成程序的運行速度變慢。這些共享資源主要是獨佔性的資源 ,如寫文件等。 (2)對線程進行管理要求額外的 CPU開銷。線程的使用會給系統帶來上下文切換的額外負擔。當這種負擔超過一定程度時,多線程的特點主要表現在其缺點上,比如用獨立的線程來更新數組內每個元素。 (3)線程的死鎖。即較長時間的等待或資源競爭以及死鎖等多線程癥狀。 (4)對公有變量的同時讀或寫。當多個線程需要對公有變量進行寫操作時,後一個線程往往會修改掉前一個線程存放的數據,從而使前一個線程的參數被修改;另外 ,當公用變量的讀寫操作是非原子性時,在不同的機器上,中斷時間的不確定性,會導致數據在一個線程內的操作產生錯誤,從而產生莫名其妙的錯誤,而這種錯誤是程序員無法預知的。

線程生命周期

線程生命周期開始於 System.Threading.Thread 類的對象被創建時,結束於線程被終止或完成執行時。

下面列出了線程生命周期中的各種狀態:

  • 未啟動狀態:當線程實例被創建但 Start 方法未被調用時的狀況。
  • 就緒狀態:當線程準備好運行並等待 CPU 周期時的狀況。
  • 不可運行狀態:下面的幾種情況下線程是不可運行的:
    • 已經調用 Sleep 方法
    • 已經調用 Wait 方法
    • 通過 I/O 操作阻塞
  • 死亡狀態:當線程已完成執行或已中止時的狀況

Thread 類常用的屬性和方法

最簡單的多線程例子,代碼如下:

static void Main(string[] agrs)          {              ThreadStart threadWork = new ThreadStart(test);              Thread t1 = new Thread(threadWork);              t1.Name = "t1";              Thread t2 = new Thread(threadWork);              t2.Name = "t2";              Thread t3 = new Thread(threadWork);              t3.Name = "t3";              //開始執行              t1.Start();              t2.Start();              t3.Start();              Console.ReadKey();          }          static public void  test(){              Console.WriteLine("{0},hello,小菜鳥",Thread.CurrentThread.Name);          }

使用多線程另一個重要的問題就是對於公共資源分配的控制,比如,火車的座位是有限的,在不同購票點買票時,就需要對座位資源進行合理分配;在電影院看電影也是這樣的,座位只有那麼多,我們不可能100個座位賣出200張票,這樣是不可以的也是不應該的,那麼接下來我們就要看看該如何解決這個問題。

二、易失域

對於類中的成員使用volatile修飾符,它就會被聲明為易失域。對於易失域,在多線程環境中,每個線程中對此域的讀取(易失讀取,volatile read)和寫入(易失寫入,volatile write)操作都會觀察其他線程中的操作,並進行操作的順序執行,這樣就保持易失域使用的一致性了。

volatile的作用是: 作為指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值。多線程的程序,共同訪問的內存當中,多個線程都可以操縱,從而無法判定何時這個變量會發生變化。

可以這樣簡單理解:線程是並行的,但對volatile的訪問是順序排除的,避免出現臟值。

理解:

Volatile 字面的意思時易變的,不穩定的。在C#中也差不多可以這樣理解。

編譯器在優化代碼時,可能會把經常用到的代碼存在Cache裏面,然後下一次調用就直接讀取Cache而不是內存,這樣就大大提高了效率。但是問題也隨之而來了。

在多線程程序中,如果把一個變量放入Cache後,又有其他線程改變了變量的值,那麼本線程是無法知道這個變化的。它可能會直接讀Cache里的數據。但是很不幸,Cache里的數據已經過期了,讀出來的是不合時宜的臟數據。這時就會出現bug。

用Volatile聲明變量可以解決這個問題。用Volatile聲明的變量就相當於告訴編譯器,我不要把這個變量寫Cache,因為這個變量是可能發生改變的。

下面貼栗子代碼:

using System;  using System.Threading;    namespace demoVolatile  {      class Program      {          //多個線程訪問的變量,標記為Volatile          //在這裡如果不標記可能會賣出不止10張票          volatile static int TicketCount = 10;          static void SellTicket()          {              while (TicketCount > 0)              {                  TicketCount--;                  Console.WriteLine("{0} 賣出了一張票", Thread.CurrentThread.Name);              }              Console.WriteLine("{0} 下班了", Thread.CurrentThread.Name);          }          static void Main(string[] args)          {              ThreadStart threadWork = new ThreadStart(SellTicket);              Thread t1 = new Thread(threadWork);              t1.Name = "t1";              Thread t2 = new Thread(threadWork);              t2.Name = "t2";              Thread t3 = new Thread(threadWork);              t3.Name = "t3";              //開始執行              t1.Start();              t2.Start();              t3.Start();              Console.ReadKey();          }      }  }

三、鎖

我們都知道,lock 關鍵字可以用來確保代碼塊完成運行,而不會被其他線程中斷。也就是,說在多線程中,使用lock關鍵字,可以讓被lock的對象,一次只被一個線程使用。

lock語句根本使用的就是Monitor.Enter和Monitor.Exit,也就是說lock(this)時執行Monitor.Enter(this),大括號結束時執行Monitor.Exit(this). 也就是說,Lock關鍵字,就是一個語法糖而已。

使用lock需要注意的地方: 1.lock不能鎖定空值某一對象可以指向Null,但Null是不需要被釋放的。(請參考:認識全面的null) 2.lock不能鎖定string類型,雖然它也是引用類型的。因為字符串類型被CLR「暫留」 3.lock鎖定的對象是一個程序塊的內存邊界 4.值類型不能被lock,因為前文標紅字的「對象被釋放」,值類型不是引用類型的 5.lock就避免鎖定public 類型或不受程序控制的對象。

using System;  using System.Threading.Tasks;    public class Account  {      private readonly object balanceLock = new object();      private decimal balance;        public Account(decimal initialBalance)      {          balance = initialBalance;      }        public decimal Debit(decimal amount)      {          lock (balanceLock)          {              if (balance >= amount)              {                  Console.WriteLine($"Balance before debit :{balance, 5}");                  Console.WriteLine($"Amount to remove     :{amount, 5}");                  balance = balance - amount;                  Console.WriteLine($"Balance after debit  :{balance, 5}");                  return amount;              }              else              {                  return 0;              }          }      }        public void Credit(decimal amount)      {          lock (balanceLock)          {              Console.WriteLine($"Balance before credit:{balance, 5}");              Console.WriteLine($"Amount to add        :{amount, 5}");              balance = balance + amount;              Console.WriteLine($"Balance after credit :{balance, 5}");          }      }  }    class AccountTest  {      static void Main()      {          var account = new Account(1000);          var tasks = new Task[100];          for (int i = 0; i < tasks.Length; i++)          {              tasks[i] = Task.Run(() => RandomlyUpdate(account));          }          Task.WaitAll(tasks);      }        static void RandomlyUpdate(Account account)      {          var rnd = new Random();          for (int i = 0; i < 10; i++)          {              var amount = rnd.Next(1, 100);              bool doCredit = rnd.NextDouble() < 0.5;              if (doCredit)              {                  account.Credit(amount);              }              else              {                  account.Debit(amount);              }          }      }  }

作用:當同一個資源被多個線程讀,少個線程寫的時候,使用讀寫鎖

引用:https://blog.csdn.net/weixin_40839342/article/details/81189596

問題: 既然讀讀不互斥,為何還要加讀鎖

答: 如果只是讀,是不需要加鎖的,加鎖本身就有性能上的損耗

如果讀可以不是最新數據,也不需要加鎖

如果讀必須是最新數據,必須加讀寫鎖

讀寫鎖相較於互斥鎖的優點僅僅是允許讀讀的並發,除此之外並無其他。

注意:不要使用ReaderWriterLock,該類有問題

ok,今天的分享就到這裡了,如有錯誤的地方請指出,謝謝。