2016-09-15 121 views
6

我有一個Windows服務,每5秒檢查一次工作。它使用System.Threading.Timer來處理檢查和處理,並使用Monitor.TryEnter來確保只有一個線程正在檢查工作。Monitor.TryEnter和Threading.Timer競爭條件

假設它必須是這種方式,因爲以下代碼是由服務創建的8個其他工作人員的一部分,每個工作人員都有自己需要檢查的特定類型的工作。

readonly object _workCheckLocker = new object(); 

public Timer PollingTimer { get; private set; } 

void InitializeTimer() 
{ 
    if (PollingTimer == null) 
     PollingTimer = new Timer(PollingTimerCallback, null, 0, 5000); 
    else 
     PollingTimer.Change(0, 5000); 

    Details.TimerIsRunning = true; 
} 

void PollingTimerCallback(object state) 
{ 
    if (!Details.StillGettingWork) 
    { 
     if (Monitor.TryEnter(_workCheckLocker, 500)) 
     { 
      try 
      { 
       CheckForWork(); 
      } 
      catch (Exception ex) 
      { 
       Log.Error(EnvironmentName + " -- CheckForWork failed. " + ex); 
      } 
      finally 
      { 
       Monitor.Exit(_workCheckLocker); 
       Details.StillGettingWork = false; 
      } 
     } 
    } 
    else 
    { 
     Log.Standard("Continuing to get work."); 
    } 
} 

void CheckForWork() 
{ 
    Details.StillGettingWork = true; 
    //Hit web server to grab work. 
    //Log Processing 
    //Process Work 
} 

現在,這裏的問題:
上面的代碼允許2個定時器線程進入CheckForWork()方法。我真的不明白這是如何可能的,但我已經體驗了這個軟件運行多個客戶端。

當我推送一些工作時,我得到的日誌顯示它檢查了兩次工作,並且有2個線程獨立地嘗試處理導致工作失敗的問題。

Processing 0-3978DF84-EB3E-47F4-8E78-E41E3BD0880E.xml for Update Request. - at 09/14 10:15:501255801 
Stopping environments for Update request - at 09/14 10:15:501255801 
Processing 0-3978DF84-EB3E-47F4-8E78-E41E3BD0880E.xml for Update Request. - at 09/14 10:15:501255801 
Unloaded AppDomain - at 09/14 10:15:10:15:501255801 
Stopping environments for Update request - at 09/14 10:15:501255801 
AppDomain is already unloaded - at 09/14 10:15:501255801 
=== Starting Update Process === - at 09/14 10:15:513756009 
Downloading File X - at 09/14 10:15:525631183 
Downloading File Y - at 09/14 10:15:525631183 
=== Starting Update Process === - at 09/14 10:15:525787359 
Downloading File X - at 09/14 10:15:525787359 
Downloading File Y - at 09/14 10:15:525787359 

日誌異步寫入和進行排隊,所以不挖太深的事實,時代嚴絲合縫,我只是想指出,我在日誌中看到證明我有2個線程擊中了我認爲應該從未被允許的一段代碼。 (日誌和時間是真實的,只是消毒消息)

最終會發生什麼是2線程開始下載足夠大的文件,其中一個最終導致文件訪問被拒絕並導致整個更新失敗。

上面的代碼如何實際允許這個?去年我遇到過這個問題,當時我有一個lock而不是Monitor,並且認爲這只是因爲Timer最終開始得到足夠的抵消,因爲我得到了定時器線程堆積,即一個阻塞了5秒鐘,通過正確的計時器觸發另一個回調,他們都以某種方式進入。這就是爲什麼我去了Monitor.TryEnter選項,所以我不會只是保持堆疊計時器線程。

任何線索?在之前我試圖解決這個問題的所有案例中,System.Threading.Timer一直是我們不斷的,我認爲它的根本原因,但我不明白爲什麼。

+0

只是好奇,是'Details.StillGettingWork'(或其後臺字段)標記爲'volatile'? – itsme86

+0

@ itsme86'Details'是一個實例類,'StillGettingWork'是一個自動屬性。沒有什麼顯着易變的。 – TyCobb

+0

是不是這樣爲什麼mutexes被創造爲什麼? https://msdn.microsoft.com/en-us/library/windows/hardware/ff548097(v=vs.85).aspx –

回答

0

TL; DR
生產存儲過程未經過多年更新。工人正在從事本應該從未得到的工作,因此多名工人正在處理更新請求。


我終於找到時間在本地正確設置自己,充當生產客戶端,通過Visual Studio。雖然我沒有像我經歷過的那樣重現它,但我偶然偶然發現了這個問題。

那些假設多名工作人員正在完成工作的人確實是正確的,而這應該是永遠不會發生的,因爲每個工人在他們所做和所要求的工作中都是獨一無二的。

事實證明,在我們的生產環境中,根據工作類型檢索工作的存儲過程在部署的年份(是,年!)內未更新。任何檢查工作的東西都會自動獲得更新,這意味着更新工作人員和工作人員Foo在同一時間檢查時,他們都會以相同的工作結束。

謝天謝地,修復是數據庫端而不是客戶端更新。

0

我可以在日誌中看到你已經提供了一個AppDomain在那裏重新啓動,是正確的嗎?如果是,您確定在AppDomain重新啓動期間,您有一個服務對象是唯一的服務對象嗎?我認爲在這期間並不是所有的線程都在同一時間停止,其中一些線程可以繼續輪詢工作隊列,因此不同的AppDomain中的兩個不同線程獲得了相同的工作Id

你也許可以帶標記的_workCheckLockerstatic關鍵字,這樣解決這個問題:

static object _workCheckLocker; 

,並介紹了與此字段(初始化類的靜態構造函數中,你可能面臨的內聯初始化的情況下,一些更復雜的問題),但我不確定這是否足夠你的情況 - 在AppDomain重新啓動靜態類將重新加載。據我所知,這不適合你。

也許你可以爲你的工人引入static字典而不是對象,所以你可以檢查Id中正在處理的文檔。

另一種方法是處理Stopping事件爲您服務,這可能可以在AppDomain重啓,在其中將引入CancellationToken期間調用,並用它在這樣的情況下,停止所有工作。

另外,正如@ fernando.reyes所說,你可以引入一個稱爲互斥體的重鎖結構來實現同步,但這會降低你的性能。

+0

AppDomain用於加載執行工作者需要的實際處理的類。工人是通用的。當它得到更新時,它基本上會自動更新。非常感謝您的時間。我只需要找到一天我可以錘擊它並嘗試通過Visual Studio重現它的那一天。 – TyCobb

+0

哦,好的。我認爲你提供的代碼是線程安全的。也許,由於某種原因,兩個不同的工作人員得到了相同的文件來處理。 – VMAtm

+0

他們確實.... = / – TyCobb