C#使用读写锁三行代码简单解决多线程并发写入文件时线程同步的问题
在开发程序的过程中,难免少不了写入错误日志这个关键功能。实现这个功能,可以选择使用第三方日志插件,也可以选择使用数据库,还可以自己写个简单的方法把错误信息记录到日志文件。
选择最后一种方法实现的时候,若对文件操作与线程同步不熟悉,问题就有可能出现了,因为同一个文件并不允许多个线程同时写入,否则会提示“文件正在由另一进程使用,因此该进程无法访问此文件”。
这是文件的并发写入问题,就需要用到线程同步。而微软也给线程同步提供了一些相关的类可以达到这样的目的,本文使用到的 System.Threading.ReaderWriterLockSlim 便是其中之一。
该类用于管理资源访问的锁定状态,可实现多线程读取或进行独占式写入访问。利用这个类,我们就可以避免在同一时间段内多线程同时写入一个文件而导致的并发写入问题。
读写锁是以 ReaderWriterLockSlim 对象作为锁管理资源的,不同的 ReaderWriterLockSlim 对象中锁定同一个文件也会被视为不同的锁进行管理,这种差异可能会再次导致文件的并发写入问题,所以 ReaderWriterLockSlim 应尽量定义为只读的静态对象。
ReaderWriterLockSlim 有几个关键的方法,本文仅讨论写入锁:
调用 EnterWriteLock 方法 进入写入状态,在调用线程进入锁定状态之前一直处于阻塞状态,因此可能永远都不返回。
调用 TryEnterWriteLock 方法 进入写入状态,可指定阻塞的间隔时间,如果调用线程在此间隔期间并未进入写入模式,将返回false。
调用 ExitWriteLock 方法 退出写入状态,应使用 finally 块执行 ExitWriteLock 方法,从而确保调用方退出写入模式。
Don't talk, show me the code.
1.多线程同时写入文件
class Program
{
static int LogCount = ;
static int WritedCount = ;
static int FailedCount = ; static void Main(string[] args)
{
//迭代运行写入日志记录,由于多个线程同时写入同一个文件将会导致错误
Parallel.For(, LogCount, e =>
{
WriteLog();
}); Console.WriteLine(string.Format("\r\nLog Count:{0}.\t\tWrited Count:{1}.\tFailed Count:{2}.", LogCount.ToString(), WritedCount.ToString(), FailedCount.ToString()));
Console.Read();
} static void WriteLog()
{
try
{
var logFilePath = "log.txt";
var now = DateTime.Now;
var logContent = string.Format("Tid: {0}{1} {2}.{3}\r\n", Thread.CurrentThread.ManagedThreadId.ToString().PadRight(), now.ToLongDateString(), now.ToLongTimeString(), now.Millisecond.ToString());
File.AppendAllText(logFilePath, logContent);
WritedCount++;
}
catch (Exception ex)
{
FailedCount++;
Console.WriteLine(ex.Message);
}
}
}
运行结果:

不使用读写锁,只有部分日志成功写入了日志文件。
2.多线程使用读写锁同步写入文件
class Program
{
static int LogCount = ;
static int WritedCount = ;
static int FailedCount = ; static void Main(string[] args)
{
//迭代运行写入日志记录
Parallel.For(, LogCount, e =>
{
WriteLog();
}); Console.WriteLine(string.Format("\r\nLog Count:{0}.\t\tWrited Count:{1}.\tFailed Count:{2}.", LogCount.ToString(), WritedCount.ToString(), FailedCount.ToString()));
Console.Read();
} //读写锁,当资源处于写入模式时,其他线程写入需要等待本次写入结束之后才能继续写入
static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim();
static void WriteLog()
{
try
{
//设置读写锁为写入模式独占资源,其他写入请求需要等待本次写入结束之后才能继续写入
//注意:长时间持有读线程锁或写线程锁会使其他线程发生饥饿 (starve)。 为了得到最好的性能,需要考虑重新构造应用程序以将写访问的持续时间减少到最小。
// 从性能方面考虑,请求进入写入模式应该紧跟文件操作之前,在此处进入写入模式仅是为了降低代码复杂度
// 因进入与退出写入模式应在同一个try finally语句块内,所以在请求进入写入模式之前不能触发异常,否则释放次数大于请求次数将会触发异常
LogWriteLock.EnterWriteLock(); var logFilePath = "log.txt";
var now = DateTime.Now;
var logContent = string.Format("Tid: {0}{1} {2}.{3}\r\n", Thread.CurrentThread.ManagedThreadId.ToString().PadRight(), now.ToLongDateString(), now.ToLongTimeString(), now.Millisecond.ToString()); File.AppendAllText(logFilePath, logContent);
WritedCount++;
}
catch (Exception)
{
FailedCount++;
}
finally
{
//退出写入模式,释放资源占用
//注意:一次请求对应一次释放
// 若释放次数大于请求次数将会触发异常[写入锁定未经保持即被释放]
// 若请求处理完成后未释放将会触发异常[此模式不下允许以递归方式获取写入锁定]
LogWriteLock.ExitWriteLock();
}
}
}
运行结果:

