【问题标题】:C# threading and Windows FormsC# 线程和 Windows 窗体
【发布时间】:2009-06-12 12:31:19
【问题描述】:

我对带有后台进程的响应式 GUI 的处理方法是否正确?如果没有,请批评并提供改进。尤其要指出哪些代码可能会遇到死锁或竞争条件。

工作线程需要能够被取消并报告它的进度。我没有使用 BackgroundWorker,因为我看到的所有示例在 Form 本身上都有 Process 代码,而不是单独的对象。我曾考虑为 BackgroundWorker 继承 LongRunningProcess,但我认为这会在对象上引入不必要的方法。理想情况下,我不希望表单引用进程(“_lrp”),但我不知道如何取消进程,除非我在 LRP 上有一个检查标志的事件在调用者身上,但这似乎不必要地复杂,甚至可能是错误的。

Windows 窗体(编辑:将 *.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;
    }
}

【问题讨论】:

  • 投票结束,因为“请批评”听起来不像一个问题。

标签: c# multithreading concurrency


【解决方案1】:

我喜欢将后台进程分离在一个单独的对象中。但是,我的印象是,您的 UI 线程在后台进程完成之前被阻塞,因为您在同一个按钮处理程序中调用 BeginInvoke 和 EndInvoke。

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

还是我错过了什么?

【讨论】:

  • 是的,我认为你是对的。我不知道我最初是怎么错过的,但我会将代码移至回调方法。
【解决方案2】:

我对您使用 MethodInvoker.BeginInvoke() 感到有些困惑。您是否有理由选择使用它而不是创建新线程并使用 Thread.Start()...?

我相信您可能会阻塞您的 UI 线程,因为您在与 BeginInvoke 相同的线程上调用 EndInvoke。我会说正常模式是在接收线程上调用 EndInvoke。这对于异步 I/O 操作当然是正确的——如果它不适用于这里,我们深表歉意。在 LRP 完成之前,您应该能够轻松地确定您的 UI 线程是否被阻塞。

最后,您依靠 BeginInvoke 的副作用在托管线程池中的工作线程上启动 LRP。同样,您应该确定这是您的意图。线程池包括排队语义,并且在加载大量短期进程时表现出色。我不确定它对于长时间运行的进程是否是一个不错的选择。我倾向于使用 Thread 类来启动您的长时间运行的线程。

另外,虽然我认为您向 LRP 发出取消它的信号的方法会起作用,但我通常会为此目的使用 ManualResetEvent。您不必担心锁定事件以检查其状态。

【讨论】:

  • 说实话,我知道我不使用 Thread.Start 是有原因的,但我现在不记得它是什么了。可能是原因不再适用,所以我将尝试不同的实现并比较结果。
【解决方案3】:

您可以使您的 _cancellationPending 易失性并避免锁定。 为什么要在另一个线程中调用 Stop?

你应该改变你的事件调用方法以避免竞争条件:

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

如果后台工作人员适合,您不必重新编码;)

【讨论】:

  • 我在单独的线程中调用 Stop,因为我需要知道它何时停止(回调),但我将远离此方法并使用 ProcessCompletedEvent 或类似的东西。我可以使用 BackgroundWorker,但是我必须重新编码 Worker 方法以与 bw.DoWork 事件兼容。我会尝试几种可能性。
  • 我认为有问题...您的停止方法/回调不会等待您的 lrp 结束。但是,如果您要更改它,那没关系。使用工人,比自制设计更安全。
【解决方案4】:

正如 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 类,它们不检查那里的竞争条件。也许我缺少一些东西。
  • 我的密码没问题,你不需要锁:blogs.msdn.com/ericlippert/archive/2009/04/29/…
  • 好吧,我想无论哪种方式,您最终都会遇到竞争条件,因为您发布的链接指出“此代码具有导致错误行为的竞争条件”。我的代码只是保证您在分配给处理程序时获得最新的值。 yoda.arachsys.com/csharp/events.html
  • 竞态条件不是你想的那样... 取消订阅后可能会调用该事件,但你无法避免。我建议你阅读这篇文章。
【解决方案5】:

您可以使用 BackgroundWorker,但仍将您的工作代码移到 Form 类之外。让你的类 Worker 使用它的方法 Work。让Work以BackgroundWorker为参数,用非BackgroundWorker签名重载Work方法,将null发送给第一个方法。

然后在您的表单中,使用具有 ProgressReporting 的 BackgroundWorker,在您的工作中(BackgroundWorker bgWorker, params object[] otherParams),您可以包含以下语句:

    if( bgWorker != null && bgWorker.WorkerReportsProgress )
    {
        bgWorker.ReportProgress( percentage );
    }

...同样包括对 CancellationPending 的检查。

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

这可以从一个名为 bgWorker.RunWorkerAsync 的按钮事件开始。

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

成功或取消后,您将处理 RunWorkerCompleted 事件,在该事件中检查工作人员是否被取消。然后,如果不是,您认为它是成功的,然后走那条路。

通过重载 Work 方法,您可以使其在不关心 Forms 或 ComponentModel 的代码中可重用。

当然,您无需重新发明轮子即可实现 progresschanged 事件。专业提示:ProgressChangedEventArgs 需要一个 int,但不会强制它最大为 100。要报告更细粒度的进度百分比,请传递一个带有倍数的参数(例如 100),因此 14.32% 将是 1432 的进度。然后你可以格式化显示,或覆盖进度条,或将其显示为文本字段。 (全部采用 DRY 友好型设计)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-03-17
    • 2014-04-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多