跨线程编排事件

3

我想这可能会被标记为重复和关闭,但我无法找到一个清晰、简洁的答案来回答这个问题。所有的回复和资源几乎都涉及Windows Forms和利用预构建的实用类,如BackgroundWorker。我非常希望能够理解这个概念的核心,以便将基本知识应用于其他线程实现。

我想要实现的一个简单例子:

//timer running on a seperate thread and raising events at set intervals
//incomplete, but functional, except for the cross-thread event raising
class Timer
{
    //how often the Alarm event is raised
    float _alarmInterval;
    //stopwatch to keep time
    Stopwatch _stopwatch;
    //this Thread used to repeatedly check for events to raise
    Thread _timerThread;
    //used to pause the timer
    bool _paused;
    //used to determine Alarm event raises
    float _timeOfLastAlarm = 0;

    //this is the event I want to raise on the Main Thread
    public event EventHandler Alarm;

    //Constructor
    public Timer(float alarmInterval)
    {
        _alarmInterval = alarmInterval;
        _stopwatch = new Stopwatch();
        _timerThread = new Thread(new ThreadStart(Initiate));
    }

    //toggles the Timer
    //do I need to marshall this data back and forth as well? or is the
    //_paused boolean in a shared data pool that both threads can access?
    public void Pause()
    {
        _paused = (!_paused);            
    }

    //little Helper to start the Stopwatch and loop over the Main method
    void Initiate()
    {
        _stopwatch.Start();
        while (true) Main();    
    }

    //checks for Alarm events
    void Main()
    {
        if (_paused && _stopwatch.IsRunning) _stopwatch.Stop();
        if (!_paused && !_stopwatch.IsRunning) _stopwatch.Start();
        if (_stopwatch.Elapsed.TotalSeconds > _timeOfLastAlarm)
        {
            _timeOfLastAlarm = _stopwatch.Elapsed.Seconds;
            RaiseAlarm();
        }
    }
}

两个问题:首先,我该如何将事件传递到主线程以便通知感兴趣的方Alarm件。
其次,关于Pause()方法,它将被在主线程上运行的对象调用;我能否通过调用_start()/_stop()直接操作在后台线程上创建的Stopwatch?如果不能,主线程是否可以像上面所示那样调整_paused布尔值,以便后台线程可以看到_paused的新值并使用它?
我发誓,我已经做过我的研究,但这些(基本和关键)细节对我来说还不清楚。
免责声明:我知道有一些类可用,它们提供了我正在描述的Timer类的确切特定功能。 (事实上,我相信这个类就叫做Threading.Timer)然而,我的问题并不是为了帮助我实现Timer类本身,而是理解如何执行驱动它的概念。

1
我认为你需要寻找的不是一种在线程之间传递事件的方式,而是一种在另一个线程上发生某些事情时通知一个线程的方法。这是一个稍微不同的概念,意味着监听线程也会阻塞或主动轮询/循环直到接收到信号。 - YavgenyP
@YavgenyP 这正是我想要做的,让ThreadTwo向ThreadOne发出信号来执行工作。我一直使用事件来通知不相关的代码执行某些操作,所以这就是我描述意图的方式。不过请原谅我,我不确定我是否看到了调度和事件与调度其他类型数据(信号)之间的区别。 - Michael
你似乎想要的是信号量(Semaphores)和互斥锁(Mutex)的概念,这些方法可以在线程之间传递事件。这些对象本身在线程之间共享,因此对它们的更改可以在其他线程中看到。这允许你阻塞一个线程直到另一个线程执行某些操作。我认为你也可以使用事件(events)和委托(delegates)来实现类似的功能,虽然我必须承认我从未尝试过这样做。 - Nevyn
可能是重复的问题:非 Win Form C# 应用中同步事件处理程序线程执行 - Hans Passant
2个回答

2

你不能在现有线程上神奇地执行代码。
相反,你需要使用线程安全的数据结构,显式地让现有线程执行你的代码,并告诉它要做什么。

这就是 Control.Invoke 的工作原理(也是 BackgroundWorker 的工作原理)。
WinForms 在 Application.Run() 中运行一个消息循环,大致如下:

while(true) {
    var message = GetMessage();  //Windows API call
    ProcessMessage(message);
}

Control.Invoke()会发送一个Windows消息(使用Windows内部的线程安全消息传递代码),告诉它运行您的委托。 ProcessMessage(在UI线程上执行)将捕获该消息并执行委托。

如果您想自己完成此操作,则需要编写自己的循环。您可以使用.NET 4.0中的新线程安全生产者-消费者集合,或者您可以使用委托字段(带有Interlocked.CompareExchange)和AutoResetEvent自己完成。


您提供的代码片段对我来说非常令人困惑,但我怀疑这可能是因为它不完整/过于简化。如果没有消息,那个循环似乎什么都不做,甚至不会绘制当前的用户界面。也许总是会有一条消息。如果ThreadOne创建并启动ThreadTwo,但随后必须进入一个循环来监听ThreadTwo的信息,那么在第二个线程上执行代码没有任何并行的好处。我猜想每个ThreadOne循环可能会执行一个名为Listen()的方法,以检查来自ThreadTwo的信息? - Michael
1
@Michael:用户界面通过 WM_PAINT 消息进行绘制。 - SLaks
听起来你不想要一个完整的消息循环,而是想要一个生产者-消费者式的设置。 - SLaks
我明白了。所以UI线程致力于处理循环及与其接收的消息相关的操作,而另一个或多个线程则持续提供执行工作所需的信息。无论这些数据在周期之间是否发生变化,另一个线程都会发送一个带有绘制数据的'Draw()'消息。 - Michael
@Michael:不需要;Windows会处理所有这些。如果想了解更多关于它是如何工作的信息,请阅读Raymond Chen或MSDN Win32文档。 - SLaks
显示剩余2条评论

2
注意:我在这里写下这些内容,因为评论空间不够用,当然这并不是一个完整的或者说是半个完整的答案:

我一直使用事件来通知其他代码去做一些事情,所以这就是我的意图。不过请原谅我,我不确定使用马歇尔和事件与使用马歇尔其他类型的数据(信号)之间有什么区别。

从概念上讲,两者都可以被视为事件。使用提供的同步/信号对象和尝试自行实现此类操作之间的区别在于执行该任务的人和方式。

.NET中的事件只是委托,即应该在事件提供程序触发时执行的方法指针列表。 如果我理解正确,你所说的(调度事件)是在发生某些事情时共享事件对象,而信号的概念通常是指在开始时共享的对象,并且两个线程通过手动或自动检查其状态(依靠.NET和Windows提供的工具)都“知道”发生了某件事情。

在最基本的场景中,您可以使用一个布尔变量来实现这种信号概念,其中一个线程不断循环以检查布尔值是否为真,另一个则将其设置为真,作为一种通知发生了某些事情的方法。.NET提供的不同信号工具以更少的资源浪费的方式实现此操作,同时在没有信号(布尔等于false)的情况下也不会执行等待线程,但从概念上讲,这是相同的思路。


尽管这并不是一个完整或者一半完整的答案,但它给了我所需要的。实例化一个对象,在该对象中以独立线程运行一个进程,并使用主线程来检查一个信号值的变化、完成等。这样更加合理,因为我对原问题的后续提问本来会是:“事件的实现方法相对于主线程的代码体在哪里执行?”现在,清楚了,它在主线程检查的地方执行。完美。 - Michael

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接