簡體   English   中英

是否可以在處理 Timer 后生成一個觸發 System.Timers.Timer 的 Elapsed 事件的示例?

[英]Is it possible to produce an example of firing Elapsed event of System.Timers.Timer after the Timer has been disposed?

System.Timers.Timer文檔System.Timers.TimerElapsed事件有可能在計時器上調用Dispose后觸發。

是否有可能產生確定性或具有某種統計可能性的條件 - 即捕獲這種情況的一個例子?

System.Timers.Timer 的文檔說 System.Timers.Timer 的 Elapsed 事件有可能在計時器上調用 Dispose 后觸發。

是的。 盡管他們警告的特定情況非常罕見。 如果引發了Elapsed事件,但在處理計時器之前無法立即在線程池中進行調度(例如,線程池當前正忙於其他任務,因此在創建新線程以服務於該計時器之前存在延遲) timer),那么一旦計時器的工作程序啟動,它就會看到計時器已被處理,並且不會引發事件。

引發事件的唯一方法是,如果計時器實際開始執行事件引發邏輯,執行已處理的檢查,但在進一步執行之前,已調度的 Windows 線程會搶占該線程並調度該線程,然后該線程將立即處理計時器。 在這種情況下,引發事件已經在進行中,對已處置的檢查已經進行,因此一旦被搶占的線程再次恢復,即使計時器已經處置,它也會繼續進行。

可以想象,這種情況非常罕見。 它可能發生,您絕對應該編寫代碼來防御它,但是很難隨意重現,因為我們無法控制Timer類本身內的精確執行順序。

注意:以上內容取決於實現。 文檔中沒有任何內容承諾這種確切的行為。 雖然不太可能,但實現總是有可能發生變化,這樣一旦計時器過去並且線程池工作線程排隊引發事件,就不會進行進一步的處理檢查。 無論如何,依賴場景的稀有性是不好的,但它尤其糟糕,因為甚至不能保證稀有性。

綜上所述,展示一個更現實的問題是微不足道的。 由於事件在 .NET 中的工作方式,即它們可以有多個訂閱者,您甚至不需要一些罕見的線程調度序列來發生。 Elapsed事件的處理程序之一處理計時器就足夠了。 只要該處理程序在另一個之前訂閱,那么另一個處理程序將在計時器被處理后執行。 這是一個代碼示例,演示了:

Timer timer = new Timer(1000);
SemaphoreSlim semaphore = new SemaphoreSlim(0);

timer.Elapsed += (sender, e) =>
{
    WriteLine("Disposing...");
    ((Timer)sender).Dispose();
};

timer.Disposed += (sender, e) =>
{
    WriteLine("Disposed!");
};

timer.Elapsed += (sender, e) =>
{
    WriteLine("Elapsed event raised");
    semaphore.Release();
};

timer.Start();
WriteLine("Started...");
semaphore.Wait();
WriteLine("Done!");

運行時,您會看到"Disposed!" 消息顯示在"Elapsed event raised"

最重要的是,你永遠不應該編寫一個Elapsed事件處理程序,它假設在別處執行的旨在停止計時器的代碼必須保證阻止事件處理程序執行。 如果處理程序對程序中其他地方的狀態有某種依賴性,則必須獨立於計時器對象本身處理該依賴性中更改的同步和信號通知。 您不能依賴計時器對象來成功阻止處理程序的執行。 一旦您訂閱並啟動了計時器,事件處理程序總是有可能執行的,您的處理程序需要為這種可能性做好准備。


值得一提的是,這里有一種不可靠的方式來演示使用線程池接近耗盡來實現效果的無序Elapsed事件:

int regular, iocp;
int started = 0;

void Started()
{
    started++;
    WriteLine($"started: {started}");
}

ThreadPool.GetMinThreads(out regular, out iocp);

WriteLine($"regular: {regular}, iocp: {iocp}");

regular -= 1;

CountdownEvent countdown = new CountdownEvent(regular);

while (regular-- > 0)
{
    ThreadPool.QueueUserWorkItem(_ => { Started(); Thread.Sleep(1000); countdown.Signal(); });
}

Timer timer = new Timer(100);

timer.Elapsed += (sender, e) => WriteLine("Elapsed event raised");
WriteLine("Starting timer...");
timer.Start();
Thread.Sleep(100);
WriteLine("Disposing timer...");
timer.Dispose();

WriteLine("Workers queued...waiting");
countdown.Wait();
WriteLine("Workers done!");

當我運行此代碼時,實際上調用了Elapsed處理程序的情況通常會發生,並且當它調用時,會在處理計時器之后發生。

有趣的是,我發現這只會在我幾乎耗盡線程池時發生。 即仍然有一個線程在池中等待計時器對象,但其他線程也保持忙碌。 如果我沒有將任何其他工作人員排隊,或者如果我完全耗盡了線程池,那么當計時器到達其工作人員運行的點時,計時器已被處置並抑制引發Elapsed事件。

很明顯為什么完全耗盡線程池會導致這種情況。 不太清楚的是為什么需要幾乎耗盡線程池才能看到效果。 我相信(但尚未證明)這是因為這樣做可確保 CPU 內核保持忙碌(包括也在運行的主線程),從而允許 OS 線程調度程序在運行計時器工作線程時獲得足夠的延遲恰逢其時。

這不是 100% 可靠,但在我自己的測試中,它確實在超過 50% 的時間內展示了該行為。

暫無
暫無

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

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