鎖的核心作用是用來控制並發環境下對變量和資源的有序訪問,c#中常見的鎖有如下幾種類型:
(1) Monitor
(2) Mutex
(3) ReaderWriterLockSlim
(4) SpinLock
(5) Semaphore
下面我們就來逐個看一看這些不同類型鎖的使用場景和使用方式 。
前置條件:
為了使示例更具有參考性,我們照例還是建一個 ASP.NET core Razor的網站項目,
把並發放到網站環境下去運行,這樣可以模擬一個多用戶的使用場景。項目目錄如下:
在此項目下新建 Finish.cshtml 和 Monitor.cshtml 兩個文件,其中 Monitor.cshtml 用來執行並發 ,
執行完后跳轉到 Finish.cshtml 並將結果顯示出來。
1 . 在 Shared 目錄下 找到 _Layout.cshtml 文件 , 將 Monitor.cshtml 頁面的鏈接放到菜單中去,
方便我們去點擊,代碼如下:
<ul class="navbar-nav flex-grow-1"> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Monitor">Monitor</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Index">Home</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a> </li> </ul>
2 . 在 Monitor.cshtml 頁面添加如下的代碼,其功能是在表單中放一個Submit 按鈕,
我們點擊后進行 POST 提交。
<div class="text-center"> <h1 class="display-4">Welcome</h1> <p>Learn about </p> <form method="post"> <button type="submit">Monitor測試</button> </form> </div>
3 . 相應的我們在 Monitor.cshtml.cs 文件中按約定定義一個 OnPost( ) 方法來響應表單的 POST 操作,代碼如下:
public class MonitorModel : PageModel { private int userCount = 10000; //模擬10000個用戶同時執行 Submit 操作; private int saleCount = 0; // 定義一個實例變量,模擬記錄商品的銷量;
public void OnGet() { } public void OnPost() { Parallel.For(0, userCount, i => SaveOrder(i)); // 使用 Parallel.For() 模擬10000個用戶並發執行下單操作。 //並發循環完后跳轉到完成頁面,並將用戶數和購買的商品數在 Finish.cshtml 頁面顯示出來。 string url = string.Format("/Finish?usercount={0}&salecount={1}", userCount, saleCount); Response.Redirect(url); } public void SaveOrder(int i) {
// do something saleCount = saleCount + 1; // 用戶下單完成后將商品銷量 +1 } }
4. 在 Finish.cshtml 頁面接收 OnPost() 方法中傳遞過來的值然后顯示出來,代碼如下:
<div>共有 <span style="font-size:18px;">@HttpContext.Request.Query["usercount"]</span> 人購買</div> <br /> <br /> <div>共賣出 <span style="font-size:18px;">@HttpContext.Request.Query["salecount"]</span> 份</div>
至此,我們就完成了一個模擬10000個用戶同時在網站下單並完成的操作。
5. 把項目編譯之后運行網站,畫面如下:
點擊按鈕,得到如下的結果:
可以看到,結果和我們想象的不太一樣,賣出的數量不是 10000 份,只有1559份,
即使打開頁面多次執行其數量也仍然遠小於10000 。
6 . 現在我們用 Monitor 對象對 SaveOrder(int i) 方法中的語句加鎖 ,避免出現實際銷量和我們看到的數量
不一致的情況,將 SaveOrder(int i) 改造如下:
private readonly object obj = new object(); public void SaveOrder(int i) { try { Monitor.Enter(obj); saleCount = saleCount + 1; } finally { Monitor.Exit(obj); } }
編譯后刷新頁面,然后再次點擊按鈕,得到結果如下:
和我們期望的結果一致。
7 . 接下來我們在上面的基礎上做一下演化:在 Monitor.cshtml.cs 中增加
一個靜態變量 staticCount ,看看頁面運行結果有什么變化,Monitor.cshtml.cs 中代碼修改如下:
public class MonitorModel : PageModel { private int userCount = 10000; private int saleCount = 0; private static int staticCount = 0; //增加一個靜態變量 public void OnGet() { } public void OnPost() { Parallel.For(0, userCount, i => SaveOrder(i)); // 增加一個數據傳遞 staticcount string url = string.Format("/Finish?usercount={0}&salecount={1}&staticcount={2}", userCount, saleCount, staticCount); Response.Redirect(url); } public void SaveOrder(int i) { saleCount = saleCount + 1; // 給靜態變量做 + 1 操作 staticCount = staticCount + 1; } }
在 Finish.cshtml 文件中 接收靜態變量的值,代碼如下:
<div>共有 <span style="font-size:18px;">@HttpContext.Request.Query["usercount"]</span> 人購買</div> <br /> <br /> <div>共賣出 <span style="font-size:18px;">@HttpContext.Request.Query["salecount"]</span> 份</div> <br /> <br /> <div>靜態數量 <span style="font-size:18px;">@HttpContext.Request.Query["staticcount"]</span> 份</div>
編譯后運行結果如下:
然后點 Monitor 鏈接 重復執行 3 次得到的結果如下表:
userCount | saleCount | staticCount(static) | 靜態變量與上一次的差值 | |
第1次執行 | 共有 10000 人購買 | 共賣出 5540 份 | 靜態數量 5533 份 | 5533 |
第2次執行 | 共有 10000 人購買 | 共賣出 7978 份 | 靜態數量 13526 份 | 7993 |
第3次執行 | 共有 10000 人購買 | 共賣出 5328 份 | 靜態數量 18815 份 | 5289 |
第4次執行 | 共有 10000 人購買 | 共賣出 4206 份 | 靜態數量 23091 份 | 4276 |
根據上面的結果我們可以得出如下的結論:
1. 並發產生的原因是因為有多個線程在操作同一個變量,和這個變量是實例變量還是靜態變量無關。
2. 產生並發的時候實例變量因為每次都會初始化,所以其值比正常執行時候小,靜態變量就不一樣了,
它只初始化一次,所以會產生累加的效果,多次運行后可能比正常值大。
最后,我們可以用lock關鍵字對簡化對 Monitor對象的使用,代碼如下:
private readonly object obj = new object(); public void SaveOrder(int i) { lock(obj) { saleCount = saleCount + 1; } }
編譯后運行結果如下:
和使用Monitor對象的效果是一樣的。