C#多线程(10):读写锁

简介: C#多线程(10):读写锁

ReaderWriterLockSlim

ReaderWriterLock 类:定义支持单个写线程和多个读线程的锁。

ReaderWriterLockSlim 类:表示用于管理资源访问的锁定状态,可实现多线程读取或进行独占式写入访问。

两者的 API 十分接近,而且 ReaderWriterLockSlim 相对 ReaderWriterLock 来说 更加安全。因此本文主要讲解 ReaderWriterLockSlim 。

两者都是实现多个线程可同时读取、只允许一个线程写入的类。


ReaderWriterLockSlim

老规矩,先大概了解一下 ReaderWriterLockSlim 常用的方法。


常用方法



方法 说明
EnterReadLock() 尝试进入读取模式锁定状态。
EnterUpgradeableReadLock() 尝试进入可升级模式锁定状态。
EnterWriteLock() 尝试进入写入模式锁定状态。
ExitReadLock() 减少读取模式的递归计数,并在生成的计数为 0(零)时退出读取模式。
ExitUpgradeableReadLock() 减少可升级模式的递归计数,并在生成的计数为 0(零)时退出可升级模式。
ExitWriteLock() 减少写入模式的递归计数,并在生成的计数为 0(零)时退出写入模式。
TryEnterReadLock(Int32) 尝试进入读取模式锁定状态,可以选择整数超时时间。
TryEnterReadLock(TimeSpan) 尝试进入读取模式锁定状态,可以选择超时时间。
TryEnterUpgradeableReadLock(Int32) 尝试进入可升级模式锁定状态,可以选择超时时间。
TryEnterUpgradeableReadLock(TimeSpan) 尝试进入可升级模式锁定状态,可以选择超时时间。
TryEnterWriteLock(Int32) 尝试进入写入模式锁定状态,可以选择超时时间。
TryEnterWriteLock(TimeSpan) 尝试进入写入模式锁定状态,可以选择超时时间。


ReaderWriterLockSlim 的读、写入锁模板如下:


private static ReaderWriterLockSlim toolLock = new ReaderWriterLockSlim();
    // 读
        private T Read()
        {
            try
            {
                toolLock.EnterReadLock();           // 获取读取锁
                return obj;
            }
            catch { }
            finally
            {
                toolLock.ExitReadLock();            // 释放读取锁
            }
            return default;
        }
        // 写
        public void Write(int key, int value)
        {
            try
            {
                toolLock.EnterUpgradeableReadLock();
                try
                {
                    toolLock.EnterWriteLock();
                    /*
                     * 
                    */
                }
                catch
                {
                }
                finally
                {
                    toolLock.ExitWriteLock();
                }
            }
            catch { }
            finally
            {
                toolLock.ExitUpgradeableReadLock();
            }
        }


订单系统示例


这里来模拟一个简单粗糙的订单系统。

开始编写代码前,先来了解一些方法的具体使用。


EnterReadLock() / TryEnterReadLockExitReadLock() 成对出现。

EnterWriteLock() / TryEnterWriteLock()ExitWriteLock() 成对出现。

EnterUpgradeableReadLock() 进入可升级的读模式锁定状态。

EnterReadLock() 使用 EnterUpgradeableReadLock() 进入升级状态,在恰当时间点 通过 EnterWriteLock() 进入写模式。(也可以倒过来)


定义三个变量:

ReaderWriterLockSlim 多线程读写锁;

MaxId 当前订单 Id 的最大值;

orders 订单表;


private static ReaderWriterLockSlim tool = new ReaderWriterLockSlim();   // 读写锁
        private static int MaxId = 1;
        public static List<DoWorkModel> orders = new List<DoWorkModel>();       // 订单表


// 订单模型
        public class DoWorkModel
        {
            public int Id { get; set; }     // 订单号
            public string UserName { get; set; }    // 客户名称
            public DateTime DateTime { get; set; }  // 创建时间
        }


然后实现查询和创建订单的两个方法。

分页查询订单:

在读取前使用 EnterReadLock() 获取锁;

读取完毕后,使用 ExitReadLock() 释放锁。

这样能够在多线程环境下保证每次读取都是最新的值。


// 分页查询订单
        private static DoWorkModel[] DoSelect(int pageNo, int pageSize)
        {
            try
            {
                DoWorkModel[] doWorks;
                tool.EnterReadLock();           // 获取读取锁
                doWorks = orders.Skip((pageNo - 1) * pageSize).Take(pageSize).ToArray();
                return doWorks;
            }
            catch { }
            finally
            {
                tool.ExitReadLock();            // 释放读取锁
            }
            return default;
        }


