本篇的内容主要是介绍 ReaderWriterLockSlim 类,来实现多线程下的读写分离。

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

C#多线程(10):读写锁的更多相关文章

  1. Java多线程之读写锁机制

    Java多线程中有很多的锁机制,他们都有各自的应用场景,例如今天我说的这种锁机制:读写锁 读写锁,见名知意,主要可以进行两种操作,读和写操作,他们之间结合使用起来又是各不相同的.比如多个线程之间可以同 ...

  2. 技术笔记:Delphi多线程应用读写锁

    在多线程应用中锁是一个很简单又很复杂的技术,之所以要用到锁是因为在多进程/线程环境下,一段代码可能会被同时访问到,如果这段代码涉及到了共享资源(数据)就需要保证数据的正确性.也就是所谓的线程安全.之前 ...

  3. java多线程 -- ReadWriteLock 读写锁

    写一条线程,读多条线程能够提升效率. 写写/读写 需要“互斥”;读读 不需要互斥. ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作.只要没有 writer,读取锁 ...

  4. java 多线程 day12 读写锁

    import java.util.Random;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent. ...

  5. 用读写锁三句代码解决多线程并发写入文件 z

    C#使用读写锁三句代码简单解决多线程并发写入文件时提示“文件正在由另一进程使用,因此该进程无法访问此文件”的问题 在开发程序的过程中,难免少不了写入错误日志这个关键功能.实现这个功能,可以选择使用第三 ...

  6. C# 防止同时调用=========使用读写锁三行代码简单解决多线程并发的问题

    http://www.jb51.net/article/99718.htm     本文主要介绍了C#使用读写锁三行代码简单解决多线程并发写入文件时提示"文件正在由另一进程使用,因此该进程无 ...

  7. C#使用读写锁三行代码简单解决多线程并发写入文件时线程同步的问题

    (补充:初始化FileStream时使用包含文件共享属性(System.IO.FileShare)的构造函数比使用自定义线程锁更为安全和高效,更多内容可点击参阅) 在开发程序的过程中,难免少不了写入错 ...

  8. C#使用读写锁解决多线程并发写入文件时线程同步的问题

    读写锁是以 ReaderWriterLockSlim 对象作为锁管理资源的,不同的 ReaderWriterLockSlim 对象中锁定同一个文件也会被视为不同的锁进行管理,这种差异可能会再次导致文件 ...

  9. c++ 读写锁

    #ifndef THREAD_UTIL_H #define THREAD_UTIL_H #include <pthread.h> namespace spider { class Auto ...

  10. Java多线程13:读写锁和两种同步方式的对比

    读写锁ReentrantReadWriteLock概述 大型网站中很重要的一块内容就是数据的读写,ReentrantLock虽然具有完全互斥排他的效果(即同一时间只有一个线程正在执行lock后面的任务 ...

随机推荐

  1. requests模块安装

    使用python写接口,必不可少的就是requests,所以事先要在python中安装requests     ​一.使用pip install安装(项目的命令行终端使用) 1.配置下载源地址路径(清 ...

  2. 【Go WEB进阶实战】开源的电商前后台API系统

    前言 最近有很多小伙伴私信我:在学完Go基础后,想使用一个框架实战一个商业项目,但是又苦于不知道选择什么框架,更不知道做什么商业项目. 为了解决大家这些问题,我结合自己的项目经历,为大家开源了一个简单 ...

  3. 【小实验】golang中的字节对齐

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 使用golang来调用SIMD指令,发现程序崩溃了: __ ...

  4. 【windows Server 2019系列】 构建IIS服务器

    个人名片: 对人间的热爱与歌颂,可抵岁月冗长 Github‍:念舒_C.ying CSDN主页️:念舒_C.ying 个人博客 :念舒_C.ying Web服务器也称为WWW(World Wide W ...

  5. 从零开始配置vim(27)——代码片段

    我们之前介绍过缩写相关的内容,缩写是可以自动帮我们将缩写的单词展开成一段完整的话.但是代码本身是结构话的,仅仅使用缩写来配置是无法完成自动生成代码这个步骤的.好在我们大量的插件来进行配置.本篇我们将要 ...

  6. 【JVM】运行时内存分配

    程序计数器 用于标识线程执行到了字节码文件(class文件)的哪一行,当执行native方法时,值为undefined,各个线程私有 Java虚拟机栈 每个线程独有,每个方法执行时会创建一个栈帧,用于 ...

  7. ElasticSearch实战指南必知必会:安装分词器、高级查询、打分机制

    ElasticSearch实战指南必知必会:安装中文分词器.ES-Python使用.高级查询实现位置坐标搜索以及打分机制 1.ElasticSearch之-安装中文分词器 elasticsearch ...

  8. 【二】分布式训练---参数服务器训练(飞桨paddle1.8)

    1.参数服务器训练简介 参数服务器训练是分布式训练领域普遍采用的编程架构,主要解决以下两类问题: 模型参数过大:单机内存空间不足,需要采用分布式存储. 训练数据过多:单机训练太慢,需要加大训练节点,来 ...

  9. 4.3 Windows驱动开发:监控进程与线程对象操作

    在内核中,可以使用ObRegisterCallbacks这个内核回调函数来实现监控进程和线程对象操作.通过注册一个OB_CALLBACK_REGISTRATION回调结构体,可以指定所需的回调函数和回 ...

  10. 面试官:什么是JIT、逃逸分析、锁消除、栈上分配和标量替换?

    JIT.逃逸分析.锁消除.栈上分配和标量替换等都属于 JVM 的优化手段,JVM 优化手段是指在运行 Java 程序时,通过对字节码的编译和执行过程进行优化,以提升程序的性能和效率. JVM 优化手段 ...