C#多线程系列(2):多线程锁lock和Monitor

C# 中,可以使用 lock 关键字和 Monitor 类来解决多线程锁定资源和死锁的问题。

官方解释:lock 语句获取给定对象的互斥 lock,执行语句块,然后释放 lock。

下面我们将来探究 lock 关键字和 Monitor 类的使用。

1,Lock

lock 用于读一个引用类型进行加锁,同一时刻内只有一个线程能够访问此对象。lock 是语法糖,是通过 Monitor 来实现的。

Lock 锁定的对象,应该是静态的引用类型(字符串除外)。

实际上字符串也可以作为锁的对象使用,只是由于字符串对象的特殊性,可能会造成不同位置的不同线程冲突。
如果你能保证字符串的唯一性,例如 Guid 生成的字符串,也是可以作为锁的对象使用的(但不建议)。
锁的对象也不一定要静态才行,也可以通过类实例的成员变量,作为锁对象。

lock 原型

lock 是 Monitor 的语法糖,生成的代码对比:

lock (x)
{
    // Your code...
}
object __lockObj = x;
bool __lockWasTaken = false;
try
{
    System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken);
    // Your code...
}
finally
{
    if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj);
}

这里先不理会 Monitor,后面再说。

lock 编写实例

首先,如果像下面这样写的话,拉出去打 si 吧。

        public void MyLock()
        {
            object o = new object();
            lock (o)
            {
            // 
            }
        }

下面编写一个简单的锁,示例如下:

    class Program
    {
        private static object obj = new object();
        private static int sum = 0;
        static void Main(string[] args)
        {

            Thread thread1 = new Thread(Sum1);
            thread1.Start();
            Thread thread2 = new Thread(Sum2);
            thread2.Start();
            while (true)
            {
                Console.WriteLine($"{DateTime.Now.ToString()}:" + sum);
                Thread.Sleep(TimeSpan.FromSeconds(1));
            }
        }

        public static void Sum1()
        {
            sum = 0;
            lock (obj)
            {
                for (int i = 0; i < 10; i++)
                {
                    sum += i;
                    Console.WriteLine("Sum1");
                    Thread.Sleep(TimeSpan.FromSeconds(2));
                }
            }
        }

        public static void Sum2()
        {
            sum = 0;
            lock (obj)
            {
                for (int i = 0; i < 10; i++)
                {
                    sum += 1;
                    Console.WriteLine("Sum2");
                    Thread.Sleep(TimeSpan.FromSeconds(2));
                }
            }
        }
    }

类将自己设置为锁, 这可以防止恶意代码对公共对象采用做锁。

例如:

  public void Access()
    {
        lock(this) {}
     }

锁可以阻止其它线程执行锁块(lock(o){})中的代码,当锁定时,其它线程必须等待锁中的线程执行完成并释放锁。但是这可能会给程序带来性能影响。
锁不太适合I/O场景,例如文件I/O,繁杂的计算或者操作比较持久的过程,会给程序带来很大的性能损失。

10 种优化锁的性能方法: 10 Ways to Reduce Lock Contention in Threaded Programs - Thinking ParallelThinking Parallel

2,Monitor

此对象提供同步访问对象的机制;Monotor 是一个静态类型,其方法比较少,常用方法如下:

操作说明
Enter, TryEnter获取对象的锁。 此操作还标记关键节的开头。 其他任何线程都不能输入临界区,除非它使用不同的锁定对象执行临界区中的说明。
Wait释放对象的锁,以允许其他线程锁定并访问对象。 调用线程会等待另一个线程访问对象。 使用脉冲信号通知等待线程关于对象状态的更改。
Pulse 、PulseAll将信号发送到一个或多个等待线程。 信号通知等待线程:锁定对象的状态已更改,锁的所有者已准备好释放该锁。 正在等待的线程置于对象的就绪队列中,因此它可能最终接收对象的锁。 线程锁定后,它可以检查对象的新状态,以查看是否已达到所需的状态。
Exit释放对象的锁。 此操作还标记受锁定对象保护的临界区的结尾。

怎么用呢

下面是一个很简单的示例:

        private static object obj = new object();
        private static bool acquiredLock = false;
		
		public static void Test()
        {
            try
            {
                Monitor.Enter(obj, ref acquiredLock);
            }
            catch { }
            finally
            {
                if (acquiredLock)
                    Monitor.Exit(obj);
            }
        }

Monitor.Enter 锁定 obj 这个对象,并且设置 acquiredLock 为 true,告诉别人 obj 已经被锁定。

