C#多线程和Windows窗体

4
我的响应式GUI与后台进程的方法正确吗?如果不是,请批评并提出改进意见。特别是,指出哪些代码可能会遇到死锁或竞态条件。
工作线程需要能够被取消并报告其进度。我没有使用BackgroundWorker,因为我看到的所有示例都将Process代码放在表单本身上,而不是一个单独的对象上。我考虑过为BackgroundWorker继承LongRunningProcess,但我认为这会在对象上引入不必要的方法。理想情况下,我希望不需要对进程("_lrp")有Form引用,但我不知道如何取消进程,除非我在LRP上有一个事件来检查调用者上的标志,但这似乎过于复杂,甚至可能是错误的。
Windows Form(编辑:将*.EndInvoke调用移动到回调中)
public partial class MainForm : Form
{
    MethodInvoker _startInvoker = null;
    MethodInvoker _stopInvoker = null;
    bool _started = false;

    LongRunningProcess _lrp = null;

    private void btnAction_Click(object sender, EventArgs e)
    {
        // This button acts as a Start/Stop switch.
        // GUI handling (changing button text etc) omitted
        if (!_started)
        {
            _started = true;
            var lrp = new LongRunningProcess();

            _startInvoker = new MethodInvoker((Action)(() => Start(lrp)));
            _startInvoker.BeginInvoke(new AsyncCallback(TransferEnded), null);
        }
        else
        {
            _started = false;
            _stopInvoker = new MethodInvoker(Stop);
                _stopInvoker.BeginInvoke(Stopped, null);
        }
    }

    private void Start(LongRunningProcess lrp)
    {
        // Store a reference to the process
        _lrp = lrp;

        // This is the same technique used by BackgroundWorker
        // The long running process calls this event when it 
        // reports its progress
        _lrp.ProgressChanged += new ProgressChangedEventHandler(_lrp_ProgressChanged);
        _lrp.RunProcess();
    }

    private void Stop()
    {
        // When this flag is set, the LRP will stop processing
        _lrp.CancellationPending = true;
    }

    // This method is called when the process completes
    private void TransferEnded(IAsyncResult asyncResult)
    {
        if (this.InvokeRequired)
        {
            this.BeginInvoke(new Action<IAsyncResult>(TransferEnded), asyncResult);
        }
        else
        {
            _startInvoker.EndInvoke(asyncResult);
            _started = false;
            _lrp = null;
        }
    }

    private void Stopped(IAsyncResult asyncResult)
    {
        if (this.InvokeRequired)
        {
            this.BeginInvoke(new Action<IAsyncResult>(Stopped), asyncResult);
        }
        else
        {
            _stopInvoker.EndInvoke(asyncResult);
            _lrp = null;
        }
    }

    private void _lrp_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        // Update the progress
        // if (progressBar.InvokeRequired) etc...
    }
}

后台进程:

public class LongRunningProcess
{
    SendOrPostCallback _progressReporter;
    private readonly object _syncObject = new object();
    private bool _cancellationPending = false;

    public event ProgressChangedEventHandler ProgressChanged;

    public bool CancellationPending
    {
        get { lock (_syncObject) { return _cancellationPending; } }
        set { lock (_syncObject) { _cancellationPending = value; } }
    }

    private void ReportProgress(int percentProgress)
    {
        this._progressReporter(new ProgressChangedEventArgs(percentProgress, null));
    }

    private void ProgressReporter(object arg)
    {
        this.OnProgressChanged((ProgressChangedEventArgs)arg);
    }

    protected virtual void OnProgressChanged(ProgressChangedEventArgs e)
    {
        if (ProgressChanged != null)
            ProgressChanged(this, e);
    }

    public bool RunProcess(string data)
    {
        // This code should be in the constructor
        _progressReporter = new SendOrPostCallback(this.ProgressReporter);

        for (int i = 0; i < LARGE_NUMBER; ++i)
        {
            if (this.CancellationPending)
                break;

            // Do work....
            // ...
            // ...

            // Update progress
            this.ReportProgress(percentageComplete);

            // Allow other threads to run
            Thread.Sleep(0)
        }

        return true;
    }
}

投票关闭,因为“请批评”听起来不像一个问题。 - John Saunders
5个回答

1

我喜欢将后台进程分离到一个单独的对象中。然而,我的印象是你在同一个按钮处理程序中调用BeginInvoke和EndInvoke,导致UI线程被阻塞直到后台进程完成。

MethodInvoker methodInvoker = new MethodInvoker((Action)(() => Start(lrp)));
IAsyncResult result = methodInvoker.BeginInvoke(new AsyncCallback(TransferEnded), null);
methodInvoker.EndInvoke(result);

还是我漏掉了什么吗?


是的,我认为你说得对。我不知道最初是怎么错过了那个,但我会将代码移动到回调方法中。 - ilitirit

1

你可以将_cancellationPending设置为volatile并避免锁定。

为什么要在另一个线程中调用Stop?

