C#线程和Windows窗体

时间:2009-06-12 12:31:19

标签: c# multithreading concurrency

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

需要能够取消工作线程并报告其进度。我没有使用BackgroundWorker,因为我看到的所有示例都在Form本身上有Process代码,而不是单独的对象。我想过为BackgroundWorker继承LongRunningProcess,但我想这会在对象上引入不必要的方法。理想情况下,我不希望对进程(“_lrp”)有一个Form引用,但我不知道如何取消进程,除非我在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;
    }
}

5 个答案:

答案 0 :(得分:1)

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

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

或者我错过了什么?

答案 1 :(得分:1)

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

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

最后,您依靠BeginInvoke的副作用在托管线程池的工作线程上启动LRP。再次,你应该确定这是你的意图。线程池包括排队语义,并在加载大量短期进程时做得很好。我不确定这对于长期运行的流程来说是个不错的选择。我倾向于使用Thread类来启动长期运行的线程。

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

答案 2 :(得分:1)

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

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

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

如果后台工作者适合,则不必重新编码;)

答案 3 :(得分: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);
}

答案 4 :(得分:0)

您可以使用BackgroundWorker,仍然可以将您的工作代码移到Form类之外。让你的班级工人,用它的方法工作。让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事件,而不需要在那个上重新发明轮子。 ProTip:ProgressChangedEventArgs接受一个int,但不强制它最大为100.要报告更细粒度的进度百分比,使用乘法器(比如100)传递一个参数,所以14.32%将是1432的进度。然后你可以格式化显示,或覆盖进度条,或将其显示为文本字段。 (均采用干式设计)