什么是在C#中实现多线程的最佳方法?

4

如何在C#中最好地进行线程处理(方法)?

例如:

假设我有一个表单,想要从数据库加载数据。

My form controls: 
 - dataGridView (to show data from DB), 
 - label (loading status) and 
 - button (start loading).

当我点击按钮时,我的表单会被冻结,直到任务完成。此外,加载状态在任务完成之前不会改变。我认为异步线程是答案?
所以我的问题是:处理这种情况的最佳方法是什么?我知道有很多关于线程的内容,但它们之间有什么区别,如何使其线程安全?
你如何解决这类问题?
最好的问候。

2
什么是在c#中创建线程的最佳方式?请小心。 - Greg
4个回答

7

谢谢回答。是的,我正在使用win forms和.NET 2.0。我尝试了AdamRalph的回答中的ThreadPool,但出现了错误:“无效的跨线程操作:从创建它的线程以外的线程访问控件“ dataGridView”。 - Jooj
一旦您将工作移动到另一个线程,它就无法直接访问用户界面(因此需要跨线程调用)。您需要使用Control.Invoke()或Control.BeginInvoke()将控制权传递回UI线程,请求其为您更新UI。 - Jason Williams
这就是为什么BackgroundWorker类很好用的原因;它在UI线程上引发事件,因此不需要使用Invoke() - Kevin Kibler

3
没有普遍适用的“最佳”线程处理方式。您需要尝试不同的方法,我很抱歉。我特别喜欢Jeremy D. Miller在这个页面上描述的延续思想(向下滚动以找到“continuations”部分)。 这真的很优雅,并意味着编写非常少的样板代码。
基本上,当您使用Func参数调用“ExecuteWithContinuation”时,该函数将异步执行,然后在完成时返回操作。 然后将该操作转发回您的UI线程以充当延续。这使您可以快速将操作分成两个部分:
1. 执行不应阻塞UI的长时间运行操作 2. ... 完成后,在UI线程上更新UI
需要一些适应时间,但很酷。
public class AsyncCommandExecutor : ICommandExecutor
{
    private readonly SynchronizationContext m_context;

    public AsyncCommandExecutor(SynchronizationContext context)
    {
        if (context == null) throw new ArgumentNullException("context");
        m_context = context;
    }

    public void Execute(Action command)
    {
        ThreadPool.QueueUserWorkItem(o => command());

    }

    public void ExecuteWithContinuation(Func<Action> command)
    {
        ThreadPool.QueueUserWorkItem(o =>
                                         {
                                             var continuation = command();
                                             m_context.Send(x => continuation(), null);
                                         });
    }
}

您可以像这样使用它(请原谅格式...)
public void DoSomethingThatTakesAgesAndNeedsToUpdateUiWhenFinished()
{
    DisableUi();
    m_commandExecutor.ExecuteWithContinuation(
                () =>
                    {
                        // this is the long-running bit
                        ConnectToServer();

                        // This is the continuation that will be run
                        // on the UI thread
                        return () =>
                                    {
                                        EnableUi();
                                    };
                    });
}

谢谢你的回答。在.NET 2.0中,我如何在不使用lambda的情况下使用它? - Jooj
现在我正在使用.NET 3.5。我遇到了一个错误:使用泛型类型'System.Action<T>'需要'1'个类型参数。该怎么办? - Jooj
"Action<T>"是具有一个参数的通用操作;我发布的代码只使用了没有参数的"Action"。看起来你正在尝试使用Action<T>而不是Action。 http://msdn.microsoft.com/en-us/library/system.action.aspx - Mark Simpson
嘿,马克。我尝试了你的解决方案,它非常好用。你能展示一下如何从ExecuteWithContinuation部分返回一些值吗?假设我有一个需要花费很长时间并且在完成后需要更新UI的DoSomethingThatTakesAgesAndNeedsToUpdateUiWhenFinished(int i)函数,并且想要更改变量。怎么做呢? - Jooj

1
你可以使用这样的模式:-
    private void RefreshButton_Click(object sender, EventArgs e)
    {
        MessageLabel.Text = "Working...";
        RefreshButton.Enabled = false;

        ThreadPool.QueueUserWorkItem(delegate(object state)
        {
            // do work here 
            // e.g. 
            object datasource = GetData();
            this.Invoke((Action<object>)delegate(object obj)
            {
                // gridview should also be accessed in UI thread 
                // e.g. 
                MyGridView.DataSource = obj;

                MessageLabel.Text = "Done.";
                RefreshButton.Enabled = true;
            }, datasource);
        });
    }

谢谢您的回答。在哪里调用Control.Invoke()?我遇到了一个错误:跨线程操作无效:从创建它的线程以外的线程访问控件“dataGridView”。 - Jooj
我已经将代码转换为Winforms。请注意,您还必须在UI线程中操作gridview,因为它是一个UI元素。这将消除您看到的运行时异常。 - Adam Ralph
我已经将我的框架更改为3.5。现在它可以正常工作了。谢谢Adam! - Jooj

0

在分离的线程中运行的代码无法访问您的控件 - 框架不允许这样做,这就解释了您遇到的错误。

您需要将从数据库检索到的数据缓存到非表单对象中,并在后台工作线程完成后使用该对象中的数据填充您的UI(并处理对该对象的访问同步)。


实际上,CrossThreadException 只会在调试模式下抛出。 - Kevin Kibler

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