TheHvidsten
TheHvidsten

Reputation: 4458

Executing dynamic list of Actions with parameters

I'm building a list of Actions based on some other data. Each action should do a call to a method, and the list of actions should be performed in parallel. I have the following code that works just fine for parameterless methods:

private void Execute() {

    List<Action> actions = new List<Action>();

    for (int i = 0; i < 5; i++) {
        actions.Add(new Action(DoSomething));
    }

    Parallel.Invoke(actions.ToArray());
}

private void DoSomething() {
    Console.WriteLine("Did something");
}

But how can I do something similar where the methods have parameters? The following does not work:

private void Execute() {
    List<Action<int>> actions = new List<Action<int>>();

    for (int i = 0; i < 5; i++) {
        actions.Add(new Action(DoSomething(i))); // This fails because I can't input the value to the action like this
    }

    Parallel.Invoke(actions.ToArray()); // This also fails because Invoke() expects Action[], not Action<T>[]
}

private void DoSomething(int value) {
    Console.WriteLine("Did something #" + value);
}

Upvotes: 2

Views: 7549

Answers (5)

D Stanley
D Stanley

Reputation: 152634

Just keep your actions variable as a List<Action> instead of a List<Action<int>> and both problems are solved:

private void Execute() {
    List<Action> actions = new List<Action>();

    for (int i = 0; i < 5; i++) {
        actions.Add(new Action(() => DoSomething(i)));         }

    Parallel.Invoke(actions.ToArray()); 
}

private void DoSomething(int value) {
    Console.WriteLine("Did something #" + value);
}

The reason you want Action is becuase you're not passing the parameter when the action is invoked - you're providing the parameter value as part of the delegate definition (note that the delegate parameter is changed to a lambda with no input parameters - () => DoSomething(i)), so it's an Action, not an Action<int>.

Upvotes: 7

Mong Zhu
Mong Zhu

Reputation: 23732

There is a pitfall that I ran into when using an indexed forloop with the creation of threads.

When giving the Parameter i directly into the DoSomething method the Output is:

Did something #5

Did something #5

Did something #5

Did something #5

Did something #5

It is probably not what one wants when using a Loop and changing the counting variable. But if you save it temporarily into a new variable like:

class Program
{

    private static void Execute()
    {
        List<Action> actions = new List<Action>();

        for (int i = 0; i < 5; i++)
        {
            // save it temporarily and pass the temp_var variable
            int tmp_var = i;
            actions.Add(new Action(() => DoSomething(tmp_var)));
        }

        Parallel.Invoke(actions.ToArray());
    }

    private static void DoSomething(int value)
    {
        Console.WriteLine("Did something #" + value);
    }


    static void Main(string[] args)
    {
        Execute();
        Console.ReadKey();
    }
}

you actually get the counting variable in its full Beauty:

Did something #0

Did something #2

Did something #3

Did something #1

Did something #4

apparently in C# the variable survives the Loop (I don't know where) and when the thread is executed the Compiler will jump to the line

actions.Add(new Action(() => DoSomething(i)));

and take the value of i which it had when the Loop ended! If you would use i to index a List or array it would always lead to an OutOfBoundsException ! This drove me mad for a week until I figured it out

Upvotes: 4

Krtti
Krtti

Reputation: 62

Mong Jhu answer is perfect, I wanted to show how Action<int> can be used for same output.

private static void Execute1()
        {
            List<Action> actions = new List<Action>();
            for (int i = 0; i < 5; i++)
            {
                actions.Add(new Action(() => DoSomething(i)));
            }
            Parallel.Invoke(actions.ToArray());
        }
        private static void Execute()
        {
            List<Action<int>> actions = new List<Action<int>>();
            for (int i = 0; i < 5; i++)
            {
                actions.Add(new Action<int>((x) => DoSomething(i)));
            }
            for (int i = 0; i < actions.Count; i++)
            {
                Parallel.Invoke(() => { actions[0](0); });
            }
        }

Upvotes: 0

Slai
Slai

Reputation: 22886

it seems like what you actually want is just:

private void Execute() {
    Parallel.For(0, 5, DoSomething);
}

private void DoSomething(int value) {
    Console.WriteLine("Did something #" + value);
}

Upvotes: 0

Sander Aernouts
Sander Aernouts

Reputation: 979

As j.i.h. pointed out, you need to use it in a Lamba. Change your snippet to this:

private void Execute()
{
    List<Action> actions = new List<Action>();

    for (int i = 0; i < 5; i++)
    {
        actions.Add(() => DoSomething(i));
    }

    Parallel.Invoke(actions.ToArray());
}

private void DoSomething(int value)
{
    Console.WriteLine("Did something #" + value);
}

You can omit new Action(() => DoSomething(i)); and directly pass the Lamba to List.Add like this List.Add(() => DoSomething(i));

Upvotes: 0

Related Questions