如何在不锁定磁盘文件的情况下读取文本文件?

How to read text file without locking the file on disk?

基本上我有一些像这样的代码将 ASCII 文本文件的内容读入列表:

    List<string> lines = new List<string> ( );
    using ( StreamReader sr = File.OpenText ( textfile ) )
    {
        string s = String.Empty;
        while ( ( s = sr.ReadLine ( ) ) != null )
            lines.Add ( s );
    }

但问题是当另一个线程正在写入文件时,它会抛出异常:

进程无法访问文件 'myfile.txt',因为另一个进程正在使用它。

File.ReadAllLines 也是如此。为什么这些函数将文件锁定在磁盘上或关心文件正在被另一个进程使用?

我只是想定期阅读内容,所以如果这次使用它,那么下次它会阅读更新的内容。我这样做是为了检查是否已添加新条目,因为用户也可以手动添加它们。

我可以使用哪些函数将此文件读入内存而不引发异常,或者我应该在 try/catch.

中使用 运行 这段代码

这是最新的代码:

        var fs = new FileStream ( filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite );
        using ( StreamReader sr = new StreamReader ( fs ) )
        {
            string s = String.Empty;
            while ( ( s = sr.ReadLine ( ) ) != null )
                lines.Add ( s );
        }

修改文件的代码:

public static void RemoveCoinFromBuyOrderLogs ( string symbol )
{
    if ( !walletLocked )
    {
        walletLocked = true;

        string [ ] lines = File.ReadAllLines ( walletFilename );

        var newlines = lines.Where ( c => !c.StartsWith ( symbol + "USDT" ) && !c.StartsWith ( symbol + "BUSD" ) && !c.StartsWith ( symbol + "USDC" ) && !c.StartsWith ( symbol + "TUSD" ) ).Select ( c => c ).ToList ( );
        File.WriteAllLines ( walletFilename, newlines );

        using ( FileStream fs = File.Open ( walletFilename, FileMode.OpenOrCreate ) )
        {
            StreamWriter sw = new StreamWriter ( fs );
            sw.AutoFlush = true;
            newlines.ForEach ( r => sw.WriteLine ( r ) );
        }

        walletLocked = false;
    }
}

public static void AddCoinToOrderLogs ( string newOrder, long orderId )
{
    if ( !walletLocked )
    {
        var lines = Utility.ReadAllLines ( walletFilename );
        lines = lines.Select ( line => line.Replace ( "\r", "" ) ).ToList ( );
        lines = lines.Where ( line => line != "" ).Select ( line => line ).ToList ( );

        var fields = lines.Select ( line => line.Split ( '\t' ) ).ToList ( );

        bool duplicate = false;
        foreach ( var field in fields )
        {
            if ( field.Length >= 5 )
            {
                long id = Convert.ToInt64 ( field [ 4 ] );
                if ( id == orderId )
                    duplicate = true;
            }
        }

        if ( !duplicate )
        {
            lines.Add ( newOrder );
            lines.Sort ( );

            walletLocked = true;
            File.WriteAllLines ( walletFilename, lines );
            walletLocked = false;
        }
    }
}

看看 File.Open()this overload。它允许您指定其他参数以避免锁定。我认为它应该可以解决问题。

例如,您可以 var stream = new FileStream(textfile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);

File.ReadAllLines(StringPath)

此函数打开一个文本文件,读取文件的所有行,然后关闭文件。 如果文件小于 4G 可能会有用。

将结果字符串数组转换为一个列表,然后用它做任何你想做的事。该文件保持免费和解锁状态。 使用 WriteAllLines(path,StringLines) 更新最终结果 最后。

如果真的需要实时做的话也有异步的方法 This article 可能真的很有帮助。功能是

阅读

public async Task SimpleReadAsync()
{
    string filePath = "simple.txt";
    string text = await File.ReadAllTextAsync(filePath);

    Console.WriteLine(text);
}

用于写作

    public async Task SimpleWriteAsync()
    {
        string filePath = "simple.txt";
        string text = $"Hello World";
    await File.WriteAllTextAsync(filePath, text);

}

首先,如果您的应用程序是多线程的,则不应使用 bool 守卫。您应该使用线程同步工具,例如锁、互斥锁、事件 and/or 信号量。

此外,您的阅读正在开放分享,但您的写作还没有。

您也没有将流包装在 using 块中。这是另一个问题。你永远不应该这样做:

StreamWriter sw = new StreamWriter(fs);

