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

来源:互联网 发布:2016淘宝严打刷单 编辑:程序博客网 时间:2024/06/08 15:46

(补充:初始化FileStream时使用包含文件共享属性(System.IO.FileShare)的构造函数比使用自定义线程锁更为安全和高效,更多内容可点击参阅

 

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

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

 

这是文件的并发写入问题,就需要用到线程同步。而微软也给线程同步提供了一些相关的类可以达到这样的目的,本文使用到的 System.Threading.ReaderWriterLockSlim 便是其中之一。

该类用于管理资源访问的锁定状态,可实现多线程读取或进行独占式写入访问。利用这个类,我们就可以避免在同一时间段内多线程同时写入一个文件而导致的并发写入问题

 

读写锁是以 ReaderWriterLockSlim 对象作为锁管理资源的,不同的 ReaderWriterLockSlim 对象中锁定同一个文件也会被视为不同的锁进行管理,这种差异可能会再次导致文件的并发写入问题,所以 ReaderWriterLockSlim 应尽量定义为只读的静态对象

 

ReaderWriterLockSlim 有几个关键的方法,本文仅讨论写入锁:

调用 EnterWriteLock 方法 进入写入状态,在调用线程进入锁定状态之前一直处于阻塞状态,因此可能永远都不返回
调用 TryEnterWriteLock 方法 进入写入状态,可指定阻塞的间隔时间,如果调用线程在此间隔期间并未进入写入模式,将返回false
调用 ExitWriteLock 方法 退出写入状态,应使用 finally 块执行 ExitWriteLock 方法,从而确保调用方退出写入模式

 

Don't talk, show me the code.

 

1.多线程同时写入文件

复制代码
 1     class Program 2     { 3         static int LogCount = 100; 4         static int WritedCount = 0; 5         static int FailedCount = 0; 6  7         static void Main(string[] args) 8         { 9             //迭代运行写入日志记录,由于多个线程同时写入同一个文件将会导致错误10             Parallel.For(0, LogCount, e =>11             {12                 WriteLog();13             });14 15             Console.WriteLine(string.Format("\r\nLog Count:{0}.\t\tWrited Count:{1}.\tFailed Count:{2}.", LogCount.ToString(), WritedCount.ToString(), FailedCount.ToString()));16             Console.Read();17         }18 19         static void WriteLog()20         {21             try22             {23                 var logFilePath = "log.txt";24                 var now = DateTime.Now;25                 var logContent = string.Format("Tid: {0}{1} {2}.{3}\r\n", Thread.CurrentThread.ManagedThreadId.ToString().PadRight(4), now.ToLongDateString(), now.ToLongTimeString(), now.Millisecond.ToString());26                 File.AppendAllText(logFilePath, logContent);27                 WritedCount++;28             }29             catch (Exception ex)30             {31                 FailedCount++;32                 Console.WriteLine(ex.Message);33             }34         }35     }
复制代码

运行结果:


不使用读写锁,只有部分日志成功写入了日志文件。

 

2.多线程使用读写锁同步写入文件

复制代码
 1     class Program 2     { 3         static int LogCount = 100; 4         static int WritedCount = 0; 5         static int FailedCount = 0; 6  7         static void Main(string[] args) 8         { 9             //迭代运行写入日志记录10             Parallel.For(0, LogCount, e =>11             {12                 WriteLog();13             });14 15             Console.WriteLine(string.Format("\r\nLog Count:{0}.\t\tWrited Count:{1}.\tFailed Count:{2}.", LogCount.ToString(), WritedCount.ToString(), FailedCount.ToString()));16             Console.Read();17         }18 19         //读写锁,当资源处于写入模式时,其他线程写入需要等待本次写入结束之后才能继续写入20         static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim();21         static void WriteLog()22         {23             try24             {25                 //设置读写锁为写入模式独占资源,其他写入请求需要等待本次写入结束之后才能继续写入26                 //注意:长时间持有读线程锁或写线程锁会使其他线程发生饥饿 (starve)。 为了得到最好的性能,需要考虑重新构造应用程序以将写访问的持续时间减少到最小。27                 //      从性能方面考虑,请求进入写入模式应该紧跟文件操作之前,在此处进入写入模式仅是为了降低代码复杂度28                 //      因进入与退出写入模式应在同一个try finally语句块内,所以在请求进入写入模式之前不能触发异常,否则释放次数大于请求次数将会触发异常29                 LogWriteLock.EnterWriteLock();30 31                 var logFilePath = "log.txt";32                 var now = DateTime.Now;33                 var logContent = string.Format("Tid: {0}{1} {2}.{3}\r\n", Thread.CurrentThread.ManagedThreadId.ToString().PadRight(4), now.ToLongDateString(), now.ToLongTimeString(), now.Millisecond.ToString());34 35                 File.AppendAllText(logFilePath, logContent);36                 WritedCount++;37             }38             catch (Exception)39             {40                 FailedCount++;41             }42             finally43             {44                 //退出写入模式,释放资源占用45                 //注意:一次请求对应一次释放46                 //      若释放次数大于请求次数将会触发异常[写入锁定未经保持即被释放]47                 //      若请求处理完成后未释放将会触发异常[此模式不下允许以递归方式获取写入锁定]48                 LogWriteLock.ExitWriteLock();49             }50         }51     }
复制代码

 

运行结果:


使用读写锁,全部日志成功写入了日志文件。

 

3.测试复杂多线程环境下使用读写锁同步写入文件

复制代码
  1     class Program  2     {  3         static int LogCount = 1000;  4         static int SumLogCount = 0;  5         static int WritedCount = 0;  6         static int FailedCount = 0;  7   8         static void Main(string[] args)  9         { 10             //往线程池里添加一个任务,迭代写入N个日志 11             SumLogCount += LogCount; 12             ThreadPool.QueueUserWorkItem((obj) => 13             { 14                 Parallel.For(0, LogCount, e => 15                 { 16                     WriteLog(); 17                 }); 18             }); 19  20             //在新的线程里,添加N个写入日志的任务到线程池 21             SumLogCount += LogCount; 22             var thread1 = new Thread(() => 23             { 24                 Parallel.For(0, LogCount, e => 25                 { 26                     ThreadPool.QueueUserWorkItem((subObj) => 27                     { 28                         WriteLog(); 29                     }); 30                 }); 31             }); 32             thread1.IsBackground = false; 33             thread1.Start(); 34  35             //添加N个写入日志的任务到线程池 36             SumLogCount += LogCount; 37             Parallel.For(0, LogCount, e => 38             { 39                 ThreadPool.QueueUserWorkItem((obj) => 40                 { 41                     WriteLog(); 42                 }); 43             }); 44  45             //在新的线程里,迭代写入N个日志 46             SumLogCount += LogCount; 47             var thread2 = new Thread(() => 48             { 49                 Parallel.For(0, LogCount, e => 50                 { 51                     WriteLog(); 52                 }); 53             }); 54             thread2.IsBackground = false; 55             thread2.Start(); 56  57             //在当前线程里,迭代写入N个日志 58             SumLogCount += LogCount; 59             Parallel.For(0, LogCount, e => 60             { 61                 WriteLog(); 62             }); 63  64             Console.WriteLine("Main Thread Processed.\r\n"); 65             while (true) 66             { 67                 Console.WriteLine(string.Format("Sum Log Count:{0}.\t\tWrited Count:{1}.\tFailed Count:{2}.", SumLogCount.ToString(), WritedCount.ToString(), FailedCount.ToString())); 68                 Console.ReadLine(); 69             } 70         } 71  72         //读写锁,当资源处于写入模式时,其他线程写入需要等待本次写入结束之后才能继续写入 73         static ReaderWriterLockSlim LogWriteLock = new ReaderWriterLockSlim(); 74         static void WriteLog() 75         { 76             try 77             { 78                 //设置读写锁为写入模式独占资源,其他写入请求需要等待本次写入结束之后才能继续写入 79                 //注意:长时间持有读线程锁或写线程锁会使其他线程发生饥饿 (starve)。 为了得到最好的性能,需要考虑重新构造应用程序以将写访问的持续时间减少到最小。 80                 //      从性能方面考虑,请求进入写入模式应该紧跟文件操作之前,在此处进入写入模式仅是为了降低代码复杂度 81                 //      因进入与退出写入模式应在同一个try finally语句块内,所以在请求进入写入模式之前不能触发异常,否则释放次数大于请求次数将会触发异常 82                 LogWriteLock.EnterWriteLock(); 83  84                 var logFilePath = "log.txt"; 85                 var now = DateTime.Now; 86                 var logContent = string.Format("Tid: {0}{1} {2}.{3}\r\n", Thread.CurrentThread.ManagedThreadId.ToString().PadRight(4), now.ToLongDateString(), now.ToLongTimeString(), now.Millisecond.ToString()); 87  88                 File.AppendAllText(logFilePath, logContent); 89                 WritedCount++; 90             } 91             catch (Exception) 92             { 93                 FailedCount++; 94             } 95             finally 96             { 97                 //退出写入模式,释放资源占用 98                 //注意:一次请求对应一次释放 99                 //      若释放次数大于请求次数将会触发异常[写入锁定未经保持即被释放]100                 //      若请求处理完成后未释放将会触发异常[此模式不下允许以递归方式获取写入锁定]101                 LogWriteLock.ExitWriteLock();102             }103         }104     }
复制代码

运行结果:

部分日志文件内容:

复制代码
 1 ... 2 Tid: 36  2016年12月11日 15:29:22.825 3 Tid: 29  2016年12月11日 15:29:22.830 4 Tid: 6   2016年12月11日 15:29:22.838 5 Tid: 26  2016年12月11日 15:29:22.845 6 Tid: 34  2016年12月11日 15:29:22.854 7 Tid: 24  2016年12月11日 15:29:22.863 8 Tid: 27  2016年12月11日 15:29:22.872 9 Tid: 14  2016年12月11日 15:29:22.87710 Tid: 23  2016年12月11日 15:29:22.88611 Tid: 20  2016年12月11日 15:29:22.89212 Tid: 30  2016年12月11日 15:29:22.89813 Tid: 9   2016年12月11日 15:29:22.90414 Tid: 21  2016年12月11日 15:29:22.90915 Tid: 22  2016年12月11日 15:29:22.91516 Tid: 7   2016年12月11日 15:29:22.92017 Tid: 3   2016年12月11日 15:29:22.92518 Tid: 12  2016年12月11日 15:29:22.93119 Tid: 5   2016年12月11日 15:29:22.93720 Tid: 13  2016年12月11日 15:29:22.94221 Tid: 11  2016年12月11日 15:29:22.94722 Tid: 19  2016年12月11日 15:29:22.95323 Tid: 37  2016年12月11日 15:29:22.95824 Tid: 37  2016年12月11日 15:29:22.96425 Tid: 40  2016年12月11日 15:29:22.97026 Tid: 40  2016年12月11日 15:29:22.97527 Tid: 40  2016年12月11日 15:29:22.98028 Tid: 40  2016年12月11日 15:29:22.98529 Tid: 40  2016年12月11日 15:29:22.99130 Tid: 40  2016年12月11日 15:29:22.99731 Tid: 31  2016年12月11日 15:29:23.332 Tid: 31  2016年12月11日 15:29:23.933 Tid: 31  2016年12月11日 15:29:23.1434 Tid: 31  2016年12月11日 15:29:23.2035 Tid: 31  2016年12月11日 15:29:23.2736 Tid: 31  2016年12月11日 15:29:23.3337 Tid: 31  2016年12月11日 15:29:23.3838 Tid: 31  2016年12月11日 15:29:23.4439 Tid: 31  2016年12月11日 15:29:23.4940 Tid: 31  2016年12月11日 15:29:23.5741 Tid: 31  2016年12月11日 15:29:23.6342 Tid: 31  2016年12月11日 15:29:23.6843 Tid: 31  2016年12月11日 15:29:23.7444 Tid: 16  2016年12月11日 15:29:23.8045 Tid: 16  2016年12月11日 15:29:23.8646 Tid: 16  2016年12月11日 15:29:23.9347 Tid: 16  2016年12月11日 15:29:23.9948 Tid: 16  2016年12月11日 15:29:23.10549 Tid: 16  2016年12月11日 15:29:23.11050 Tid: 16  2016年12月11日 15:29:23.11651 Tid: 38  2016年12月11日 15:29:23.12252 Tid: 38  2016年12月11日 15:29:23.12853 Tid: 28  2016年12月11日 15:29:23.13454 Tid: 19  2016年12月11日 15:29:23.13955 Tid: 25  2016年12月11日 15:29:23.14656 Tid: 37  2016年12月11日 15:29:23.15257 Tid: 39  2016年12月11日 15:29:23.15858 Tid: 32  2016年12月11日 15:29:23.16459 Tid: 33  2016年12月11日 15:29:23.17060 Tid: 31  2016年12月11日 15:29:23.17661 Tid: 35  2016年12月11日 15:29:23.18262 Tid: 40  2016年12月11日 15:29:23.18963 Tid: 15  2016年12月11日 15:29:23.19464 Tid: 18  2016年12月11日 15:29:23.20265 Tid: 17  2016年12月11日 15:29:23.20866 Tid: 10  2016年12月11日 15:29:23.21567 Tid: 16  2016年12月11日 15:29:23.221
复制代码

复杂多线程环境下使用读写锁,全部日志成功写入了日志文件,由ThreadId和DateTime可以看出是由不同的线程同步写入。

 

4.补充:初始化FileStream时使用包含文件共享属性(System.IO.FileShare)的构造函数比使用自定义线程锁更为安全和高效

复制代码
 1     class Program 2     { 3         static int LogCount = 100; 4         static int WritedCount = 0; 5         static int FailedCount = 0; 6  7         static void Main(string[] args) 8         { 9             //迭代运行写入日志记录10             Parallel.For(0, LogCount, e =>11             {12                 WriteLog();13             });14 15             Console.WriteLine(string.Format("\r\nLog Count:{0}.\t\tWrited Count:{1}.\tFailed Count:{2}.", LogCount.ToString(), WritedCount.ToString(), FailedCount.ToString()));16             Console.Read();17         }18 19         static void WriteLog()20         {21             try22             {23                 var logFilePath = "log.txt";24                 var now = DateTime.Now;25                 var logContent = string.Format("Tid: {0}{1} {2}.{3}\r\n", Thread.CurrentThread.ManagedThreadId.ToString().PadRight(4), now.ToLongDateString(), now.ToLongTimeString(), now.Millisecond.ToString());26 27                 var logContentBytes = Encoding.Default.GetBytes(logContent);28                 //由于设置了文件共享模式为允许随后写入,所以即使多个线程同时写入文件,也会等待之前的线程写入结束之后再执行,而不会出现错误29                 using (FileStream logFile = new FileStream(logFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write))30                 {31                     logFile.Seek(0, SeekOrigin.End);32                     logFile.Write(logContentBytes, 0, logContentBytes.Length);33                 }34 35                 WritedCount++;36             }37             catch (Exception ex)38             {39                 FailedCount++;40                 Console.WriteLine(ex.Message);41             }42         }43     }
复制代码
阅读全文
0 0