最后结束时,判断 acquiredLock ,释放锁,并设置 acquiredLock 为 false。

解释一下

临界区:指被某些符号包围的范围。例如 {} 内。

Monitor 对象的 Enter 和 Exit 方法来标记临界区的开头和结尾。

Enter() 方法获取锁后,能够保证只有单个线程能够使用临界区中的代码。使用 Monitor 类,最好搭配 try{...}catch{...}finally{...} 来使用,因为如果获取到锁但是没有释放锁的话,会导致其它线程无限阻塞,即发生死锁。

一般来说,lock 关键字够用了。

示例

下面示范了多个线程如何使用 Monitor 来实现锁:

       private static object obj = new object();
        private static bool acquiredLock = false;
        static void Main(string[] args)
        {
            new Thread(Test1).Start();
            Thread.Sleep(1000);
            new Thread(Test2).Start();
        }

        public static void Test1()
        {
            try
            {
                Monitor.Enter(obj, ref acquiredLock);
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("Test1正在锁定资源");
                    Thread.Sleep(1000);
                }

            }
            catch { }
            finally
            {
                if (acquiredLock)
                    Monitor.Exit(obj);
                Console.WriteLine("Test1已经释放资源");
            }
        }
        public static void Test2()
        {
            bool isGetLock = false;
            Monitor.Enter(obj);
            try
            {
                Monitor.Enter(obj, ref acquiredLock);
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("Test2正在锁定资源");
                    Thread.Sleep(1000);
                }

            }
            catch { }
            finally
            {
                if (acquiredLock)
                    Monitor.Exit(obj);
                Console.WriteLine("Test2已经释放资源");
            }
        }

设置获取锁的时效

如果对象已经被锁定,另一个线程使用 Monitor.Enter 对象,就会一直等待另一个线程解除锁定。

但是,如果一个线程发生问题或者出现死锁的情况,锁一直被锁定呢?或者线程具有时效性,超过一段时间不执行,已经没有了意义呢?

我们可以通过 Monitor.TryEnter() 来设置等待时间,超过一段时间后,如果锁还没有释放,就会返回 false。

改造上面的示例如下:

        private static object obj = new object();
        private static bool acquiredLock = false;
        static void Main(string[] args)
        {
            new Thread(Test1).Start();
            Thread.Sleep(1000);
            new Thread(Test2).Start();
        }

        public static void Test1()
        {
            try
            {
                Monitor.Enter(obj, ref acquiredLock);
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("Test1正在锁定资源");
                    Thread.Sleep(1000);
                }
            }
            catch { }
            finally
            {
                if (acquiredLock)
                    Monitor.Exit(obj);
                Console.WriteLine("Test1已经释放资源");
            }
        }
        public static void Test2()
        {
            bool isGetLock = false;
            isGetLock = Monitor.TryEnter(obj, 500);
            if (isGetLock == false)
            {
                Console.WriteLine("锁还没有释放,我不干活了");
                return;
            }
            try
            {
                Monitor.Enter(obj, ref acquiredLock);
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("Test2正在锁定资源");
                    Thread.Sleep(1000);
                }
            }
            catch { }
            finally
            {
                if (acquiredLock)
                    Monitor.Exit(obj);
                Console.WriteLine("Test2已经释放资源");
            }
        }

对于锁的使用,还有很多高级复杂的技术,本文简单地介绍了 Lock 和 Monitor 的使用。

