使用C#从多个线程异步写入文件

49

这是我的情况。我希望在我的应用程序中尽可能高效地向文件系统写入。该应用程序是多线程的,每个线程都可能写入同一文件。有没有办法让每个线程异步地写入文件,而不会使得不同线程之间发生冲突呢?

我正在使用C#和.NET 3.5,并且已安装了反应式扩展。

7个回答

31

对于那些喜欢编程的人,我正在使用以下代码从Web应用程序进行远程记录...

public static class LoggingExtensions
{
    static ReaderWriterLock locker = new ReaderWriterLock();
    public static void WriteDebug(this string text)
    {
        try
        {
            locker.AcquireWriterLock(int.MaxValue); //You might wanna change timeout value 
            System.IO.File.AppendAllLines(Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase).Replace("file:\\", ""), "debug.txt"), new[] { text });
        }
        finally
        {
            locker.ReleaseWriterLock();
        }
    }
}

希望这可以节省您一些时间


非常好!如果您想将此方法作为字符串的扩展方法,方法签名中缺少一件事。我建议在第一个参数前加上“this string text”,以便进行修饰。 - Jason Foglia
4
问题指定为"异步"。但这里全部是同步的。 - Servy
4
@Servy 我在三个不同的线程中使用了这个方法,所有的文本都被写入了文件。无论是同步还是异步,这个解决方案对我来说都有效,并且没有丢失数据。另外,请原谅我如果我错了,但是 IO 不是只能同步吗?异步的解决方案是在 IO 被锁定时收集字符串,一旦解锁后将集合写入文件,以期保持顺序。 - Jason Foglia
@JasonFoglia 我并没有说它不会写入文件,我说的是它会同步地进行,而问题明确指定他们想要异步进行。IO实际上本质上是异步的,而不是同步的。您拥有的任何同步IO操作都涉及本质上是异步操作和一些工作,以显式地使线程休眠,直到该异步操作完成。解决此问题的方法是简单地调用异步文件IO(和同步)操作;就是这样。 - Servy
2
@Servy 我假设...但大多数人都不知道什么...当他们得到解决方案时,通常会感激...为了辩护自己,我可以说是OP先开始的...如果我给你的代码可以从多个线程异步写入文件,你会看到一堆乱码...你知道的...所以关于答案正确性的整个争论有点无意义。是的,我正在回答不正确的问题,但这解决了那些来到这里的人的问题,每个人(除了你)都很高兴,哈哈...不信,发表正确的答案,看看会发生什么... - Matas Vaitkevicius
显示剩余12条评论

15

请查看异步 I/O。这将释放 CPU 继续执行其他任务。
结合ReaderWriterLock,就像 @Jack B Nimble 提到的那样。

如果您指的是希望将文件系统写入尽可能地提高效率,则很难让实际的文件 I/O 更快,因为磁盘速度本身较慢。也许可以考虑使用固态硬盘(SSD)?


5
似乎这并没有解决问题,即“大家一起协商”的问题。 - Hans Passant
是的,如何防止异步写入发生冲突? - Jeffrey Cameron
3
在多线程系统中处理任何资源争用的方式都是相同的:使用锁。ReadWriterLock(或在4.0 / Parallel Extensions + 3.5中使用ReaderWriterLockSlim)允许多个读操作同时发生,因此如果您需要这样做,请使用它。 - µBio
1
当前的异步I/O可以在此处找到:https://msdn.microsoft.com/zh-cn/library/kztecsys(v=vs.110).aspx - MaLiN2223
你如何异步等待ReaderWriteLock? - TigerBear

9
我会做的是有专门的工作线程来写文件。当其他线程需要写出数据时,它应该调用一个函数将数据添加到ArrayList中(或其他容器/类中)。在此函数内部,顶部应有锁定语句以防止多个线程同时执行。添加引用到ArrayList后,它返回并继续完成其任务。处理写入线程的方法有几种。可能最简单的方法是将其放入一个无限循环中,并在末尾使用sleep语句,以便它不会占用您的CPU。另一种方法是使用线程原语,并在没有更多要写出的数据时进入等待状态。这种方法意味着您需要使用ManualResetEvent.Set方法激活线程。
在.NET中,有许多不同的读取和写入文件的方法。我编写了一个基准测试程序,并在我的博客中给出了结果:

http://designingefficientsoftware.wordpress.com/2011/03/03/efficient-file-io-from-csharp

我建议使用Windows的ReadFile和WriteFile方法,如果你需要更好的性能。避免使用任何异步方法,因为我的基准测试结果表明,同步I/O方法可以获得更好的性能。

6

虽然基于线程的锁可以解决这个问题,但是有一种跨线程工作的方式,当您有多个进程写入单个文件末尾时,最好使用它。

要在进程(或线程)之间实现此行为,请在创建操作系统文件句柄时指定您想要原子追加写入。这可以通过在Posix(Linux,Unix)下指定O_APPEND,在Windows下指定FILE_APPEND_DATA来完成。

