安全还是玩火?

Windows服务:从配置文件中的目录列表生成一组FileWatcher对象,并具有以下要求:

  • 文件处理可能非常耗时 - 必须在他们自己的任务线程上处理事件
  • 保持事件处理程序任务的句柄,以等待OnStop()事件完成。
  • 跟踪上传文件的哈希值; 如果没有不同,不要重新处理
  • 保留文件哈希值以允许OnStart()处理服务关闭时上传的文件。
  • 永远不要多次处理文件。
  • (关于#3,当没有变化时,我们确实得到事件......最主要的是因为FileWatchers发生重复事件问题)

    为了做到这些,我有两本字典 - 一本用于上传文件,另一个用于任务本身。 两个对象都是静态的,我需要在添加/删除/更新文件和任务时锁定它们。 简化代码:

    public sealed class TrackingFileSystemWatcher : FileSystemWatcher {
    
        private static readonly object fileWatcherDictionaryLock = new object();
        private static readonly object runningTaskDictionaryLock = new object();
    
        private readonly Dictionary<int, Task> runningTaskDictionary = new Dictionary<int, Task>(15);
        private readonly Dictionary<string, FileSystemWatcherProperties>  fileWatcherDictionary = new Dictionary<string, FileSystemWatcherProperties>();
    
        //  Wired up elsewhere
        private void OnChanged(object sender, FileSystemEventArgs eventArgs) {
            this.ProcessModifiedDatafeed(eventArgs);
        }
    
        private void ProcessModifiedDatafeed(FileSystemEventArgs eventArgs) {
    
            lock (TrackingFileSystemWatcher.fileWatcherDictionaryLock) {
    
                //  Read the file and generate hash here
    
                //  Properties if the file has been processed before
                //  ContainsNonNullKey is an extension method
                if (this.fileWatcherDictionary.ContainsNonNullKey(eventArgs.FullPath)) {
    
                    try {
                        fileProperties = this.fileWatcherDictionary[eventArgs.FullPath];
                    }
                    catch (KeyNotFoundException keyNotFoundException) {}
                    catch (ArgumentNullException argumentNullException) {}
                }
                else {  
                    // Create a new properties object
                }
    
    
                fileProperties.ChangeType = eventArgs.ChangeType;
                fileProperties.FileContentsHash = md5Hash;
                fileProperties.LastEventTimestamp = DateTime.Now;
    
                Task task;
                try {
                    task = new Task(() => new DatafeedUploadHandler().UploadDatafeed(this.legalOrg, datafeedFileData), TaskCreationOptions.LongRunning);
                }
                catch {
                  ..
                }
    
                //  Only lock long enough to add the task to the dictionary
                lock (TrackingFileSystemWatcher.runningTaskDictionaryLock) {
                     try {
                        this.runningTaskDictionary.Add(task.Id, task);  
                    }
                    catch {
                      ..
                    }    
                }
    
    
                try {
                    task.ContinueWith(t => {
                        try {
                            lock (TrackingFileSystemWatcher.runningTaskDictionaryLock) {
                                this.runningTaskDictionary.Remove(t.Id);
                            }
    
                            //  Will this lock burn me?
                            lock (TrackingFileSystemWatcher.fileWatcherDictionaryLock) {
                                //  Persist the file watcher properties to
                                //  disk for recovery at OnStart()
                            }
                        }
                        catch {
                          ..
                        }
                    });
    
                    task.Start();
                }
                catch {
                  ..
                }
    
    
            }
    
        }
    
    }
    

    委托在相同对象的锁定中定义时,在ContinueWith()委托中请求锁定FileSystemWatcher集合的效果如何? 我期望它很好,即使任务开始,完成并在ProcessModifiedDatafeed()释放锁之前输入ContinueWith(),任务线程将暂停,直到创建线程释放锁。 但我想确保我没有踩到任何延迟执行的地雷。

    看看代码,我可能会更快地释放锁,避免出现问题,但我还不确定...需要查看完整的代码以确保安全。


    UPDATE

    为了遏制这种“这段代码太糟糕了”的评论,有很多理由说明我为什么要捕捉我所做的例外,并且抓住了很多这样的例外。 这是一个带有多线程处理程序的Windows服务,它可能不会崩溃。 永远。 如果任何这些线程有未处理的异常,它将执行哪个操作。

    此外,这些例外是为未来防弹编写的。 我在下面的评论中给出的示例将为处理程序添加一个工厂......由于今天编写的代码永远不会有空任务,但如果工厂未正确实现,代码可能会引发异常。 是的,这应该在测试中被发现。 不过,我的团队中有初级开发人员......“五月,不是。崩溃。” (另外,如果存在未处理的异常,则它必须正常关闭,从而允许当前正在运行的线程完成 - 我们使用main()中设置的未处理的异常处理程序完成该操作。 我们将企业级监视器配置为在应用程序错误出现在事件日志中时发送警报 - 这些例外将记录并标记我们。 该方法是一个经过深思熟虑的讨论决定。

    每个可能的例外均经过仔细考虑和选择,以便分为两类 - 适用于单一数据馈送并且不会关闭服务的类别(大多数),以及那些表明清楚编程或其他基本上使代码对所有的数据传输都没有用处。 例如,如果我们无法写入事件日志,我们选择关闭服务,因为这是我们指示数据传输未得到处理的主要机制。 例外情况在当地发生,因为当地环境是唯一可以继续做出决定的地方。 此外,允许例外情况升级到更高级别(1)违反了抽象概念,并且(2)在工作者线程中没有意义。

    我对于反对处理例外的人数感到惊讶。 如果我为每一次try..catch(例外){无所事事}都有一毛钱,我明白,你会在剩下的永恒中得到你的镍币变化。 我会认为死亡1,如果调用.NET框架或自己的代码引发异常,则需要考虑会导致发生异常并明确决定如何处理的场景。 我的代码捕获IO操作中的UnauthorizedExceptions,因为当我考虑如何发生这种情况时,我意识到添加一个新的datafeed目录需要授予服务帐户的权限(默认情况下它不会有这些权限)。

    我很欣赏建设性的意见......只是请不要批评简单的示例代码与广泛的“这个烂”刷。 代码不吸收 - 它是防弹的,必然如此。


    1如果Jon Skeet不同意,我只会争论很长时间


    不,它不会烧你。 即使ContinueWith被内联到当前正在运行new Task(() => new DatafeedUploadHandler()..它将得到锁,例如没有死锁。 lock语句在内部使用Monitor类,它是reentrant ,例如,如果线程已经拥有/拥有锁,线程可以多次获得锁。多线程和锁定(线程安全操作)

    另一种情况是在ProcessModifiedDatafeed完成之前task.ContinueWith启动就像你说的那样。 运行ContinueWith的线程只需等待获取锁定即可。

    如果您查看了它,我真的会考虑执行task.ContinueWith和锁之外的task.Start() 。 这可能基于您发布的代码。

    您还应该查看System.Collections.Concurrent命名空间中的ConcurrentDictionary。 它会使代码更容易,而且您不必自己管理锁定。 if (this.fileWatcherDictionary.ContainsNonNullKey(eventArgs.FullPath))在这里做某种比较交换/更新。 例如只添加,如果不在字典中。 这是一个原子操作。 没有函数可以用ConcurrentDictionary来做到这一点,但有一个AddOrUpdate方法。 也许你可以用这种方法重写它。 根据你的代码,你可以安全地使用ConcurrentDictionary至少为runningTaskDictionary

    哦, TaskCreationOptions.LongRunning是为每个任务创建一个新的线程,这是一种昂贵的操作。 Windows内部线程池在新的Windows版本中是智能的,并且正在动态调整。 它会“看到”你正在做大量的IO东西,并会根据需要和实际产生新线程。

    问候


    首先,你的问题:在ContinueWith中请求锁定本身并不是问题。 如果你打扰你在另一个锁块里面做 - 那就不要。 你的延续将在不同的时间,不同的线程异步执行。

    现在,代码本身是有问题的。 为什么在几乎不能抛出异常的语句中使用很多try-catch块? 例如这里:

     try {
         task = new Task(() => new DatafeedUploadHandler().UploadDatafeed(this.legalOrg, datafeedFileData), TaskCreationOptions.LongRunning);
     }
     catch {}
    

    你只是创造任务 - 我无法想象这会抛出什么时候。 与ContinueWith同样的故事。 这里:

    this.runningTaskDictionary.Add(task.Id, task); 
    

    你可以检查这个键是否已经存在。 但即便如此,task.Id也是您刚创建的给定任务实例的唯一ID。 这个:

    try {
        fileProperties = this.fileWatcherDictionary[eventArgs.FullPath];
    }
    catch (KeyNotFoundException keyNotFoundException) {}
    catch (ArgumentNullException argumentNullException) {}
    

    更糟糕。 你不应该使用异常lile这个 - 不要捕获KeyNotFoundException但在Dictionary上使用适当的方法(如TryGetValue)。

    因此,首先,删除所有try catch块,并为整个方法使用一个,或者将它们用于可能真正抛出异常的语句,否则就无法处理这种情况(并且您知道如何处理异常抛出)。

    那么,你处理文件系统事件的方法不是很规模和可靠的。 许多程序在将更改保存到文件时(在同一文件按顺序存在多个事件的情况下),会在很短的时间间隔内生成多个更改事件。 如果您刚开始处理每个事件的文件,这可能会导致不同类型的麻烦。 因此,您可能需要对给定文件中的事件进行限制,并且仅在最后检测到更改后的某个延迟后才开始处理。 不过,这可能有点高级。

    不要忘记尽快获取文件上的读锁,以便其他进程在您使用文件时不能更改文件(例如,您可能计算文件的md5,然后有人更改文件,然后启动上传 - 现在您的md5无效)。 其他方法是记录上次写入时间和上传时间 - 抓取读锁,并检查文件是否在两者之间没有改变。

    更重要的是,一次可以进行很多更改。 假设我非常快地复制了1000个文件 - 您不希望立即使用1000个线程立即上传它们。 你需要一个文件队列来处理,并从多个线程中取出该队列中的项目。 这样,数以千计的事件可能会同时发生,并且您的上传仍将可靠地运行。 现在您为每个更改事件创建新线程,立即开始上传(根据方法名称) - 在严重负载事件(以及上述情况)下,这将失败。


    我没有完全遵循这段代码的逻辑,但是你是否知道任务延续和对Wait / Result的调用可以内联到当前线程上? 这可能会导致重入。

    这是非常危险的,已经烧了很多。

    我也不太明白你为什么延迟开始task 。 这是一种代码味道。 另外你为什么用try包装任务创建? 这永远不会抛出。

    这显然是部分答案。 但代码看起来很纠结于我。 如果这很难审计,你可能应该首先以不同的方式写它。

    链接地址: http://www.djcxy.com/p/29445.html

    上一篇: Safe, or playing with fire?

    下一篇: creating daemon using Python libtorrent for fetching meta data of 100k+ torrents