创建订单:

创建订单的信息十分简单,知道用户名和创建时间就行。

订单系统要保证的时每个 Id 都是唯一的(实际情况应该用Guid),这里为了演示读写锁,设置为 数字。


在多线程环境下,我们不使用 Interlocked.Increment() ,而是直接使用 += 1,因为有读写锁的存在,所以操作也是原则性的。


// 创建订单
        private static DoWorkModel DoCreate(string userName, DateTime time)
        {
            try
            {
                tool.EnterUpgradeableReadLock();        // 升级
                try
                {
                    tool.EnterWriteLock();              // 获取写入锁
                    // 写入订单
                    MaxId += 1;                         // Interlocked.Increment(ref MaxId);
                    DoWorkModel model = new DoWorkModel
                    {
                        Id = MaxId,
                        UserName = userName,
                        DateTime = time
                    };
                    orders.Add(model);
                    return model;
                }
                catch { }
                finally
                {
                    tool.ExitWriteLock();               // 释放写入锁
                }
            }
            catch { }
            finally
            {
                tool.ExitUpgradeableReadLock();         // 降级
            }
            return default;
        }


Main 方法中:

开 5 个线程,不断地读,开 2 个线程不断地创建订单。线程创建订单时是没有设置 Thread.Sleep() 的,因此运行速度十分快。

Main 方法里面的代码没有什么意义。


static void Main(string[] args)
        {
            // 5个线程读
            for (int i = 0; i < 5; i++)
            {
                new Thread(() =>
                {
                    while (true)
                    {
                        var result = DoSelect(1, MaxId);
                        if (result is null)
                        {
                            Console.WriteLine("获取失败");
                            continue;
                        }
                        foreach (var item in result)
                        {
                            Console.Write($"{item.Id}|");
                        }
                        Console.WriteLine("\n");
                        Thread.Sleep(1000);
                    }
                }).Start();
            }
            for (int i = 0; i < 2; i++)
            {
                new Thread(() =>
                {
                    while(true)
                    {
                        var result = DoCreate((new Random().Next(0, 100)).ToString(), DateTime.Now);      // 模拟生成订单
                        if (result is null)
                            Console.WriteLine("创建失败");
                        else Console.WriteLine("创建成功");
                    }
                }).Start();
            }
        }


在 ASP.NET Core 中,则可以利用读写锁,解决多用户同时发送 HTTP 请求带来的数据库读写问题。


这里就不做示例了。

如果另一个线程发生问题,导致迟迟不能交出写入锁,那么可能会导致其它线程无限等待。


那么可以使用 TryEnterWriteLock() 并且设置等待时间,避免阻塞时间过长。


bool isGet = tool.TryEnterWriteLock(500);


并发字典写示例


因为理论的东西,笔者这里不会说太多,主要就是先掌握一些 API(方法、属性) 的使用,然后简单写出示例,后面再慢慢深入了解底层原理。


这里来写一个多线程共享使用字典(Dictionary)的使用示例。


增加两个静态变量:

private static ReaderWriterLockSlim toolLock = new ReaderWriterLockSlim();
        private static Dictionary<int, int> dict = new Dictionary<int, int>();


实现一个写操作:

public static void Write(int key, int value)
        {
            try
            {
                // 升级状态
                toolLock.EnterUpgradeableReadLock();
                // 读,检查是否存在
                if (dict.ContainsKey(key))
                    return;
                try
                {
                    // 进入写状态
                    toolLock.EnterWriteLock();
                    dict.Add(key,value);
                }
                finally
                {
                    toolLock.ExitWriteLock();
                }
            }
            finally
            {
                toolLock.ExitUpgradeableReadLock();
            }
        }


上面没有 catch { } 是为了更好观察代码,因为使用了读写锁,理论上不应该出现问题的。


模拟五个线程同时写入字典,由于不是原子操作,所以 sum 的值有些时候会出现重复值。


原子操作请参考:https://www.cnblogs.com/whuanle/p/12724371.html#1,出现问题


private static int sum = 0;
        public static void AddOne()
        {
            for (int i = 0; i < 100_0000; i++)
            {
                sum += 1;
                Write(sum,sum);
            }
        }
        static void Main(string[] args)
        {
            for (int i = 0; i < 5; i++)
                new Thread(() => { AddOne(); }).Start();
            Console.ReadKey();
        }


ReaderWriterLock


大多数情况下都是推荐 ReaderWriterLockSlim 的,而且两者的使用方法十分接近。

例如 AcquireReaderLock 是获取读锁,AcquireWriterLock 获取写锁。使用对应的方法即可替换 ReaderWriterLockSlim 中的示例。


这里就不对 ReaderWriterLock 进行赘述了。

ReaderWriterLock 的常用方法如下:


方法 说明
AcquireReaderLock(Int32) 使用一个 Int32 超时值获取读线程锁。
AcquireReaderLock(TimeSpan) 使用一个 TimeSpan 超时值获取读线程锁。
AcquireWriterLock(Int32) 使用一个 Int32 超时值获取写线程锁。
AcquireWriterLock(TimeSpan) 使用一个 TimeSpan 超时值获取写线程锁。
AnyWritersSince(Int32) 指示获取序列号之后是否已将写线程锁授予某个线程。
DowngradeFromWriterLock(LockCookie) 将线程的锁状态还原为调用 UpgradeToWriterLock(Int32) 前的状态。
ReleaseLock() 释放锁,不管线程获取锁的次数如何。
ReleaseReaderLock() 减少锁计数。
ReleaseWriterLock() 减少写线程锁上的锁计数。
RestoreLock(LockCookie) 将线程的锁状态还原为调用 ReleaseLock() 前的状态。
UpgradeToWriterLock(Int32) 使用一个 Int32 超时值将读线程锁升级为写线程锁。
UpgradeToWriterLock(TimeSpan) 使用一个 TimeSpan 超时值将读线程锁升级为写线程锁。


官方示例可以看:

https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.readerwriterlock?view=netcore-3.1#examples

相关文章
|
2月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
246 1
|
10月前
|
物联网 数据处理 C#
C#实现上位机开发,串口通信,读写串口数据并处理16进制数据
C#实现上位机开发,串口通信,读写串口数据并处理16进制数据。在自动化、物联网以及工业控制行业中,上位机开发是一项重要的技能。本教程主要介绍使用C#进行上位机开发,重点在于串口通信和数据处理。
1920 82
|
6月前
|
机器学习/深度学习 监控 算法
局域网行为监控软件 C# 多线程数据包捕获算法:基于 KMP 模式匹配的内容分析优化方案探索
本文探讨了一种结合KMP算法的多线程数据包捕获与分析方案,用于局域网行为监控。通过C#实现,该系统可高效检测敏感内容、管理URL访问、分析协议及审计日志。实验表明,相较于传统算法,KMP在处理大规模网络流量时效率显著提升。未来可在算法优化、多模式匹配及机器学习等领域进一步研究。
190 0
|
11月前
|
并行计算 安全 Java
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
在Python开发中,GIL(全局解释器锁)一直备受关注。本文基于CPython解释器,探讨GIL的技术本质及其对程序性能的影响。GIL确保同一时刻只有一个线程执行代码,以保护内存管理的安全性,但也限制了多线程并行计算的效率。文章分析了GIL的必要性、局限性,并介绍了多进程、异步编程等替代方案。尽管Python 3.13计划移除GIL,但该特性至少要到2028年才会默认禁用,因此理解GIL仍至关重要。
840 16
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
|
供应链 安全 NoSQL
PHP 互斥锁:如何确保代码的线程安全?
在多线程和高并发环境中,确保代码段互斥执行至关重要。本文介绍了 PHP 互斥锁库 `wise-locksmith`,它提供多种锁机制(如文件锁、分布式锁等),有效解决线程安全问题,特别适用于电商平台库存管理等场景。通过 Composer 安装后,开发者可以利用该库确保在高并发下数据的一致性和安全性。
188 6
|
12月前
|
Java 关系型数据库 MySQL
【JavaEE“多线程进阶”】——各种“锁”大总结
乐/悲观锁,轻/重量级锁,自旋锁,挂起等待锁,普通互斥锁,读写锁,公不公平锁,可不可重入锁,synchronized加锁三阶段过程,锁消除,锁粗化
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
307 0
|
安全 C# 数据安全/隐私保护
实现C#编程文件夹加锁保护
【10月更文挑战第16天】本文介绍了两种用 C# 实现文件夹保护的方法:一是通过设置文件系统权限,阻止普通用户访问;二是使用加密技术,对文件夹中的文件进行加密,防止未授权访问。提供了示例代码和使用方法,适用于不同安全需求的场景。
624 0
|
安全 调度 数据安全/隐私保护
iOS线程锁
iOS线程锁
164 0
|
Java API
【多线程】乐观/悲观锁、重量级/轻量级锁、挂起等待/自旋锁、公平/非公锁、可重入/不可重入锁、读写锁
【多线程】乐观/悲观锁、重量级/轻量级锁、挂起等待/自旋锁、公平/非公锁、可重入/不可重入锁、读写锁
136 0

热门文章

最新文章