你应该这样做:

using(var sw = new StreamWriter(fs))
{
    // ...
}

实现 Dispose 的对象的基本规则是您应该始终将它们包装在 using 块中。

除此之外,您可能不希望边写边读或边读边写。这会给你带来大量的竞争条件问题,当你需要调试正在发生的事情时,这些问题将很难重现。

由于您没有使用 async/await,我建议您使用锁。这将一次只允许一个线程执行文件操作。没有竞争条件,没有“共享”文件。

private static readonly object _fileLock = new object();

public static void RemoveCoinFromBuyOrderLogs(string symbol)
{
    lock(_fileLock)
    {
        var newlines = File.ReadAllLines(walletFilename)
            .Where(c =>
                !c.StartsWith(symbol + "USDT") &&
                !c.StartsWith(symbol + "BUSD") &&
                !c.StartsWith(symbol + "USDC") &&
                !c.StartsWith(symbol + "TUSD"));

        File.WriteAllLines(walletFilename, newlines);
    }
}

public static void AddCoinToOrderLogs(string newOrder, long orderId)
{
    lock (_fileLock)
    {
        var lines = File.ReadAllLines(walletFilename).ToList();
        lines = lines.Select(line => line.Replace("\r", "")).ToList();
        lines = lines.Where(line => line != "").Select(line => line).ToList();

        var fields = lines.Select(line => line.Split('\t')).ToList();

        bool duplicate = false;
        foreach (var field in fields)
        {
            if (field.Length >= 5)
            {
                long id = Convert.ToInt64(field[4]);
                if (id == orderId)
                    duplicate = true;
            }
        }

        if (!duplicate)
        {
            lines.Add(newOrder);
            lines.Sort();
            File.WriteAllLines(walletFilename, lines);
        }
    }
}

我无法测试此代码,因为我没有要测试的数据,但请尝试让它看起来接近那个。

而且,老实说,我认为您应该使用 SQLite 数据库之类的东西来完成此类工作。使用多个线程操作单个平面文件是一件很难正确有效地完成的事情。

ETA

这是使用 SemaphoreSlim 进行同步的 async/await 模式示例

private static readonly SemaphoreSlim _smph = new SemaphoreSlim(1, 1);

private static async Task<IEnumerable<string>> ReadAllLinesAsync(
    string fileName, bool removeEmptyLines = true)
{
    using (var s = File.OpenText(fileName))
    {
        var data = await s.ReadToEndAsync().ConfigureAwait(false);
        return await Task.Run(() =>
            data.Split(new[] { Environment.NewLine },
                removeEmptyLines ? StringSplitOptions.RemoveEmptyEntries : StringSplitOptions.None));
    }
}

private static async Task WriteAllLinesAsync(string fileName, IEnumerable<string> lines)
{
    using (var s = File.OpenWrite(fileName))
    using (var sr = new StreamWriter(s))
    {
        var data = await Task.Run(() => 
            string.Join(Environment.NewLine, lines)).ConfigureAwait(false);
        await sr.WriteAsync(data);
    }
}

public static async Task RemoveCoinFromBuyOrderLogsAsync(string symbol)
{
    await _smph.WaitAsync().ConfigureAwait(false);
    try
    {
        var lines = await ReadAllLinesAsync(walletFilename);
        lines = lines.Where(c =>
                !c.StartsWith(symbol + "USDT") &&
                !c.StartsWith(symbol + "BUSD") &&
                !c.StartsWith(symbol + "USDC") &&
                !c.StartsWith(symbol + "TUSD"));
        await WriteAllLinesAsync(walletFilename, lines);
    }
    finally
    {
        _smph.Release();
    }
}

public static async Task AddCoinToOrderLogsAsync(string newOrder, long orderId)
{
    await _smph.WaitAsync().ConfigureAwait(false);
    try
    {
        var lines = await ReadAllLinesAsync(walletFilename);

        var duplicate = lines.Select(line => line.Split('\t'))
                .Any(x => (x.Length >= 5) && Convert.ToInt64(x[4]) == orderId);

        if (!duplicate)
        {
            var newLines = await Task.Run(() =>
            {
                var newList = lines.ToList();
                newList.Add(newOrder);
                newList.Sort();
                return newList;
            });

            await WriteAllLinesAsync(walletFilename, newLines);
        }
    }
    finally
    {
        _smph.Release();
    }
}

我在我认为 可能 是 CPU 密集操作的部分添加了 Task.Run