使用读写锁,全部日志成功写入了日志文件。
3.测试复杂多线程环境下使用读写锁同步写入文件
class Program
{
static int LogCount = ;
static int SumLogCount = ;
static int WritedCount = ;
static int FailedCount = ; static void Main(string[] args)
{
//往线程池里添加一个任务,迭代写入N个日志
SumLogCount += LogCount;
ThreadPool.QueueUserWorkItem((obj) =>
{
Parallel.For(, LogCount, e =>
{
WriteLog();
});
}); //在新的线程里,添加N个写入日志的任务到线程池
SumLogCount += LogCount;
var thread1 = new Thread(() =>
{
Parallel.For(, LogCount, e =>
{
ThreadPool.QueueUserWorkItem((subObj) =>
{
WriteLog();
});
});
});
thread1.IsBackground = false;
thread1.Start(); //添加N个写入日志的任务到线程池
SumLogCount += LogCount;
Parallel.For(, LogCount, e =>
{
ThreadPool.QueueUserWorkItem((obj) =>
{
WriteLog();
});
}); //在新的线程里,迭代写入N个日志
SumLogCount += LogCount;
var thread2 = new Thread(() =>
{
Parallel.For(, LogCount, e =>
{
WriteLog();
});
});
thread2.IsBackground = false;
thread2.Start(); //在当前线程里,迭代写入N个日志
SumLogCount += LogCount;
Parallel.For(, LogCount, e =>
{
WriteLog();
}); Console.WriteLine("Main Thread Processed.\r\n");
while (true)
{
Console.WriteLine(string.Format("Sum Log Count:{0}.\t\tWrited Count:{1}.\tFailed Count:{2}.", SumLogCount.ToString(), WritedCount.ToString(), FailedCount.ToString()));
Console.ReadLine();
}
} //读写锁,当资源处于写入模式时,其他线程写入需要等待本次写入结束之后才能继续写入
static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim();
static void WriteLog()
{
try
{
//设置读写锁为写入模式独占资源,其他写入请求需要等待本次写入结束之后才能继续写入
//注意:长时间持有读线程锁或写线程锁会使其他线程发生饥饿 (starve)。 为了得到最好的性能,需要考虑重新构造应用程序以将写访问的持续时间减少到最小。
// 从性能方面考虑,请求进入写入模式应该紧跟文件操作之前,在此处进入写入模式仅是为了降低代码复杂度
// 因进入与退出写入模式应在同一个try finally语句块内,所以在请求进入写入模式之前不能触发异常,否则释放次数大于请求次数将会触发异常
LogWriteLock.EnterWriteLock(); var logFilePath = "log.txt";
var now = DateTime.Now;
var logContent = string.Format("Tid: {0}{1} {2}.{3}\r\n", Thread.CurrentThread.ManagedThreadId.ToString().PadRight(), now.ToLongDateString(), now.ToLongTimeString(), now.Millisecond.ToString()); File.AppendAllText(logFilePath, logContent);
WritedCount++;
}
catch (Exception)
{
FailedCount++;
}
finally
{
//退出写入模式,释放资源占用
//注意:一次请求对应一次释放
// 若释放次数大于请求次数将会触发异常[写入锁定未经保持即被释放]
// 若请求处理完成后未释放将会触发异常[此模式不下允许以递归方式获取写入锁定]
LogWriteLock.ExitWriteLock();
}
}
}
运行结果:

