-1

Notice: I do not control the UI code. Only the class PeriodicalThing code.


Consider this scenario:

The UI thread is happily doing UI stuff, while there is another background thread which periodically raises events. The events are subscribed to by some UI, which means the event handlers generally have to use Invoke.

What I need is a way to perform the clean up operation (cleaning up means stopping the background thread, basically) in safe manner, that guarantees:

  1. User-defined code (i.e. event handler code) is not asynchronously aborted in the middle of execution
  2. There are no more periodical operations executed or executing after the clean up function has returned
  3. No more event handlers will be executed or executing after the clean up function has returned

I came up with something, but there is a deadlock. The deadlock is basically an error in the user-defined code, where the usage of BeginInvoke could have fixed the problem, but a straight out deadlock in case of a non-trivial programming error isn't the way to go.
Also note that BeginInvoke only happens to work in the FormClosing scenario because the invocation list of a Form happens to be cleared after FormClosing; something that seems to be consistent but I haven't found it documented yet.

If there is a solution, it's apparently not obvious, but maybe I'm missing out a trick. I can't believe noone has run into a similar problem before.

class PeriodicalThing
{
    bool abort = false;
    Thread PeriodicalThread;
    ...
    PeriodicalThreadProc()
    {
        while (!this.abort)
        {
            DoStuff();
            OnThingsHappened(new EventArgs(...));
        }
    }

    public event EventHandler<EventArgs> ThingsHappened;
    protected virtual void OnThingsHappaned(EventArgs e)
    {
        // update -- oversight by me - see Henk Holterman's answer
        var handler = this.ThingsHappened;
        if (handler != null)
        {
            handler(this, e);
        }
    }

    public void CleanUp()
    {
        this.abort = true;
        // ui thread will deadlock here
        this.PeriodicalThread.Join();
    }
}

...

// user-defined code; consider this immutable
class Form1 : Form
{
    .ctor()
    {
        ...
        this.PeriodicalThing.ThingsHappened += this.ThingsHappenedHandler
    }

    private void ThingsHappenedHandler(object sender, EventArgs e)
    {
        if (this.InvokeRequired) // actually always true
        {
            // periodical thread will deadlock here
            this.Invoke(
                new Action<object, EventArgs>(this.ThingsHappenedHandler), sender, e)
                );
            return;
        }
        this.listBox1.Items.Add("things happened");
    }

    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        this.PeriodicalThing.CleanUp();
    }
}

When the UI thread is shutting down due to an event such as FormClosing, which happened on the UI thread, then it will trigger the clean up. When the background thread issues an Invoke at this time, the Invoke has to block until the UI thread is finished with its current event handler (which triggered the clean up in the first place). Also, the clean up operation needs to wait for the background thread (and thus the current Invoke) to terminate, causing a deadlock.


The optimal solution would be to interrupt the UI thread at Thread.Join() and let all the waiting Invokes execute, and then going back to Thread.Join(). But that seems impossible to me with C#. Maybe someone has a crazy idea how I could use some helper threads to move the clean up method away from the UI thread, but I don't know how I would do that.

4

3 回答 3

1

问题就在这里

public void CleanUp()
{
    this.abort = true;
    // ui thread will deadlock here
    this.PeriodicalThread.Join();  // just delete this
}

Join()阻塞(!)调用线程。而这反过来又会阻止所有 Invoke 操作。

这枚硬币的另一部分是

{
  if (this.InvokeRequired) // actually always true
    {
        // periodical thread will deadlock here
    //    this.Invoke(
        this.BeginInvoke(            // doesn't wait so it doesn't block
            new Action<object, EventArgs>(this.ThingsHappenedHandler), sender, e)
            );
        return;
    }
    this.listBox1.Items.Add("things happened");
 }

BeginInvoke 无论如何都是对 Invoke 的改进,只要您只返回void并且不使 MessageLoop 过载。

但是请摆脱 Join()。它没有用。

编辑,因为某些部分不受您的控制:

以下确实是答案,并且是可能的:

在 Thread.Join() 中断 UI 线程并让所有等待的 Invokes 执行,然后返回 Thread.Join()

public void CleanUp()
{
    this.abort = true;
 
    while (! this.PeriodicalThread.Join(20)) 
    { 
       Application.DoEvents(); 
    }
}

这不会死锁,但使用Application.DoEvents(). 您必须检查所有其他事件(FormClosing)中发生的情况,这也不在您的控制之下......
它可能会起作用,但需要进行一些严格的测试。


由于您正在混合线程和事件,请使用以下模式:

protected virtual void OnThingsHappaned(EventArgs e)
{
    var handler = ThingsHappened;

    if (handler  != null)
    {
        handler (this, e);
    }
}
于 2012-09-19T20:23:16.450 回答
0

我不喜欢建议重写,但我可以建议使用BackgroundWorker?

我发现它是一个快速简单的异步过程,您可以将有关其状态的消息发送回调用线程(如百分比或状态对象)。

可以在此处找到快速操作方法:http: //msdn.microsoft.com/en-us/library/cc221403 (v=vs.95).aspx

于 2012-09-19T20:36:31.327 回答
0

不确定这是否适合您的情况,但我使用 BackGroundWorker 和 SupportCancellation。如果我离开,请发表评论,我将删除。

BackgroundWorker.WorkerSupportsCancellation 属性

我使用它来延迟加载动态创建的昂贵的 FlowDocument。如果他们单击下一个文档,则需要取消该工作并开始下一个。

抱歉,我刚刚看到另一个 BackGroundWorker 以及您无法使用它的评论。我没有在 UI 级别使用它。我什至没有使用 WinForms。我在业务/数据层使用。

于 2012-09-19T20:44:24.770 回答