C#多线程学习笔记十二


在.net中锁机制很多,事件锁,信号量,互斥锁,读写锁,互锁,易变构造等。
分类:
<1>用户模式锁
【就是通过一些CPU指令或者一个死循环】来达到thread等待和休眠
<2>内核模式锁
就是通过win32底层的代码,来实现thread的各种操作 Thread.Sleep
<3>混合锁
用户模式+内核模式 xxxslim

用户模式锁

易变构造

一个线程读 一个线程写,在release的某种情况下会有debug。如笔记2中的release版本带来的问题。解决方法:
1.Thread.MemoryBarrier
2.VolatileRead
3.Volatile关键字:不可以底层对代码进行优化,读写都是从Memory中读取。即读取的都是最新的数据

        public static volatile bool isStop = false;
        static void Main(string[] args)
        {
    
    
            
            Thread t = new Thread(() =>
            {
    
    
                var isSuccess = false;
                while (!isStop)
                {
    
    
                    isSuccess = !isSuccess;
                }
            });
            t.Start();
            Thread.Sleep(1000);
            isStop = true;
            t.Join();
            Console.WriteLine("主线程执行结束");
            Console.ReadKey();
        }

互锁结构:Interlocked

【只能做一些简单的计算】用途

Interlocked.Increment 自增操作 Interlocked.Increment(i);相当于++i;
Interlocked.Decrement 自减操作 Interlocked.Decrement(i);相当于–i;
Interlocked.Add 增加指定的值 Interlocked.Add(ref sum,5);相当于sum+5
Interlocked.ExChange 赋值 Interlocked.ExChange(ref sum,10);相当sum = 10;
Interlocked.CompareExChange 比较赋值

旋转锁:SpinLock

特殊的业务逻辑让Thread在用户模式下进行自旋,欺骗CPU当前Thread正在运行中。用户模式到内核模式到用户模式的循环会消耗内存,通过自旋留在用户模式,防止过度消耗。

        static SpinLock sp = new SpinLock();
        static void Main(string[] args)
        {
    
    
            for (int i = 0; i < 5; i++)
            {
    
    
                Task.Factory.StartNew(Run);
            }
            Console.Read();
        }

        static int nums = 0;

        static void Run() 
        {
    
    
            for (int i = 0; i < 100; i++)
            {
    
    
                try
                {
    
    
                    var b = false;

                    sp.Enter(ref b);

                    Console.WriteLine(nums++);
                }
                catch (Exception ex)
                {
    
    
                    Console.WriteLine(ex.Message);
                }
                finally 
                {
    
    
                    sp.Exit();
                }
            }
        }

内核模式锁

不到万不得已的情况下,不要使用内核模式,因为代价太大,而且有更多的方式可以替代:用户模式锁、混合锁机制、Lock等。

事件锁

1.自动事件锁【AutoResetEvent】
场景:可以用此锁实现多线程环境下某个变量的自增
true:表示终止状态 false:表示非终止状态
现实中的场景:检票闸机,用车票来实现进站操作。
true(终止状态):闸机里没有车票 =》初始状态
false(非终止状态):闸机里有车票
WaitOne()方法相当于往闸机中放入一张车票
Set()方法相当于从闸机中取出火车票

        static AutoResetEvent areLock = new AutoResetEvent(false);
        static void Main(string[] args)
        {
    
    
            areLock.WaitOne();
            Console.WriteLine("pass");
            areLock.Set();
            Console.Read();
        }

结果:什么也不输出,因为实例化时参数为false,WaitOne方法阻塞了当前线程。
将false改为true,即可输出“pass”

2.手动事件锁【ManualResetEvent】
现实场景:铁道栅栏。如果有火车来,栅栏就会合围阻止行人机动车等通过,等火车走了栅栏就会收起让行人机动车等通过。
true(终止状态):栅栏没有合围,没有阻止行人机动车等通过
false(非终止状态):栅栏合围,阻止行人机动车等通过

注意:
ManualResetEvent和AutoResetEvent模式是不一样的,所以不能混用。
AutoResetEvent收到Set后,一次只能执行一个线程,其它线程继续 WaitOne。就行车站闸机依次检票进展。

ManualResetEvent收到Set后,所有处理WaitOne状态线程均继续执行。就像铁路栅栏打开,所有人车都会蜂拥而过。

