在开发程序的过程中,难免少不了写入错误日志这个关键功能。实现这个功能,可以选择使用第三方日志插件,也可以选择使用数据库,还可以自己写个简单的方法把错误信息记录到日志文件。

选择最后一种方法实现的时候,若对文件操作与线程同步不熟悉,问题就有可能出现了,因为同一个文件并不允许多个线程同时写入,否则会提示“文件正在由另一进程使用,因此该进程无法访问此文件”。

这是文件的并发写入问题,就需要用到线程同步。而微软也给线程同步提供了一些相关的类可以达到这样的目的,本文使用到的 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#使用读写锁三行代码简单解决多线程并发写入文件时线程同步的问题的更多相关文章

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

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

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

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

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

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

  4. sublime python3中读取和写入文件时如何解决编码问题

    # -*- coding: utf-8 -*- #分析用户身份审核信息 #python 3.5 #xiaodeng #http://apistore.baidu.com/apiworks/servic ...

  5. 解决pyinstaller在单一文件时无法正确添加权限清单问题,(UAC,uac_admin,manifest,asInvoker,python,requireAdministrator)

    做了3天的win10的兼容性测试,大部分时间都卡权限获取这了. 以下废话很多,想直接找解决方法,请跳至红字 首先,简单说下uac,自vista后windows再次加严了权限管理,uac (账户控制) ...

  6. C# 多线程学习(五)线程同步和冲突解决

    from:https://blog.csdn.net/codedoctor/article/details/74358257 首先先说一个线程不同步的例子吧,以下为售票员的模拟售票,多个售票员出售10 ...

  7. 解决Protege打开owl文件时程序卡死问题

    Protege在打开本地owl文件时,程序卡死,而且在终端或是命令行中也没有报错.这是因为存放该本体的文件夹下面有很多其他的文件,只需要创建一个新的文件夹并把owl文件放入其中就可以解决该问题.

  8. JAVA线程锁-读写锁应用,简单的缓存系统

    在JAVA1.5版本以后,JAVA API中提供了ReadWriteLock,此类是一个接口,在它的实现类中ReentrantReadWriteLock中有这样一段代码 class CachedDat ...

  9. Fedora 20中解决zip解压文件时中文文件名的乱码问题[已解决]

    该方法的原文地址: http://wangqige.com/the-solution-of-unzip-files-which-zip-under-windows/(链接已失效) 解决方法:保存如下P ...

随机推荐

  1. Python2.6下基于rsa的加密解密

    生成公钥的私钥: # -*- coding: UTF-8 -*- import rsa import base64 (public_key, private_key) = rsa.newkeys(10 ...

  2. java提高篇(二三)-----HashMap

    HashMap也是我们使用非常多的Collection,它是基于哈希表的 Map 接口的实现,以key-value的形式存在.在HashMap中,key-value总是会当做一个整体来处理,系统会根据 ...

  3. 建立 svn 服务端

    上一篇文章 (SVN 使用)是针对于客户端,本文是说明如何在本地设置服务端 1,建立服务端站点 svnadmin create /Users/hour/Desktop/svn 2,终端进入svn 里的 ...

  4. 大叔也说Xamarin~Android篇~原生登陆与WebView的网站如何共享Session

    回到目录 事情是这样的,我们最近开了一个APP,主要使用xamarin做了一个登陆,它与服务器API进行数据通讯,当用户名密码正确去,跳转到新的activity,并在webview控件中打开服务端的H ...

  5. fir.im Weekly - 给女朋友的 iOS 开发教程

    俗话说:技多不压身,功到自然成.本期 fir.im Weekly 收集的热度资源,大部分关于Android.iOS 开发工具和源码,还有一些有关设计的 Tips ,希望对你有帮助. 给女朋友的 iOS ...

  6. js获取当前时间显示在页面上

    <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...

  7. 如何使用ITEXTSHARP将HTML代码字符串写进PDF

    原文 如何使用ITEXTSHARP将HTML代码字符串写进PDF itextsharp包括一个简单的类,可以用来根据html代码或字符串创建pdf文件.使用此类,你可以使用短短几行代码,就将 HTML ...

  8. XML学习笔记1——概述

    我对于XML是很不够重视的,认识也是非常肤浅的,因为在之前的Web经验中,基本上都可以使用JSON来代替XML,JSON网络流量少,解析快,JS支持好等这些特点让我对自己的观点坚信不疑.然而我渐渐地改 ...

  9. 快速入门系列--WCF--04元数据和异常处理

    本章节将进行元数据和异常处理的介绍,这部分内容概念型比较强,可以快速浏览一下就好. 客户端和服务器借助于终结点进行通信,服务的提供者通过一个或者多个终结点将服务发布出来,服务的消费者则通过创建与之匹配 ...

  10. WPF自定义控件与样式(9)-树控件TreeView与菜单Menu-ContextMenu

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要内容: 菜单M ...