在C#中无限/定期执行代码的最佳实践

时间:2010-12-15 15:31:41

标签: c# .net winapi events execution

通常在我的代码中,我开始的威胁基本上是这样的:

void WatchForSomething()
{
    while(true)
    {
        if(SomeCondition)
        {
             //Raise Event to handle Condition
             OnSomeCondition();
        }
        Sleep(100);
    }
}

只是要知道某些条件是否为真(例如,如果一个错误的编码库没有事件,只需要布尔变量,我需要一个“实时视图”)。

现在我想知道是否有更好的方法来完成这种工作,就像一个Windows函数挂钩,可以运行我的方法所有x秒。或者我应该为我的应用程序编写一个全局事件,提高所有x秒并让他调用我的方法:

//Event from Windows or selfmade
TicEvent += new TicEventHandler(WatchForSomething));

然后这个方法:

    void WatchForSomething()
    {
        if(SomeCondition)
        {
             //Raise Event to handle Condition
             OnSomeCondition();
        }
    }

所以,我希望这不会因为出现“主观问题”而被关闭,我只是想知道这种工作的最佳实践是什么。

7 个答案:

答案 0 :(得分:18)

编写长时间运行的事件处理代码并不一定是“最好的方法”。这取决于您正在开发的应用程序类型。

您展示的第一个示例是您经常会看到长时间运行的线程的主要方法的惯用方法。虽然通常需要使用互斥或​​等待事件同步原语而不是调用Sleep() - 它是用于实现事件处理循环的典型模式。这种方法的好处是它允许专门的处理在一个单独的线程上运行 - 允许应用程序的主线程执行其他任务或保持对用户输入的响应。这种方法的缺点是它可能需要使用内存屏障(例如锁)来确保共享资源不会被破坏。这也使得更新UI变得更加困难,因为您通常必须将此类调用封送回UI线程。

经常使用第二种方法 - 特别是在已经具有事件驱动API的系统中,例如WinForms,WPF或Silverlight。使用计时器对象或空闲事件是典型的方式如果没有用户启动的事件触发您的处理,则可以定期进行背景检查。这样做的好处是可以轻松地交互和更新用户界面对象(因为它们可以直接从同一个线程访问),并且可以减少锁和互斥锁对受保护数据的需求。这种方法的一个潜在缺点是,如果必须执行的处理非常耗时,则可能会使您的应用程序无法响应用户输入。

如果您没有编写具有用户界面的应用程序(例如服务),那么第一个表单的使用频率会更高。

暂且不谈......如果可能的话,最好使用EventWaitHandleSemaphore之类的同步对象来表示何时可以处理工作。这允许您避免使用Thread.Sleep和/或Timer对象。它减少了可以执行工作和触发事件处理代码之间的平均延迟,并且它最大限度地减少了使用后台线程的开销,因为运行时环境可以更有效地调度它们,并且不会消耗任何CPU周期直到有工作要做。

值得一提的是,如果您所做的处理是为了响应与外部源(MessageQueues,HTTP,TCP等)的通信,您可以使用WCF等技术来提供事件处理代码的框架。 WCF提供了基类,使得实现异步响应通信事件活动的客户端和服务器系统变得更加容易。

答案 1 :(得分:12)

如果您查看Reactive Extensions,它会使用observable pattern提供一种优雅的方式。

var timer = Observable.Interval(Timespan.FromMilliseconds(100));
timer.Subscribe(tick => OnSomeCondition());

可观察量的一个好处是能够组合和组合现有的可观察量,甚至使用LINQ表达式来创建新的可观察量。例如,如果你想让第二个计时器与第一个同步,但每1秒只触发一次,你可以说

var seconds = from tick in timer where tick % 10 == 0 select tick;
seconds.Subscribe(tick => OnSomeOtherCondition());

答案 2 :(得分:10)

顺便说一下,Thread.Sleep可能永远不是一个好主意。

人们通常不知道的Thread.Sleep的基本问题是Thread.Sleep 的内部实现不会提取STA消息。如果您必须等待给定时间并且不能使用内核同步对象,那么最好和最简单的替代方法是在当前线程上用Thread.Sleep替换Thread.Join并使用所需的超时。 Thread.Join的行为相同,即线程会等待所需的时间,但同时将抽取STA对象。

为什么这很重要(以下是一些详细的解释)?

有时候,在你不知道的情况下,你的一个线程可能已经创建了一个STA COM对象。 (例如,当您使用Shell API时,这有时会发生在幕后)。现在假设你的一个线程创建了一个STA COM对象,现在正在调用Thread.Sleep。 如果在某个时候必须删除COM对象(可能在GC意外的时间发生),则Finalizer线程将尝试调用对象的distruvtor。此调用将被编组到对象的STA线程,该线程将被阻止。

现在,事实上,你将有一个被阻止的终结者线程。在这种情况下,对象不能从内存中释放出来,并且会出现不好的事情。

所以底线:Thread.Sleep =糟糕。 Thread.Join =合理的替代方案。

答案 3 :(得分:5)

您展示的第一个示例是实现周期性计时器的一种相当不优雅的方式。 .NET有许多计时器对象,这使得这种事情几乎是微不足道的。查看System.Windows.Forms.TimerSystem.Timers.TimerSystem.Threading.Timer

例如,以下是您使用System.Threading.Timer替换第一个示例的方法:

System.Threading.Timer MyTimer = new System.Threading.Timer(CheckCondition, null, 100, 100);

void CheckCondition(object state)
{
    if (SomeCondition())
    {
        OnSomeCondition();
    }
}

该代码每100毫秒(或左右)调用CheckCondition

答案 4 :(得分:0)

您没有提供很多关于您为什么要这样做或您正在尝试完成的内容的背景信息,但如果可能的话,您可能需要考虑创建一个Windows服务

答案 5 :(得分:0)

使用BackgroundWoker进行其他线程安全措施:

BackgroundWorker bw = new BackgroundWorker();


bw.WorkerSupportsCancellation = true;


bw.WorkerReportsProgress = true;
.
.
.
private void bw_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker worker = sender as BackgroundWorker;

    for (;;)
    {
        if (worker.CancellationPending == true)
        {
           e.Cancel = true;

           break;
        }

        else
        {
            // Perform a time consuming operation and report progress.

            System.Threading.Thread.Sleep(100);
        }
    }
}

有关详细信息,请访问:http://msdn.microsoft.com/en-us/library/cc221403%28v=vs.95%29.aspx

答案 6 :(得分:0)

非阻塞等待其他线程/任务的一种非常简单的方法是:

(new ManualResetEvent(false)).WaitOne(500); //Waits 500ms