以下这两段代码,使用ManualResetEvent会输出两个pass,使用AutoResetEvent只会输出一个pass。

        static AutoResetEvent areLock = new AutoResetEvent(true);
        static void Main(string[] args)
        {
    
    
            areLock.WaitOne();
            Console.WriteLine("pass");
            areLock.WaitOne();
            Console.WriteLine("pass");
            areLock.Set();
            Console.Read();
        }
 static ManualResetEvent mreLock = new ManualResetEvent(true);
        static void Main(string[] args)
        {
    
    
            mreLock.WaitOne();
            Console.WriteLine("pass");
            mreLock.WaitOne();
            Console.WriteLine("pass");
            Console.Read();
        }

信号量:Semaphore

他是通过int数值来控制线程个数

        static Semaphore seLock = new Semaphore(1, 1);
        static void Main(string[] args)
        {
    
    
            for (int i = 0; i < 5; i++)
            {
    
    
                Task.Factory.StartNew(Run);
            }
            Console.Read();
        }

        static int nums = 0;

        static void Run()
        {
    
    
            for (int i = 0; i < 100; i++)
            {
    
    
                try
                {
    
    
                    seLock.WaitOne();

                    Console.WriteLine(nums++);

                    seLock.Release();
                }
                catch (Exception ex)
                {
    
    
                    Console.WriteLine(ex.Message);
                }
            }
        }

互斥锁:mutex

        static Mutex mutex = new Mutex();
        static void Main(string[] args)
        {
    
    
            for (int i = 0; i < 5; i++)
            {
    
    
                Task.Factory.StartNew(Run);
            }
            Console.Read();
        }

        static int nums = 0;

        static void Run()
        {
    
    
            for (int i = 0; i < 100; i++)
            {
    
    
                try
                {
    
    
                    mutex.WaitOne();

                    Console.WriteLine(nums++);

                    mutex.ReleaseMutex();
                }
                catch (Exception ex)
                {
    
    
                    Console.WriteLine(ex.Message);
                }
            }
        }

这三种锁都有waione方法,因为他们都是继承于waithandle的。三种锁都是同根生的,底层都是通过SafeWaitHandle来对win32api的引用。

读写锁:ReaderWriterLock

不是从限定个数的角度出发。而是按照读写的角度进行功能分区。
模拟:多个线程读,一个线程写,那么写的线程是否会阻止读取的线程。

        static ReaderWriterLock rwLock = new ReaderWriterLock();
        static void Main(string[] args)
        {
    
    
            for (int i = 0; i < 5; i++)
            {
    
    
                Task.Factory.StartNew(Read);
            }

            Task.Factory.StartNew(Write);

            Console.Read();
        }

        static int nums = 0;

        /// <summary>
        /// 线程读
        /// </summary>
        static void Read()
        {
    
    
            while (true)
            {
    
    
                Thread.Sleep(10);
                rwLock.AcquireReaderLock(int.MaxValue);
                Thread.Sleep(10);
                Console.WriteLine("当前t = {0} 进行读取 {1}", 
                                  Thread.CurrentThread.ManagedThreadId, DateTime.Now);
                rwLock.ReleaseReaderLock();
            }
        }

        /// <summary>
        /// 线程写
        /// </summary>
        static void Write() 
        {
    
    
            while (true)
            {
    
    
                Thread.Sleep(3000);
                rwLock.AcquireWriterLock(int.MaxValue);
                Thread.Sleep(5000);
                Console.WriteLine("当前t = {0} 进行写入 .................{1}", 
                                  Thread.CurrentThread.ManagedThreadId, DateTime.Now);
                rwLock.ReleaseWriterLock();
            }
        }

输出结果:读操作会被写操作打断5秒。说明读写操作是互斥的。
在这里插入图片描述

CountDownEvent