你应该更改事件调用方法以避免竞态条件:

protected virtual void OnProgressChanged(ProgressChangedEventArgs e)
{
    var progressChanged = ProgressChanged;
    if (progressChanged != null)
        progressChanged(this, e);
}

如果后台工作程序适合,你就不必重新编写它 ;)

我在单独的线程中调用Stop,因为我需要知道它何时停止(回调),但我将放弃这种方法并使用ProcessCompletedEvent或类似的东西。我可以使用BackgroundWorker,但那样我就必须重新编写Worker方法以与bw.DoWork事件兼容。我会尝试几种可能性。 - ilitirit
我认为有一个问题...你的停止方法/回调没有等待你的LRP结束。但是如果你正在更改它,那就没关系了。 使用工作器比自制设计更安全。 - François

1

我对你使用MethodInvoker.BeginInvoke()有点困惑。你选择使用这个方法而不是创建一个新线程并使用Thread.Start(),是否有原因呢?

我认为你可能会阻塞UI线程,因为你在与BeginInvoke相同的线程上调用了EndInvoke。我认为正常的模式是在接收线程上调用EndInvoke。这在异步I/O操作中肯定是正确的 - 如果这里不适用,请谅解。无论如何,你应该很容易确定你的UI线程是否被阻塞,直到LRP完成。

最后,你依赖于BeginInvoke的副作用来在托管线程池中的工作线程上启动你的LRP。同样,你应该确定这是你的意图。线程池包括排队语义,并在加载大量短生命周期进程时表现出色。但我不确定它是否适合长时间运行的进程。我更倾向于使用Thread类来启动你的长时间运行的线程。

此外,虽然我认为你取消LRP的信号方法可以工作,但我通常使用ManualResetEvent来实现这个目的。你不必担心锁定事件以检查其状态。


说实话,我知道我没有使用Thread.Start的原因,只是现在想不起来了。可能是这个原因已经不适用了,所以我会尝试不同的实现并比较结果。 - ilitirit

0

正如Guillaume所说,你在OnProgressChanged方法中有一个竞态条件,但是我不认为提供的答案是一个解决方案。你仍然需要一个同步对象来处理它。

private static object eventSyncLock = new object();

protected virtual void OnProgressChanged(ProgressChangedEventArgs e)
{
    ProgressChangedEventHandler handler;
    lock(eventSyncLock)
    {
      handler = ProgressChanged;
    }
    if (handler != null)
        handler(this, e);
}

我在OnProgressChanged中使用的代码基本上来自BackgroundWorker类,但它们在那里没有检查竞态条件。也许有些东西我漏掉了。 - ilitirit
我的代码没问题,你不需要使用锁定机制: http://blogs.msdn.com/ericlippert/archive/2009/04/29/events-and-races.aspx - François
好吧,我想无论如何你最终都会遇到竞态条件,因为你发布的链接指出:“此代码存在竞态条件,导致行为不正确”。我的代码只是保证在分配给处理程序时获取到最新的值。http://www.yoda.arachsys.com/csharp/events.html - scottm
竞态条件不在你想象的地方...事件可能会在取消订阅后被调用,但你无法避免它。我建议你阅读这篇文章。 - François

0
你可以使用BackgroundWorker,仍然将工作代码移出Form类。创建一个名为Worker的类,其中包含方法Work。让Work接受一个BackgroundWorker参数,并重载Work方法,使其具有非BackgroundWorker签名,该签名将null发送到第一个方法。
然后在你的表单中,使用一个具有进度报告功能的BackgroundWorker,在Work(BackgroundWorker bgWorker, params object[] otherParams)中,你可以包括诸如以下语句:
    if( bgWorker != null && bgWorker.WorkerReportsProgress )
    {
        bgWorker.ReportProgress( percentage );
    }

... 同样包括检查是否取消了操作。

然后在您的窗体代码中,您可以处理事件。首先设置 bgWorker.DoWork += new DoWorkEventHandler( startBgWorker );,其中该方法启动您的 Worker.Work 方法,将 bgWorker 作为参数传递。

然后可以从按钮事件开始,调用 bgWorker.RunWorkerAsync。

然后,第二个取消按钮可以调用 bgWorker.CancelAsync,这将在您检查 CancellationPending 的部分中被捕获。

成功或取消后,您将处理 RunWorkerCompleted 事件,在其中检查工作者是否已取消。然后,如果它没有被取消,您就假定它成功并沿着那个路线继续。

通过重载 Work 方法,您可以使其可重复使用,而不需要关心表单或 ComponentModel。

当然,你可以实现progresschanged事件而不需要重新发明轮子。专业提示:ProgressChangedEventArgs接受一个int参数,但不强制将其限制在100以内。为了报告更精细的进度百分比,可以传递一个带有乘数(比如100)的参数,这样14.32%就会成为1432的进度。然后,您可以格式化显示,或覆盖进度条,或将其显示为文本字段。(所有这些都具有DRY友好的设计)

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