Question

I'm learning the TPL on this page, and one code block confuses me a lot.

I was reading this page: Task Parallelism (Task Parallel Library)

in one section, it said that the following code is the right solution because a lambda in a loop can't get the value as it mutates after each iteration, but the final value. So you should wrap the "i" in a CustomData object. The code is below:

class CustomData
{
   public long CreationTime;
   public int Name; 
   public int ThreadNum;
}

public class Example
{
   public static void Main()
   {
      // Create the task object by using an Action(Of Object) to pass in custom data 
      // to the Task constructor. This is useful when you need to capture outer variables 
      // from within a loop. 
      Task[] taskArray = new Task[10];
      for (int i = 0; i < taskArray.Length; i++)
      {
         taskArray[i] = Task.Factory.StartNew( (Object obj ) =>
            {
               CustomData data = obj as CustomData;
               if (data == null) 
                  return;

               data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
               Console.WriteLine("Task #{0} created at {1} on thread #{2}.", data.Name, data.CreationTime, data.ThreadNum);
            },
            new CustomData()
            {
               Name = i,
               CreationTime = DateTime.Now.Ticks
            });
      }
      Task.WaitAll(taskArray);     
   }
}

The code is rather straightforward and easy to understand but here comes my problem:

in the Task.Factory.StartNew() method, the author uses one of its overload form:

Task.StartNew(Action<Object>, Object)

I am so curious to know where does the "obj" come from? How does it have 3 properties: Name, CreationTime and ThreadNum.

I searched over MSDN found no useful detail but this: "An object containing data to be used by the action delegate." MSDN It really doesn't explain anything.

Could anyone explain it?

Was it helpful?

Solution

Here is a more concise example which may help to explain it.

void StartNew(Action<object> action, object o) {
  action(o);
}

The StartNew method just takes the action delegate and invokes it by passing o as the parameter. The value passed to the lambda is simply the value that is passed into StartNew after the lambda

// Prints "hello world"
StartNew(o => Console.WriteLine(o), "hello world");

In the case you outlined the value being passed as the second parameter is

new CustomData() {Name = i, CreationTime = DateTime.Now.Ticks} 

This just creates a new object of type CustomData, assigns it some properties and makes it the argument to the lambda defined immediately before it. It will eventually become the value obj in the lambda

OTHER TIPS

This is a standard pattern to pass an opaque state object to a callback, it is used in many other places in .NET framework. A more simple example, SendOrPostCallback:

SynchronizationContext.Current.Post(state => 
    MessageBox.Show(state.ToString()), state: "Hello");

The callback lambda of type SendOrPostCallback will be invoked in the future with "Hello" as state.

The state parameter can be used as optimization, but it is not really necessary, neither here nor for Task.Factory.StartNew or in most other cases a state argument is provided.

Your lambda is a closure which has access to the outer scope local variables, so the following produces the identical result, without passing the state explicitly:

var message = "Hello";
SynchronizationContext.Current.Post(_ => 
    MessageBox.Show(message), state: null);

The same applies to Task.Factory.StartNew. For this purpose, Task.Factory.StartNew provides a set of overrides which accept non-generic action Action, rather than Action<object>.

So, your code might look like this, which is IMO much more readable:

  for (int i = 0; i < taskArray.Length; i++) {
     var data = new CustomData() {Name = i, CreationTime = DateTime.Now.Ticks};

     taskArray[i] = Task.Factory.StartNew(() => 
     {
         if (data == null) 
           return;

         data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
         Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
             data.Name, data.CreationTime, data.ThreadNum);
     });
  }
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top