關於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,今天的分享就到這裡了,如有錯誤的地方請指出,謝謝。