c# 并发编程系列之五:常见的几种锁及各自的使用场景-Monitor锁

锁的核心作用是用来控制并发环境下对变量和资源的有序访问,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对象的效果是一样的。

 

posted @ 2021-08-16 21:13  屏风马  阅读(1845)  评论(0编辑  收藏  举报