Я изучаю TPL на этой странице, и один блок кода меня смущает.
Я читал эту страницу: Задача Parallelism (параллельная библиотека задач)
в одном разделе, он сказал, что следующий код является правильным решением, потому что лямбда в цикле не может получить значение, поскольку оно мутирует после каждой итерации, но окончательное значение. Поэтому вы должны обернуть "i" в объект CustomData. Код ниже:
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);
}
}
Код довольно прост и понятен, но здесь возникает моя проблема:
в методе Task.Factory.StartNew() автор использует одну из своих форм перегрузки:
Task.StartNew(Action<Object>, Object)
Мне так любопытно узнать , откуда происходит "obj"? Как у него есть 3 свойства: имя, CreationTime и ThreadNum.
Я искал через MSDN не нашел никакой полезной детали, но это: "Объект, содержащий данные, которые будут использоваться делегатом действия". MSDN Это ничего не объясняет.
Может ли кто-нибудь объяснить это?
Вот более краткий пример, который может помочь объяснить его.
void StartNew(Action<object> action, object o) {
action(o);
}
Метод StartNew
просто принимает делегат action
и вызывает его, передавая o
в качестве параметра. Значение, переданное лямбда - это просто значение, которое передается в StartNew
после лямбда
// Prints "hello world"
StartNew(o => Console.WriteLine(o), "hello world");
В случае, если вы указали переданное значение, поскольку второй параметр
new CustomData() {Name = i, CreationTime = DateTime.Now.Ticks}
Это просто создает новый объект типа CustomData
, присваивает ему некоторые свойства и делает его аргументом лямбда, определенным непосредственно перед ним. Это в конечном итоге станет значением obj
в lambda
Это стандартный шаблон для передачи непрозрачного объекта состояния для обратного вызова, он используется во многих других местах в .NET framework. Более простой пример: SendOrPostCallback
:
SynchronizationContext.Current.Post(state =>
MessageBox.Show(state.ToString()), state: "Hello");
В дальнейшем будет вызываться lambda с обратным вызовом типа SendOrPostCallback
с именем "Hello" как state
.
Параметр state
может использоваться как оптимизация, но это не обязательно, ни здесь, ни для Task.Factory.StartNew
, либо в большинстве других случаев предоставляется аргумент state
.
Ваша лямбда - это замыкание, которое имеет доступ к локальным переменным внешней области видимости, поэтому следующее приводит к идентичному результату, не передавая явно state
:
var message = "Hello";
SynchronizationContext.Current.Post(_ =>
MessageBox.Show(message), state: null);
То же самое относится к Task.Factory.StartNew
. Для этой цели Task.Factory.StartNew
предоставляет набор переопределений, которые принимают не общее действие Action
, а не Action<object>
.
Итак, ваш код может выглядеть следующим образом: IMO гораздо читабельнее:
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);
});
}