部分日志文件内容:
...
Tid: 2016年12月11日 ::22.825
Tid: 2016年12月11日 ::22.830
Tid: 2016年12月11日 ::22.838
Tid: 2016年12月11日 ::22.845
Tid: 2016年12月11日 ::22.854
Tid: 2016年12月11日 ::22.863
Tid: 2016年12月11日 ::22.872
Tid: 2016年12月11日 ::22.877
Tid: 2016年12月11日 ::22.886
Tid: 2016年12月11日 ::22.892
Tid: 2016年12月11日 ::22.898
Tid: 2016年12月11日 ::22.904
Tid: 2016年12月11日 ::22.909
Tid: 2016年12月11日 ::22.915
Tid: 2016年12月11日 ::22.920
Tid: 2016年12月11日 ::22.925
Tid: 2016年12月11日 ::22.931
Tid: 2016年12月11日 ::22.937
Tid: 2016年12月11日 ::22.942
Tid: 2016年12月11日 ::22.947
Tid: 2016年12月11日 ::22.953
Tid: 2016年12月11日 ::22.958
Tid: 2016年12月11日 ::22.964
Tid: 2016年12月11日 ::22.970
Tid: 2016年12月11日 ::22.975
Tid: 2016年12月11日 ::22.980
Tid: 2016年12月11日 ::22.985
Tid: 2016年12月11日 ::22.991
Tid: 2016年12月11日 ::22.997
Tid: 2016年12月11日 ::23.3
Tid: 2016年12月11日 ::23.9
Tid: 2016年12月11日 ::23.14
Tid: 2016年12月11日 ::23.20
Tid: 2016年12月11日 ::23.27
Tid: 2016年12月11日 ::23.33
Tid: 2016年12月11日 ::23.38
Tid: 2016年12月11日 ::23.44
Tid: 2016年12月11日 ::23.49
Tid: 2016年12月11日 ::23.57
Tid: 2016年12月11日 ::23.63
Tid: 2016年12月11日 ::23.68
Tid: 2016年12月11日 ::23.74
Tid: 2016年12月11日 ::23.80
Tid: 2016年12月11日 ::23.86
Tid: 2016年12月11日 ::23.93
Tid: 2016年12月11日 ::23.99
Tid: 2016年12月11日 ::23.105
Tid: 2016年12月11日 ::23.110
Tid: 2016年12月11日 ::23.116
Tid: 2016年12月11日 ::23.122
Tid: 2016年12月11日 ::23.128
Tid: 2016年12月11日 ::23.134
Tid: 2016年12月11日 ::23.139
Tid: 2016年12月11日 ::23.146
Tid: 2016年12月11日 ::23.152
Tid: 2016年12月11日 ::23.158
Tid: 2016年12月11日 ::23.164
Tid: 2016年12月11日 ::23.170
Tid: 2016年12月11日 ::23.176
Tid: 2016年12月11日 ::23.182
Tid: 2016年12月11日 ::23.189
Tid: 2016年12月11日 ::23.194
Tid: 2016年12月11日 ::23.202
Tid: 2016年12月11日 ::23.208
Tid: 2016年12月11日 ::23.215
Tid: 2016年12月11日 ::23.221
复杂多线程环境下使用读写锁,全部日志成功写入了日志文件,由ThreadId和DateTime可以看出是由不同的线程同步写入。
本文旨在提供简单的方式解决文件并发写入的问题,需要深入了解线程同步及高性能文件读写相关知识,请参阅其他相关资料。
C#使用读写锁三行代码简单解决多线程并发写入文件时线程同步的问题的更多相关文章
- C# 防止同时调用=========使用读写锁三行代码简单解决多线程并发的问题
http://www.jb51.net/article/99718.htm 本文主要介绍了C#使用读写锁三行代码简单解决多线程并发写入文件时提示"文件正在由另一进程使用,因此该进程无 ...
- C#使用读写锁解决多线程并发写入文件时线程同步的问题
读写锁是以 ReaderWriterLockSlim 对象作为锁管理资源的,不同的 ReaderWriterLockSlim 对象中锁定同一个文件也会被视为不同的锁进行管理,这种差异可能会再次导致文件 ...
- 用读写锁三句代码解决多线程并发写入文件 z
C#使用读写锁三句代码简单解决多线程并发写入文件时提示“文件正在由另一进程使用,因此该进程无法访问此文件”的问题 在开发程序的过程中,难免少不了写入错误日志这个关键功能.实现这个功能,可以选择使用第三 ...
- sublime python3中读取和写入文件时如何解决编码问题
# -*- coding: utf-8 -*- #分析用户身份审核信息 #python 3.5 #xiaodeng #http://apistore.baidu.com/apiworks/servic ...
- 解决pyinstaller在单一文件时无法正确添加权限清单问题,(UAC,uac_admin,manifest,asInvoker,python,requireAdministrator)
做了3天的win10的兼容性测试,大部分时间都卡权限获取这了. 以下废话很多,想直接找解决方法,请跳至红字 首先,简单说下uac,自vista后windows再次加严了权限管理,uac (账户控制) ...
- C# 多线程学习(五)线程同步和冲突解决
from:https://blog.csdn.net/codedoctor/article/details/74358257 首先先说一个线程不同步的例子吧,以下为售票员的模拟售票,多个售票员出售10 ...
- 解决Protege打开owl文件时程序卡死问题
Protege在打开本地owl文件时,程序卡死,而且在终端或是命令行中也没有报错.这是因为存放该本体的文件夹下面有很多其他的文件,只需要创建一个新的文件夹并把owl文件放入其中就可以解决该问题.
- JAVA线程锁-读写锁应用,简单的缓存系统
在JAVA1.5版本以后,JAVA API中提供了ReadWriteLock,此类是一个接口,在它的实现类中ReentrantReadWriteLock中有这样一段代码 class CachedDat ...
- Fedora 20中解决zip解压文件时中文文件名的乱码问题[已解决]
该方法的原文地址: http://wangqige.com/the-solution-of-unzip-files-which-zip-under-windows/(链接已失效) 解决方法:保存如下P ...
随机推荐
- hadoop研究:mapreduce研究前的准备工作
继续研究hadoop,有童鞋问我,为啥不接着写hive的文章了,原因主要是时间不够,我对hive的研究基本结束,现在主要是hdfs和mapreduce,能写文章的时间也不多,只有周末才有时间写文章,所 ...
- 远程调试Java程序
java 可以以调试模式启动,启动后可以使用 jdb 对程序进行调试.以调试模式启动,需要加入以下参数: java -Xdebug -Xrunjdwp:transport=dt_socket,addr ...
- AngularJS快速入门指南15:API
thead>tr>th, table.reference>tbody>tr>th, table.reference>tfoot>tr>th, table ...
- 前端chrome浏览器调试总结
引言 "工欲善其事,必先利其器" 恩,这句话我觉得说的特别有道理,举个例子来说吧,厉害的化妆师都有一套非常专业的刷子,散粉刷负责定妆,眼影刷负责打眼影,各司其职,有了专业的工具才能 ...
- Bootstrap~日期控制
回到目录 一个成熟的框架,日期控制是少不了的,在网上也有很多日期控制可以选择,而主框架用了bootstrap,日期控制也当前要用它自己的, 控件地址:http://www.bootcss.com/p/ ...
- 2013 duilib入门简明教程 -- 总结 (20)
duilib的入门系列就到尾声了,再次提醒下,Alberl用的duilib版本是SVN上第个版本,时间是2013.08.15~ 这里给出Alberl最后汇总的一个工程,戳我下载,效 ...
- Atitit js中的依赖注入di ioc的实现
Atitit js中的依赖注入di ioc的实现 全类名(FQCN)为标识符1 混合请求模式1 使用类内 builder 即可..2 Service locator method走ok拦2 Jav ...
- Atitti 文本分类 以及 垃圾邮件 判断原理 以及贝叶斯算法的应用解决方案
Atitti 文本分类 以及 垃圾邮件 判断原理 以及贝叶斯算法的应用解决方案 1.1. 七.什么是贝叶斯过滤器?1 1.2. 八.建立历史资料库2 1.3. 十.联合概率的计算3 1.4. 十一. ...
- cordova填坑
cordova填坑
- 手把手教你实现一个完整的 Promise
用过 Promise,但是总是有点似懂非懂的感觉,也看过很多文章,还是搞不懂 Promise的 实现原理,后面自己边看文章,边调试代码,终于慢慢的有感觉了,下面就按自己的理解来实现一个 Promise ...