限制线程数的一个机制,非常实用。
static CountdownEvent cdeLock = new CountdownEvent(threadcount);
初始化的时候设置了一个默认的thread上限threadcount,当你使用一个thread,这个threadcount就会减一,直到为0,相当于执行结束,task.wait()执行完成,继续执行下一步。
cdeLock.Reset() 重置当前的threadcount。
cdeLock.Signal();当前的threadcount减一操作。
cdeLock.Wait();相当于Task.WaitAll();

        static CountdownEvent cdeLock = new CountdownEvent(10);

        static void Main(string[] args)
        {
    
    
            cdeLock.Reset(10);
            for (int i = 0; i < 10; i++)
            {
    
    
                Task.Factory.StartNew(LoadOrders);
            }
            cdeLock.Wait();

            cdeLock.Reset(5);
            for (int i = 0; i < 5; i++)
            {
    
    
                Task.Factory.StartNew(LoadProducts);
            }
            cdeLock.Wait();

            cdeLock.Reset(2);
            for (int i = 0; i < 2; i++)
            {
    
    
                Task.Factory.StartNew(LoadUsers);
            }
            cdeLock.Wait();

            Console.WriteLine("执行结束");
            Console.Read();
        }

        static void LoadOrders() 
        {
    
    
            Console.WriteLine("LoadOrders...{0}",Thread.CurrentThread.ManagedThreadId);
            cdeLock.Signal();
        }

        static void LoadProducts()
        {
    
    
            Console.WriteLine("LoadProducts...{0}", Thread.CurrentThread.ManagedThreadId);
            cdeLock.Signal();
        }

        static void LoadUsers()
        {
    
    
            Console.WriteLine("LoadUsers...{0}", Thread.CurrentThread.ManagedThreadId);
            cdeLock.Signal();
        }
    }

运行结果:各方法输出是互斥的。
在这里插入图片描述

监视锁

限定线程个数的一种锁:Monitor
Enter 锁住一个资源
Exit 解锁某个资源

        static object locker = new object();
        static void Main(string[] args)
        {
    
    
            for (int i = 0; i < 5; i++)
            {
    
    
                Task.Factory.StartNew(Run);
            }
            Console.Read();
        }

        static int nums = 0;

        static void Run()
        {
    
    
            for (int i = 0; i < 100; i++)
            {
    
    
                var b = false;
                try
                {
    
    
                    Monitor.Enter(locker, ref b);
                    Console.WriteLine(nums++);
                }
                catch (Exception ex)
                {
    
    
                    Console.WriteLine(ex.Message);
                }
                finally
                {
    
    
                    if (b)
                        Monitor.Exit(locker);
                }
            }
        }

输出结果0-499依次输出。说明5个Task是互斥依次运行的,否则输出是乱序的。
但是,为了严谨,是不是每次都要try catch finally 还要if来判断呢,
为了不这么麻烦,可以使用Lock。

        static void Run()
        {
    
    
            for (int i = 0; i < 100; i++)
            {
    
    
                lock (locker)
                {
    
    
                    Console.WriteLine(nums++);
                }
            }
        }

因为众多的锁机制中,只有Monitor有专用的语法糖。本质就是利用堆上的同步块实现资源锁定。
总结:Enter中添加的对象,相当于把对象的同步块索引和CLR的同步块数组关联。Exit中释放的资源,相当于把对象的同步块索引和CLR的同步块数组进行了解绑。
注意事项:你锁住的资源一定要让你可访问的线程能够访问到。所以锁住的资源千万不要用值类型。比如上图中的locker变量要放在外面,而不是Run方法里面。

混合模式锁

混合模式锁 = 用户模式锁 + 内核模式锁

混合模式锁有SemaphoreSlim、ManualResetEventSlim、ReaderWriterLockSlim,不用说他们比之前的内核版本性能要高的多。具体使用方法和前面介绍的内核版本一样,只比较其差别。

ManualResetEventSlim

优化点:
<1>构造函数中已经可以不提供默认状态,默认状态就是ManualResetEvent的false状态。
<2>使用Wait来代替WaitOne(WaitOne是WaitHandel祖先类提供的方法),其他方法保持原来的方法名和用法。
<3>支持任务取消

ReaderWriterLockSlim

用EnterReadLock来代替AcquireReaderLock方法,性能要比内核版本要高得多,而且不用输入参数。

SemaphoreSlim

可以在某个时刻想改变默认线程并行个数。

        //默认一个线程同时运行,最大10个
        static SemaphoreSlim shSlim = new SemaphoreSlim(1, 10);
        
        static void Main(string[] args)
        {
    
    
            for (int i = 0; i < 10; i++)
            {
    
    
                Task.Factory.StartNew(Run);
            }

            //某个时刻想改变默认线程并行个数,从1改成9
            Thread.Sleep(2000);
            shSlim.Release(9);
            Console.Read();
        }

        static void Run()
        {
    
    
            shSlim.Wait();
            Thread.Sleep(1000 * 5);
            Console.WriteLine("当前thread = {0}正在运行   {1}", 
                               Thread.CurrentThread.ManagedThreadId,DateTime.Now);
            shSlim.Release();
        }

猜你喜欢

转载自blog.csdn.net/Z960515/article/details/113443239