.Net 並發寫入文件的多種方式


1、簡介

本文主要演示日常開發中利用多線程寫入文件存在的問題,以及解決方案,本文使用最常用的日志案例!

 

2、使用File.AppendAllText寫入日志

這是種常規的做法,通過File定位到日志文件所在位置,然后寫入相應的日志內容,代碼如下:

        static string _filePath = @"C:\Users\zhengchao\Desktop\測試文件.txt";
        static void Main(string[] args)
        {
            WriteLogAsync();
            Console.ReadKey();
        }

        static void WriteLogAsync()
        {
            var logRequestNum = 100000;//請求寫入日志次數
            var successCount =0;//執行成功次數
            var failCount = 0;//執行失敗次數
            //模擬100000次用戶請求寫入日志操作
            Parallel.For(0, logRequestNum, i =>
            {
                try
                {
                    var now = DateTime.Now;
                    var logContent = $"當前線程Id:{Thread.CurrentThread.ManagedThreadId},日志內容:暫時沒有,日志級別:Warn,寫入時間:{now.ToString()}";
                    File.AppendAllText(_filePath, logContent);
                    successCount++;
                }
                catch (Exception ex)
                {
                    failCount++;
                    Console.WriteLine(ex.Message);
                }
            });
            Console.WriteLine($"Request Count:{logRequestNum}. Success Count:{successCount} Failed Count:{failCount}.");
        }

報錯了,原因,Windows不允許多個線程同時操作同一個文件,所以,拋異常.所以必須解決這個問題。

 

3、利用ReadWriterSlim解決多線程征用文件問題

關於ReadWriterSlim的使用,在本人的這篇隨筆中已介紹,在其基礎上,對SynchronizedCache類稍稍改造,形成一個SynchronizedFile類,對相關操作代碼進行線程安全處理,即能解決當前的問題,代碼如下:

   public class SynchronizedFile
    {
        private static ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();

        /// <summary>
        /// 線程安全的寫入文件操作
        /// </summary>
        /// <param name="action"></param>
        public static void WriteFile(Action action)
        {
            cacheLock.EnterWriteLock();
            try
            {
                action.Invoke();
            }
            finally
            {
                cacheLock.ExitWriteLock();
            }
        }
    }

調用代碼如下所示:

        static string _filePath = @"C:\Users\zhengchao\Desktop\測試文件.txt";
        static void Main(string[] args)
        {
            WriteLogSync();
            Console.ReadKey();
        }

        /// <summary>
        /// 多線程同步寫入文件
        /// </summary>
        static void WriteLogSync()
        {
            var logRequestNum = 10000;//請求寫入日志次數
            var successCount =0;//執行成功次數
            var failCount = 0;//執行失敗次數

            var stopWatch = Stopwatch.StartNew();
            //模擬100000次用戶請求寫入日志操作
            var result=Parallel.For(0, logRequestNum, i =>
            {
                SynchronizedFile.WriteFile(() =>
                {
                    try
                    {
                        var now = DateTime.Now;
                        var logContent = $"當前線程Id:{Thread.CurrentThread.ManagedThreadId},日志內容:暫時沒有,日志級別:Warn,寫入時間:{now.ToString()}\r\n";
                        File.AppendAllText(_filePath, logContent);
                        successCount++;
                    }
                    catch (Exception ex)
                    {
                        failCount++;
                        Console.WriteLine(ex.Message);
                    }
                });

            });
            if (result.IsCompleted)
            {
                stopWatch.Stop();
                Console.WriteLine($"Request Count:{logRequestNum}. Success Count:{successCount} Failed Count:{failCount},總耗時:{stopWatch.ElapsedMilliseconds/1000}秒");
            }
        }

內容全部寫入成功,但是還沒有結束,原因是,反編譯

 

一直反編譯下去,會發現

 

用的是同步Api,所以代碼可以繼續優化,同步意味着每個線程在寫入文件時,當前的寫入托管代碼會轉換成托管代碼,最后,Windows會把當前寫入操作的數據初始化成IRP數據包傳給硬件設備,之后硬件設備開始執行寫入操作。這個過程,當前線程在和硬件交互時,不會返回到線程池,而是被Windows置為休眠狀態,等待硬件設置執行寫入操作完畢后,接着Windows會喚起該線程,最后又回到我的托管代碼也就是C#代碼中,繼續執行下面的邏輯.所以當前的日志寫入代碼可以優化,使用異步Api來做.這樣當前線程不會等待硬件設備,而是返回線程池.提高CPU的利用率.

 

4、優化代碼

        static string _filePath = @"C:\Users\zhengchao\Desktop\測試文件.txt";
        static void Main(string[] args)
        {
            WriteLogAsync();
            Console.ReadKey();
        }

        /// <summary>
        /// 多線程異步寫入文件
        /// </summary>
        static void WriteLogAsync()
        {
            var logRequestNum = 10000;//請求寫入日志次數
            var successCount = 0;//執行成功次數
            var failCount = 0;//執行失敗次數

            var stopWatch = Stopwatch.StartNew();
            //模擬100000次用戶請求寫入日志操作
            var result = Parallel.For(0, logRequestNum, i =>
            {
                SynchronizedFile.WriteFile(() =>
                {
                    try
                    {
                        var now = DateTime.Now;
                        var logContent = $"當前線程Id:{Thread.CurrentThread.ManagedThreadId},日志內容:暫時沒有,日志級別:Warn,寫入時間:{now.ToString()}\r\n";
                        var utf8NoBom = new UTF8Encoding(false, true);//去掉Dom頭
                        using (StreamWriter writer = new StreamWriter(_filePath, true, utf8NoBom))
                        {
                            writer.WriteAsync(logContent);
                        }
                        successCount++;
                    }
                    catch (Exception ex)
                    {
                        failCount++;
                        Console.WriteLine(ex.Message);
                    }
                });

            });
            if (result.IsCompleted)
            {
                stopWatch.Stop();
                Console.WriteLine($"Request Count:{logRequestNum}. Success Count:{successCount} Failed Count:{failCount},總耗時:{stopWatch.ElapsedMilliseconds / 1000}秒");
            }

        }

雖然效果差不多,但是能提升CPU利用率.暫時還沒找到多線程寫入一個文件,不需要加讀鎖的方法,如果有,請告知.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM