簡體   English   中英

Task.ContinueWith中的嵌套鎖 - 安全,還是玩火?

[英]Nested lock in Task.ContinueWith - Safe, or playing with fire?

Windows服務:從配置文件中要監視的目錄列表生成一組FileWatcher對象,具有以下要求:

  1. 文件處理可能非常耗時 - 必須在自己的任務線程上處理事件
  2. 保持事件處理程序任務的句柄以等待OnStop()事件中的完成。
  3. 跟蹤上傳文件的哈希值; 如果沒有不同,不要重新處理
  4. 保留文件哈希以允許OnStart()處理服務關閉時上載的文件。
  5. 永遠不要多次處理文件。

(關於#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服務,它可能不會崩潰。 永遠。 如果任何這些線程有未處理的異常,它將會做什么。

此外,這些例外也寫入了未來的防彈。 我在下面的評論中給出的例子是為處理程序添加一個工廠......因為今天編寫代碼,永遠不會有空任務,但如果工廠沒有正確實現,代碼可能拋出異常。 是的,應該在測試中捕獲。 但是,我的團隊中有初級開發人員......“May。不。崩潰。” (另外,它必須正常關閉,如果未處理的異常,允許當前正在運行的線程來完成-這是我們做的主設置未處理的異常處理())。 我們將企業級監視器配置為在事件日志中出現應用程序錯誤時發送警報 - 這些異常將記錄並標記我們。 這種方法是經過深思熟慮和討論的決定。

每個可能的異常都經過仔細考慮和選擇,分為兩類 - 適用於單個數據饋送但不會關閉服務(大多數)的類別,以及那些表明清晰編程或其他根本導致代碼對所有數據饋送都沒用。 例如,如果我們無法寫入事件日志,我們就選擇關閉服務,因為這是我們指示數據饋送未得到處理的主要機制。 異常是在本地捕獲的,因為本地上下文是唯一可以繼續做出決定的地方。 此外,允許異常冒泡到更高級別(1)違反了抽象概念,並且(2)在工作線程中沒有意義。

我對反對處理異常的人數感到驚訝。 如果我每次嘗試都有一角硬幣。只需要一點錢(例外){什么都不做}我知道,你會在永恆的剩余時間內獲得鎳幣的變化。 我會爭辯死亡1 ,如果調用.NET框架或您自己的代碼拋出異常,您需要考慮會導致該異常發生的情況並明確決定如何處理它。 我的代碼捕獲了IO操作中的UnauthorizedExceptions,因為當我考慮如何發生這種情況時,我意識到添加一個新的datafeed目錄需要授予服務帳戶的權限(默認情況下不會有這些權限)。

我很欣賞這些建設性的意見......請不要批評簡化的示例代碼,並使用廣泛的“這個糟透了”的畫筆。 代碼並不糟糕 - 它是防彈的,必然如此。


1如果Jon Skeet不同意,我只會說很長時間

首先,你的問題:在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個線程開始上傳它們。 您需要一個要處理的文件隊列,並從該隊列中獲取具有多個線程的項目。 這樣一來就可能發生數千個事件,您的上傳仍然可靠。 現在,您為每個更改事件創建新線程,您立即開始上傳(根據方法名稱) - 這將在嚴重的事件加載(以及上述情況)下失敗。

不,它不會燒你。 即使將ContinueWith內聯到正在運行new Task(() => new DatafeedUploadHandler()..的當前線程new Task(() => new DatafeedUploadHandler()..它將獲得鎖定,例如沒有死鎖。 lock語句在內部使用Monitor類,並且它如果它已經擁有/擁有鎖,則線程可以多次獲取鎖。 多線程和鎖定(線程安全操作)reentrant

task.ContinueWithProcessModifiedDatafeed完成之前啟動的另一種情況就像你說的那樣。 運行ContinueWith的線程只需要等待獲取鎖定。

我真的會考慮做task.ContinueWith和在鎖之外的task.Start() ,如果你審查它。 並且可以根據您發布的代碼進行操作。

您還應該查看System.Collections.Concurrent命名空間中的ConcurrentDictionary 這將使代碼更容易,你不必自己管理鎖定。 if (this.fileWatcherDictionary.ContainsNonNullKey(eventArgs.FullPath))你在這里進行某種比較交換/更新 例如,只有在詞典中沒有添加。 這是一個原子操作。 使用ConcurrentDictionary沒有任何功能,但有一個AddOrUpdate方法。 也許你可以使用這種方法重寫它。 根據您的代碼,您可以安全地使用ConcurrentDictionary至少用於runningTaskDictionary

哦, TaskCreationOptions.LongRunning是為每個任務創建一個新線程,這是一種昂貴的操作。 Windows內部線程池在新的Windows版本中是智能的,並且正在動態調整。 它會“看到”你正在做很多IO的東西,並會根據需要和實際產生新的線程。

問候

我還沒有完全遵循這段代碼的邏輯,但你知道任務延續和對Wait / Result的調用可以內聯到當前線程嗎? 這可能會導致重入。

這是非常危險的,已經燒毀了很多。

另外,我不太明白你為什么要開始延遲task 這是一種代碼味道。 另外,為什么要try包裝任務創建? 這永遠不會拋出。

這顯然是部分答案。 但代碼看起來非常糾結於我。 如果這很難審核它,你可能應該首先以不同的方式編寫它。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM