【问题标题】:Prevent multiple threads writing the same file防止多个线程写入同一个文件
【发布时间】:2014-04-28 14:16:23
【问题描述】:

我正在设计和开发一个api,其中多个线程从网上下载文件,然后将其写入磁盘。

如果使用不当,可能会发生同一个文件被多个线程下载和写入,这将导致写入磁盘时出现异常。

我想在写入文件的部分周围使用lock() { ... } 来避免这个问题,但显然我不想用全局对象锁定,只是与该特定文件相关的东西,因此并非所有线程都写入文件时锁定。

我希望这个问题可以理解。

【问题讨论】:

  • 使用memory-mapped文件?
  • @Sinatr:你到底是什么意思?
  • 使用异常处理这种类型的竞争条件似乎是合适的。问题是什么?我认为不需要锁。
  • @Bananamansam 这样的全局锁正是我想要避免的。

标签: c# .net multithreading file-io mutex


【解决方案1】:

所以你想要做的是同步一堆没有给定键的动作。在这种情况下,该键可以是绝对文件名。我们可以将其实现为将键映射到某个同步对象的字典。如果我们想实现阻塞同步机制,这可以是一个要锁定的对象,如果我们想在适当的时候表示运行代码的异步方法,则可以是Task;我和后者一起去了。我还使用ConcurrentDictionary 让它处理同步,而不是手动处理,并使用Lazy 确保每个任务只创建一次:

public class KeyedSynchronizer<TKey>
{
    private ConcurrentDictionary<TKey, Lazy<Task>> dictionary;
    public KeyedSynchronizer(IEqualityComparer<TKey> comparer = null)
    {
        dictionary = new ConcurrentDictionary<TKey, Lazy<Task>>(
            comparer ?? EqualityComparer<TKey>.Default);
    }

    public Task ActOnKey(TKey key, Action action)
    {
        var dictionaryValue = dictionary.AddOrUpdate(key,
            new Lazy<Task>(() => Task.Run(action)),
                (_, task) => new Lazy<Task>(() =>
                    task.Value.ContinueWith(t => action())));
        return dictionaryValue.Value;
    }

    public static readonly KeyedSynchronizer<TKey> Default =
        new KeyedSynchronizer<TKey>();
}

您现在可以创建此同步器的实例,然后指定操作以及它们对应的键(文件)。您可以确信在该文件上的任何先前操作完成之前不会执行这些操作。如果您想等到该操作完成,那么您可以在该任务上Wait,如果您不需要等待,那么您可以不用。这也允许您通过等待任务来进行异步处理。

【讨论】:

  • The documentation on ConcurrentDictionary.AddOrUpdate 说:“如果你在不同的线程上同时调用AddOrUpdateaddValueFactory 可能会被多次调用,但它的键/值对可能不会每次调用都添加到字典中”。这意味着在某些极端情况下,两个任务可能同时针对一个键运行。
  • @vivicat 正确。这就是为什么惰性会确保它只运行一次。
  • 但是你在 addValueFactory 内部创造了懒惰。所以这只会产生两个同时执行的独立惰性对象。
  • @vivicat 您正在创建两个惰性对象,是的。但是,由于并发字典的保证,字典实际上只返回了其中一个,因此只有其中一个的值实现了。另一个是惰性的,它被创建并且永远不会运行它的价值函数。
  • 啊,所以在这种情况下,对 addValueFactory 的额外调用会创建一个流浪的惰性项目,但 AddOrUpdate 返回的项目始终是由 other 调用创建的项目并存在在字典里?这是有道理的。
【解决方案2】:

您可以考虑使用 ReaderWriterLockSlim

http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim.aspx

private ReaderWriterLockSlim fileLock = new ReaderWriterLockSlim();


fileLock.EnterWriteLock();
try
{
   //write your file here
}
finally
{
    fileLock.ExitWriteLock();
}

【讨论】:

  • 听起来很有趣:那么第二个线程会自动等待到第一个线程完成吗?
  • 是的。如果你打开链接,第一行显示Represents a lock that is used to manage access to a resource, allowing multiple threads for reading or exclusive access for writing.
  • 如果除了写作之外还有兴趣从线程中阅读,这看起来是一个很好的解决方案。我可以看到一个漂亮的线程安全文件包装类,它使用类和一些流对象来完全抽象安全读写。
  • 是的,fileLock.EnterWriteLock() 不允许其他线程进入。您可以创建一个对象来处理文件并利用ReaderWriterLockSlim 来保护写入。
  • 我在我的代码中快速尝试了一下,但没有成功,所以我可能遗漏了一些东西。我必须在所有线程上使用相同的 ReaderWriterLockSLim 实例吗?我是否还必须用这样的锁来包装所有读取访问?
【解决方案3】:

我遇到了类似的情况,并通过lock()ing 解决了相关问题的StreamWriter 对象:

private Dictionary<string, StreamWriter>  _writers;  // Consider using a thread-safe dictionary

void  WriteContent(string file, string content)
    {
    StreamWriter  writer;
    if (_writers.TryGetValue(file, out writer))
        lock (writer)
            writer.Write(content);
    // Else handle missing writer
    }

这是凭记忆的,它可能无法编译。我会阅读 Andrew 的解决方案(我会的),因为它可能更准确地满足您的需求……但如果您只是想要一个快速而肮脏的东西,这非常简单。

【讨论】:

  • 这假设作者列表在整个过程中是不变的。如果不是,这将不起作用,因为您需要从多个线程将项目添加到字典中,并且它并非旨在支持这一点。
  • 是的。我的假设是 OP 在用户开始下载文件时或在第一次写入操作时创建写入器。如果设计要求偶尔丢弃和重新创建写入器,则有必要锁定一些更持久的对象。
【解决方案4】:

我会用一些解释来回答。

Windows 已经有你想要的一些东西,背后的想法很简单:允许多个进程访问同一个文件并进行所有写入/读取操作,这样:1) 所有进程都使用该文件的最新数据 2) 多次写入或读取无需等待(如果可能)。

它叫做Memory-Mapped Files。我主要用于IPC(没有文件),所以无法提供示例,但应该有some

您可以通过在其上使用一些缓冲区和某种层来模仿 MMF 行为,这会将所有读/写操作重定向到该缓冲区并定期将更新的内容刷新到物理文件中。

P.S:尝试同时寻找文件共享(打开文件以进行共享读/写)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-11-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多