views:

317

answers:

5

What's the best way to thread work (methods) in c#?

For example:

Let's say I have a form and want to load data from db.

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

When I click the button my form is frozen until the task is done. Also the loading status does not change until task is done. I think async threading would be the answer?

So my question: what's the best way to handle this? I know there is a lot stuff about Threading, but what's the difference between them and how do you make it thread safe?

How do you solve this kind of problems?

Best Regards.

+7  A: 

If using Windows Forms, you should look at BackrgroundWorker. More generally, it is often useful to use the ThreadPool class. And finally, it is worth to take a look at the new .NET 4's Parallel class.

Konamiman
Thank you for the answer. Yes, I'm using win forms and .NET 2.0. I tried ThreadPool's from AdamRalph's answer and get and error: Cross-thread operation not valid: Control 'dataGridView' accessed from a thread other than the thread it was created on.
Jooj
Once you move the work onto another thread, it can no longer directly access the user interface (hence the cross-thread call). You need to use Control.Invoke() or Control.BeginInvoke() to pass control back to the UI thread to ask it to update the UI for you.
Jason Williams
That's why the `BackgroundWorker` class is nice; it raises events on the UI thread so no `Invoke()` is necessary.
CodeSavvyGeek
+1  A: 

You can use this kind of pattern:-

    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);
        });
    }
AdamRalph
Thanks for your answer. Where to call Control.Invoke()? I get an error:Cross-thread operation not valid: Control 'dataGridView' accessed from a thread other than the thread it was created on.
Jooj
I've converted the code Winforms. Note that you have to manipulate the gridview in the UI thread too, since it is a UI element. This will eliminate the runtime exception you are seeing.
AdamRalph
Adam, what is Action in this code? I got an error.
Jooj
http://msdn.microsoft.com/en-us/library/system.action.aspx
AdamRalph
ah - I see you are using .NET 2.0 - ok, I'll edit the answer
AdamRalph
I have changed my framework to 3.5. Now it works. Thanks Adam!
Jooj
+2  A: 

There is no universal 'best' way to thread work. You just have to try different ways of doing things, I'm afraid.

I particularly like Jeremy D. Miller's continuation idea described at this page (scroll down to find the "continuations" section). It's really elegant and means writing very little boilerplate code.

Basically, when you call "ExecuteWithContinuation" with a Func argument, the function is executed asynchronously, then returns an action when it finishes. The action is then marshalled back onto your UI thread to act as a continuation. This allows you to quickly split your operations into two bits:

  1. Perform long running operation that shouldn't block the UI
  2. ... when finished, update the UI on the UI thread

It takes a bit of getting used to, but it's pretty cool.

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);
                                         });
    }
}

You'd then use it like this (forgive the formatting...)

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();
                                    };
                    });
}
Mark Simpson
Thank you for the answer. How can I use it without lambda in .NET 2.0?
Jooj
Mark now I'm on .NET 3.5. I got the error:Using the generic type 'System.Action<T>' requires '1' type arguments. What to do?
Jooj
"Action<T>" is the generic action that has one parameter; the code I posted just uses "Action" which has no parameters. It looks like you're trying to use Action<T> instead of Action. http://msdn.microsoft.com/en-us/library/system.action.aspx
Mark Simpson
Hey Mark. I tried your solution and it works great. Could you pleas show how to return some values from ExecuteWithContinuation section? Let's say I have DoSomethingThatTakesAgesAndNeedsToUpdateUiWhenFinished(int i) and want to change the variable. How to do it?
Jooj
Look on my last post...
Jooj
A: 

You cannot access your controls from the code that runs in the spun-off thread - the framework does not allow this, which explains the error you are getting.

You need to cache the data retrieved from the db in a non-forms object and populate your UI with data from that object after the background worker thread is done (and handle synchronization for access to that object).

Mikkel Christensen
Actually, the `CrossThreadException` will only be thrown in debug mode.
CodeSavvyGeek
A: 
int test_i = 0;
DoSomethingThatTakesAgesAndNeedsToUpdateUiWhenFinished(test_i);
test_i <- still is 0 and not 3!!!

public void DoSomethingThatTakesAgesAndNeedsToUpdateUiWhenFinished(int i)
{
    DisableUi();
    m_commandExecutor.ExecuteWithContinuation(
                () =>
                    {
                        // this is the long-running bit
                        ConnectToServer();
                        i = 3; <-------------------------- 
                        // This is the continuation that will be run
                        // on the UI thread
                        return () =>
                                    {
                                        EnableUi();
                                    };
                    });
}

Why I can't set test_i to 3? I also tried ref and out, but it doesn't work.

Please help.

Jooj