在C#中,您不直接调用操作系统的“open”或“CreateFile”系统调用,但有方法可以获得此结果。

我以前问过如何在Windows下执行此操作,并在此处获得了两个很好的答案:How can I do an atomic write/append in C#, or how do I get files opened with the FILE_APPEND_DATA flag?

基本上,您可以使用FileStream()或PInvoke,出于明显的原因,我建议使用FileStream()而不是PInvoke。

您可以使用构造函数参数向FileStream()指定异步文件I/O,除了FileSystemRights.AppendData标志外,这应该会给您既有异步I/O又有对文件的原子追加写入。

警告:某些操作系统对以这种方式原子写入的最大字节数有限制,超过该阈值将取消操作系统对原子性的承诺。

由于这个问题的最后一个陷阱,当尝试在单个进程中解决问题时,我建议使用lock()样式的争用管理。


5

1

使用队列和多线程将日志保存 (已在 .Net Core 2.2 Linux 中进行了测试)

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Text.RegularExpressions;
// add
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.Net.Security;
using System.Security.Authentication;
using System.IO;
using System.Timers;

namespace LogToFile
{
    class Program
    {
        public static Logger logger = new Logger("debug.log");

        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            logger.add("[001][LOGGER STARTING]");

            Thread t0 = new Thread(() => DoWork("t0"));
            t0.Start();

            Thread t1 = new Thread(() => DoWork("t1"));
            t1.Start();

            Thread t2 = new Thread(() => DoWork("t2"));
            t2.Start();

            Thread ts = new Thread(() => SaveWork());
            ts.Start();
        }

        public static void DoWork(string nr){
            while(true){
                logger.add("Hello from worker .... number " + nr);
                Thread.Sleep(300);
            }
        }

        public static void SaveWork(){
            while(true){
                logger.saveNow();
                Thread.Sleep(50);
            }
        }
    }

    class Logger
    {
        // Queue import: 
        // using System.Collections
        public Queue logs = new Queue();
        public string path = "debug.log";

        public Logger(string path){
            this.path = path;
        }

        public void add(string t){
            this.logs.Enqueue("[" + currTime() +"] " + t);
        }

        public void saveNow(){
            if(this.logs.Count > 0){
                // Get from queue
                string err = (string) this.logs.Dequeue();
                // Save to logs
                saveToFile(err, this.path);
            }
        }

        public bool saveToFile(string text, string path)
        {
            try{
                // string docPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
                // text = text + Environment.NewLine;
                using (StreamWriter sw = File.AppendText(path))
                {
                    sw.WriteLine(text);
                    sw.Close();
                }
            }catch(Exception e){
                // return to queue
                this.logs.Enqueue(text + "[SAVE_ERR]");
                return false;
            }
            return true;
        }

        public String currTime(){
            DateTime d = DateTime.UtcNow.ToLocalTime();
            return d.ToString("yyyy-MM-dd hh:mm:ss");
        }
    }
}

编译(保存至:LogToFile/Program.cs):

dotnet new console -o LogToFile
cd LogToFile
dotnet build
dotnet run

停止应用程序CTRL+C并查看日志文件

cat debug.log

0

你可以使用事件来记录器:

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;

namespace EventLogger
{
    class Program
    {
        static void Main(string[] args)
        {                   
            // Event handler
            LogData ld = new LogData();
            // Logger
            Logger lo = new Logger();                    
            // Subscribe to event
            ld.MyEvent += lo.OnMyEvent;     
            // Thread loop
            int cnt = 1;
            while(cnt < 5){         
                Thread t = new Thread(() => RunMe(cnt, ld));
                t.Start();
                cnt++;
            }
            Console.WriteLine("While end");
        }

        // Thread worker
        public static void RunMe(int cnt, LogData ld){
            int nr = 0;
            while(true){
                nr++;
                // Add user and fire event
                ld.AddToLog(new User(){Name = "Log to file Thread" + cnt + " Count " + nr, Email = "em@em.xx"});
                Thread.Sleep(1);
            }
        }
    }

    class LogData
    {
        public delegate void MyEventHandler(object o, User u);
        public event MyEventHandler MyEvent;

        protected virtual void OnEvent(User u)
        {
            if(MyEvent != null){
                MyEvent(this, u);
            }

        }

        // Wywołaj
        public void AddToLog(User u){
            Console.WriteLine("Add to log.");

            // Odpal event
            OnEvent(u);

            Console.WriteLine("Added.");
        }
    }

    class User
    {
        public string Name = "";
        public string Email =  "";
    }

    class Logger
    {
        // Catch event
        public void OnMyEvent(object o, User u){
            try{
                Console.WriteLine("Added to file log! " + u.Name + " " + u.Email);
                File.AppendAllText(@"event.log", "Added to file log! " + u.Name + " " + u.Email+"\r\n");
            }catch(Exception e){
                Console.WriteLine("Error file log " + e);
            }
        }
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接