Reputation: 3769
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?
Upvotes: 3
Views: 4658
Reputation: 61744
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);
});
}
Upvotes: 3
Reputation: 755587
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
Upvotes: 5