随着教程的深入,会继续学习很多高级的使用方法。

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1.几种同步方法的区别 lockMonitor是.NET用一个特殊结构实现的,Monitor对象是完全托管的、完全可移植的,并且在操作系统资源要求方 面可能更为有效,同步速度较快,但不能跨进程同步。lockMonitor.Enter和Monitor.Exit方法的封装),主要作用是锁定临界区,使临 界区代码只能被获得锁的线程执行。Monitor.Wait和Monitor.Pulse用于线程同步,类似信号操作,个人感觉使用比较复杂,容易造成死 锁。 互斥体Mutex和事件对象EventWaitHandler属于内核对象,利用内核对象进行线程同步,线程必须要在用户模式和内核模 式间切换,所以一般效率很低,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。 互斥体Mutex类似于一个接力棒,拿到接力棒的线程才可以开始跑,当然接力棒一次只属于一个线程(Thread Affinity),如果这个线程不释放接力棒(Mutex.ReleaseMutex),那么没办法,其他所有需要接力棒运行的线程都知道能等着看热 闹。 EventWaitHandle 类允许线程通过发信号互相通信。 通常,一个或多个线程在 EventWaitHandle 上阻止,直到一个未阻止的线程调用 Set 方法,以释放一个或多个被阻止的线程。 2.什么时候需要锁定 首先要理解锁定是解决竞争条件的,也就是多个线程同时访问某个资源,造成意想不到的结果。比如,最简单的情况是,一个计数器,两个线程 同时加一,后果就是损失了一个计数,但相当频繁的锁定又可能带来性能上的消耗,还有最可怕的情况死锁。那么什么情况下我们需要使用锁,什么情况下不需要 呢? 1)只有共享资源才需要锁定 只有可以被多线程访问的共享资源才需要考虑锁定,比如静态变量,再比如某些缓存中的值,而属于线程内部的变量不需要锁定。 2)多使用lock,少用Mutex 如果你一定要使用锁定,请尽量不要使用内核模块的锁定机制,比如.NET的Mutex,Semaphore,AutoResetEvent和 ManuResetEvent,使用这样的机制涉及到了系统在用户模式和内核模式间的切换,性能差很多,但是他们的优点是可以跨进程同步线程,所以应该清 楚的了解到他们的不同和适用范围。 3)了解你的程序是怎么运行的 实际上在web开发中大多数逻辑都是在单个线程中展开的,一个请求都会在一个单独的线程中处理,其中的大部分变量都是属于这个线程的,根本没有必要考虑锁 定,当然对于ASP.NET中的Application对象中的数据,我们就要考虑加锁了。 4)把锁定交给数据库 数 据库除了存储数据之外,还有一个重要的用途就是同步,数据库本身用了一套复杂的机制来保证数据的可靠和一致性,这就为我们节省了很多的精力。保证了数据源 头上的同步,我们多数的精力就可以集中在缓存等其他一些资源的同步访问上了。通常,只有涉及到多个线程修改数据库中同一条记录时,我们才考虑加锁。 5)业务逻辑对事务和线程安全的要求 这 条是最根本的东西,开发完全线程安全的程序是件很费时费力的事情,在电子商务等涉及金融系统的案例中,许多逻辑都必须严格的线程安全,所以我们不得不牺牲 一些性能,和很多的开发时间来做这方面的工作。而一般的应用中,许多情况下虽然程序有竞争的危险,我们还是可以不使用锁定,比如有的时候计数器少一多一, 对结果无伤大雅的情况下,我们就可以不用去管它。 3.InterLocked类 Interlocked 类提供了同步对多个线程共享的变量的访问的方法。如果该变量位于共享内存中,则不同进程的线程就可以使用该机制。互锁操作是原子的,即整个操作是不能由相 同变量上的另一个互锁操作所中断的单元。这在抢先多线程操作系统中是很重要的,在这样的操作系统中,线程可以在从某个内存地址加载值之后但是在有机会更改 和存储该值之前被挂起。
C#中,我们可以使用线程同步技术来确保多个线程之间的正确协调和互斥访问共享资源。以下是一些常用的线程同步技术: 1. 互斥锁(Mutex):互斥锁是一种最基本的线程同步机制,它确保在任何时候只有一个线程可以访问被保护的资源。通过`lock`关键字或`Mutex`类来实现互斥锁。 ```csharp private static readonly object lockObject = new object(); lock (lockObject) { // 访问共享资源的代码 } ``` 2. 信号量(Semaphore):信号量是一种计数器,它限制同时访问某个资源的线程数量。通过`Semaphore`类来实现信号量。 ```csharp private static Semaphore semaphore = new Semaphore(2, 2); // 允许同时两个线程访问 semaphore.WaitOne(); // 请求访问资源 try { // 访问共享资源的代码 } finally { semaphore.Release(); // 释放资源 } ``` 3. 事件(Event):事件是一种线程间的通信机制,它允许一个或多个线程等待某个操作完成。通过`ManualResetEvent`或`AutoResetEvent`类来实现事件。 ```csharp private static ManualResetEvent eventObject = new ManualResetEvent(false); // 初始状态为非终止状态 // 线程1等待事件 eventObject.WaitOne(); // 线程2触发事件 eventObject.Set(); ``` 4. 互斥体(Monitor):互斥体是一种特殊的内核对象,它提供了一种同步机制,确保只有一个线程可以进入临界区。通过`Monitor`类来实现互斥体。 ```csharp private static readonly object lockObject = new object(); Monitor.Enter(lockObject); try { // 访问共享资源的代码 } finally { Monitor.Exit(lockObject); } ``` 这些线程同步技术可以帮助你在多线程环境中实现线程安全和资源互斥访问。根据实际需求,选择适合的同步机制来确保线程间的正确协调。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值