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

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

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

 

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

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

 

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

 

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

调用 EnterWriteLock 方法 进入写入状态,在调用线程进入锁定状态之前一直处于阻塞状态,因此可能永远都不返回
调用 TryEnterWriteLock 方法 进入写入状态,可指定阻塞的间隔时间,如果调用线程在此间隔期间并未进入写入模式,将返回false

调用 ExitWriteLock 方法 退出写入状态,应使用 finally 块执行 ExitWriteLock 方法,从而确保调用方退出写入模式

1.多线程同时写入文件

class Program
    {
        static int LogCount = 100;
        static int WritedCount = 0;
        static int FailedCount = 0;

        static void Main(string[] args)
        {
            //迭代运行写入日志记录,由于多个线程同时写入同一个文件将会导致错误
            Parallel.For(0, 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(4), 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 = 100;
        static int WritedCount = 0;
        static int FailedCount = 0;

        static void Main(string[] args)
        {
            //迭代运行写入日志记录
            Parallel.For(0, 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(4), now.ToLongDateString(), now.ToLongTimeString(), now.Millisecond.ToString());

                File.AppendAllText(logFilePath, logContent);
                WritedCount++;
            }
            catch (Exception)
            {
                FailedCount++;
            }
            finally
            {
                //退出写入模式,释放资源占用
                //注意:一次请求对应一次释放
                //      若释放次数大于请求次数将会触发异常[写入锁定未经保持即被释放]
                //      若请求处理完成后未释放将会触发异常[此模式不下允许以递归方式获取写入锁定]
                LogWriteLock.ExitWriteLock();
            }
        }
    }


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

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

class Program
    {
        static int LogCount = 1000;
        static int SumLogCount = 0;
        static int WritedCount = 0;
        static int FailedCount = 0;

        static void Main(string[] args)
        {
            //往线程池里添加一个任务,迭代写入N个日志
            SumLogCount += LogCount;
            ThreadPool.QueueUserWorkItem((obj) =>
            {
                Parallel.For(0, LogCount, e =>
                {
                    WriteLog();
                });
            });

            //在新的线程里,添加N个写入日志的任务到线程池
            SumLogCount += LogCount;
            var thread1 = new Thread(() =>
            {
                Parallel.For(0, LogCount, e =>
                {
                    ThreadPool.QueueUserWorkItem((subObj) =>
                    {
                        WriteLog();
                    });
                });
            });
            thread1.IsBackground = false;
            thread1.Start();

            //添加N个写入日志的任务到线程池
            SumLogCount += LogCount;
            Parallel.For(0, LogCount, e =>
            {
                ThreadPool.QueueUserWorkItem((obj) =>
                {
                    WriteLog();
                });
            });

            //在新的线程里,迭代写入N个日志
            SumLogCount += LogCount;
            var thread2 = new Thread(() =>
            {
                Parallel.For(0, LogCount, e =>
                {
                    WriteLog();
                });
            });
            thread2.IsBackground = false;
            thread2.Start();

            //在当前线程里,迭代写入N个日志
            SumLogCount += LogCount;
            Parallel.For(0, 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(4), now.ToLongDateString(), now.ToLongTimeString(), now.Millisecond.ToString());

                File.AppendAllText(logFilePath, logContent);
                WritedCount++;
            }
            catch (Exception)
            {
                FailedCount++;
            }
            finally
            {
                //退出写入模式,释放资源占用
                //注意:一次请求对应一次释放
                //      若释放次数大于请求次数将会触发异常[写入锁定未经保持即被释放]
                //      若请求处理完成后未释放将会触发异常[此模式不下允许以递归方式获取写入锁定]
                LogWriteLock.ExitWriteLock();
            }
        }
    }

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


后记:

虽然读写IO有共享模式,也可以实现但不推荐

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(4), now.ToLongDateString(), now.ToLongTimeString(), now.Millisecond.ToString());
        //File.AppendAllText(logFilePath, logContent);
 
        var logContentBytes = Encoding.Default.GetBytes(logContent);
        using (FileStream logFile = new FileStream(logFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Write))
        {
            logFile.Seek(0, SeekOrigin.End);
            logFile.Write(logContentBytes, 0, logContentBytes.Length);
        }
 
        WritedCount++;
    }
    catch (Exception ex)
    {
        FailedCount++;
        Console.WriteLine(ex.Message);
    }
}



  • 1
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个使用C#多线程ping并将结果写入文件的示例代码: ```csharp using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Net.NetworkInformation; using System.Threading; class Program { static void Main(string[] args) { // 创建一个列表,包含要ping的IP地址 List<string> addresses = new List<string> { "www.google.com", "www.baidu.com", "www.microsoft.com", "www.apple.com", "www.amazon.com" }; // 创建一个文件流,用于入ping结果 using (StreamWriter writer = new StreamWriter("ping_results.txt")) { // 创建一个计数器,用于跟踪已完成ping的地址数量 int count = 0; // 遍历IP地址列表 foreach (string address in addresses) { // 创建一个新线程执行ping操作 Thread t = new Thread(() => { try { // 创建一个Ping对象并发送ping请求 Ping pingSender = new Ping(); PingReply reply = pingSender.Send(address); // 将ping结果写入文件 writer.WriteLine("{0}: {1}", address, reply.Status); } catch (Exception ex) { // 发生异常也将结果写入文件 writer.WriteLine("{0}: {1}", address, ex.Message); } finally { // 计数器加1 Interlocked.Increment(ref count); } }); // 启动线程 t.Start(); } // 等待所有线程完成 while (count < addresses.Count) { Thread.Sleep(100); } } Console.WriteLine("Ping完成,结果已写入文件。"); Console.ReadKey(); } } ``` 在此示例中,我们使用了一个`List<string>`对象来存储要ping的IP地址。我们遍历该列表,并为每个地址创建一个新线程执行ping操作。每次ping完成后,我们将结果入一个文件中。 注意,我们在写入文件使用了`StreamWriter`类,它会自动处理文件流的打开和关闭。我们还使用了`Interlocked.Increment`方法来对计数器进行原子操作,以确保线程安全。最后,我们在主线程中等待所有线程完成后输出一个完成消息。 当然,这只是一个简单的示例,你可以根据自己的需求对代码进行修改和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值