Task.Run с параметрами?

87

Я работаю над многозадачным сетевым проектом, и я новичок Threading.Tasks. Я реализовал простой, Task.Factory.StartNew()и мне интересно, как мне это сделать Task.Run()?

Вот базовый код:

Task.Factory.StartNew(new Action<object>(
(x) =>
{
    // Do something with 'x'
}), rawData);

Я заглянул System.Threading.Tasks.Taskв обозревателе объектов , и я не мог найти , Action<T>как параметр. Есть только Actionто, что принимает voidпараметр, а не тип .

Есть только 2 похожие вещи: static Task Run(Action action)и, static Task Run(Func<Task> function)но нельзя опубликовать параметры с обоими.

Да, я знаю, что могу создать для него простой метод расширения, но мой главный вопрос в том, можем ли мы написать его в одной строке с помощью Task.Run()?

MFatihMAR
источник
Непонятно, каким должно быть значение параметра. Откуда это взялось? Если он у вас уже есть, просто запишите его в лямбда-выражение ...
Джон Скит,
@JonSkeet rawData- это сетевой пакет данных, который имеет контейнерный класс (например, DataPacket), и я повторно использую этот экземпляр, чтобы уменьшить давление GC. Итак, если я использую rawDataнапрямую Task, его можно (вероятно) изменить до того, как Taskобработать его. Теперь, думаю, я могу создать byte[]для него еще один экземпляр. Думаю, для меня это самое простое решение.
MFatihMAR
Да, если вам нужно клонировать массив байтов, вы клонируете массив байтов. Наличие Action<byte[]>не меняет этого.
Джон Скит,
Вот несколько хороших решений для передачи параметров задаче.
Just Shadow

Ответы:

116
private void RunAsync()
{
    string param = "Hi";
    Task.Run(() => MethodWithParameter(param));
}

private void MethodWithParameter(string param)
{
    //Do stuff
}

редактировать

По многочисленным просьбам я должен отметить, что Taskзапущенный будет работать параллельно с вызывающим потоком. Предполагая, что по умолчанию TaskSchedulerбудет использоваться .NET ThreadPool. В любом случае, это означает, что вам необходимо учитывать любой передаваемый параметр (ы) Taskкак потенциально доступный для нескольких потоков одновременно, делая их общим состоянием. Это включает доступ к ним в вызывающем потоке.

В моем приведенном выше коде этот случай совершенно спорный. Строки неизменны. Вот почему я использовал их в качестве примера. Но скажите, что вы не используете String...

Одно из решений - использовать asyncи await. Это, по умолчанию, захватывает SynchronizationContextвызывающий поток и создает продолжение для остальной части метода после вызова awaitи присоединяет его к созданному Task. Если этот метод выполняется в потоке графического интерфейса WinForms, он будет типа WindowsFormsSynchronizationContext.

Продолжение будет запущено после отправки обратно в захваченное SynchronizationContext- опять же только по умолчанию. Так что после awaitзвонка вы вернетесь к теме, с которой начали . Вы можете изменить это разными способами, в частности, используя ConfigureAwait. Короче говоря, остальная часть этого метода не будет продолжаться до тех пор , послеTask завершения другого потока. Но вызывающий поток будет продолжать работать параллельно, а не остальная часть метода.

Это ожидание завершения выполнения остальной части метода может быть или нежелательно. Если в дальнейшем ничто в этом методе не обращается к параметрам, переданным в, возможно, Taskвы вообще не захотите использовать await.

Или, может быть, вы используете эти параметры намного позже в методе. Нет причин awaitсразу же, так как вы можете спокойно продолжать работу. Помните, что вы можете сохранить Taskвозвращаемое значение в переменной, а awaitпозже - даже в том же методе. Например, когда вам нужно безопасно получить доступ к переданным параметрам после выполнения кучи другой работы. Опять же, вам не нужно делать awaitэто Taskсправа при запуске.

В любом случае, простой способ сделать это потокобезопасным по отношению к переданным параметрам Task.Run- это сделать следующее:

Вы должны сначала украсить RunAsyncс async:

private async void RunAsync()

Важная заметка

Желательно, чтобы отмеченный метод не возвращал void, как упоминается в связанной документации. Распространенным исключением из этого правила являются обработчики событий, такие как нажатие кнопки и т.п. Они должны вернуться в пустоту. В противном случае я всегда стараюсь вернуть или при использовании . Это хорошая практика по нескольким причинам.async TaskTask<TResult>async

Теперь вы можете awaitзапустить Taskкак показано ниже. Вы не можете использовать awaitбез него async.

await Task.Run(() => MethodWithParameter(param));
//Code here and below in the same method will not run until AFTER the above task has completed in one fashion or another

Итак, в целом, если вы awaitвыполняете задачу, вы можете избежать обработки переданных параметров как потенциально разделяемого ресурса со всеми подводными камнями изменения чего-либо сразу из нескольких потоков. Также остерегайтесь закрытия . Я не буду описывать их подробно, но связанная статья отлично справляется с этим.

Примечание

Немного не по теме, но будьте осторожны при использовании любого типа «блокировки» потока графического интерфейса WinForms из-за того, что он помечен значком [STAThread]. Использование awaitне будет блокировать вообще, но иногда я вижу, что оно используется в сочетании с какой-то блокировкой.

«Блокировать» заключено в кавычки, потому что вы технически не можете заблокировать поток графического интерфейса WinForms . Да, если вы используете lockпоток графического интерфейса WinForms, он все равно будет перекачивать сообщения, несмотря на то, что вы думаете, что он «заблокирован». Это не.

В очень редких случаях это может вызвать странные проблемы. Одна из причин, по которой вы никогда не захотите использовать lock, например, при рисовании. Но это крайний и сложный случай; однако я видел, как это вызывает сумасшедшие проблемы. Поэтому я отметил это для полноты картины.

Zer0
источник
21
Вы не ждете Task.Run(() => MethodWithParameter(param));. Это означает , что если paramмодифицируется послеTask.Run , вы могли бы иметь неожиданные результаты на MethodWithParameter.
Александр Северино
7
Почему это принятый ответ, когда он неверен. Это совсем не эквивалент передачи объекта состояния.
Егор Павлихин
6
@ Zer0 объект состояния является вторым параметром в Task.Factory.StartNew msdn.microsoft.com/en-us/library/dd321456(v=vs.110).aspx и сохраняет значение объекта на момент вызов StartNew, в то время как ваш ответ создает закрытие, которое сохраняет ссылку (если значение параметра изменяется до запуска задачи, оно также изменится в задаче), поэтому ваш код совсем не эквивалентен тому, что задавал вопрос . На самом деле ответ заключается в том, что невозможно написать это с помощью Task.Run ().
Егор Павлихин
2
@ Zer0 для структур Task.Run с закрытием и Task.Factory.StartNew со вторым параметром (который не совпадает с Task.Run по вашей ссылке) будут вести себя иначе, поскольку в последнем случае будет сделана копия. Моя ошибка заключалась в том, что в исходном комментарии я ссылался на объекты в целом, я имел в виду, что они не полностью эквивалентны.
Егор Павлихин
3
Читая статью Туба, я выделю это предложение: «Вы можете использовать перегрузки, которые принимают состояние объекта, которое для чувствительных к производительности путей кода можно использовать, чтобы избежать замыканий и соответствующих выделений». Я думаю, что это то, что подразумевает @Zero при рассмотрении использования Task.Run over StartNew.
davidcarr 05
35

Используйте захват переменных для «передачи» параметров.

var x = rawData;
Task.Run(() =>
{
    // Do something with 'x'
});

Вы также можете использовать rawDataнапрямую, но вы должны быть осторожны, если вы измените значение rawDataвне задачи (например, итератор в forцикле), это также изменит значение внутри задачи.

Скотт Чемберлен
источник
11
+1 за то, что принял во внимание тот важный факт, что переменная может быть изменена сразу после вызова Task.Run.
Александр Северино
1
как это поможет? если вы используете x внутри потока задачи, а x является ссылкой на объект, и если объект изменяется в то же время, когда выполняется поток задачи, это может привести к хаосу.
Ovi
1
@ Ovi-WanKenobi Да, но вопрос был не об этом. Это было как передать параметр. Если вы передали ссылку на объект в качестве параметра нормальной функции, у вас возникла бы такая же проблема.
Скотт Чемберлен
Ага, это не работает. Моя задача не имеет обратной ссылки на x в вызывающем потоке. Я просто получаю ноль.
Дэвид Прайс
7

Теперь вы также можете:

Action<int> action = (o) => Thread.Sleep(o);
int param = 10;
await new TaskFactory().StartNew(action, param)
Арно Ф.
источник
Это лучший ответ, поскольку он позволяет передать состояние и предотвращает возможную ситуацию, упомянутую в ответе Кадена Бургарта . Например, если вам нужно передать IDisposableобъект делегату задачи, чтобы разрешить предупреждение ReSharper «Захваченная переменная расположена во внешней области» , это очень удобно. Вопреки распространенному мнению, нет ничего плохого в использовании Task.Factory.StartNewвместо того, Task.Runгде вам нужно передать состояние. Смотрите здесь .
Neo,
7

Я знаю, что это старый поток, но я хотел поделиться решением, которое мне пришлось использовать, поскольку в принятом сообщении все еще есть проблема.

Проблема:

Как указал Александр Северино, если param(в функции ниже) изменится вскоре после вызова функции, вы можете получить неожиданное поведение в MethodWithParameter.

Task.Run(() => MethodWithParameter(param)); 

Мое решение:

Чтобы учесть это, я написал что-то вроде следующей строки кода:

(new Func<T, Task>(async (p) => await Task.Run(() => MethodWithParam(p)))).Invoke(param);

Это позволило мне безопасно использовать параметр асинхронно, несмотря на то, что параметр изменился очень быстро после запуска задачи (что вызвало проблемы с опубликованным решением).

Используя этот подход, param(тип значения) получает свое значение, передаваемое, поэтому даже если метод async запускается после paramизменений, он pбудет иметь то же значение, paramчто и при выполнении этой строки кода.

Каден Бургарт
источник
5
Я с нетерпением жду любого, кто сможет придумать способ сделать это более разборчиво и с меньшими затратами. По общему признанию, это довольно некрасиво.
Каден Бургарт,
5
Вот, пожалуйста:var localParam = param; await Task.Run(() => MethodWithParam(localParam));
Стивен Клири
1
Что, кстати, Стивен уже обсуждал в своем ответе полтора года назад.
Servy
1
@Servy: Собственно, это был ответ Скотта . Я не ответил на этот вопрос.
Стивен Клири
На самом деле ответ Скотта не сработал бы для меня, поскольку я запускал это в цикле for. Локальный параметр был бы сброшен на следующей итерации. Разница в ответе, который я опубликовал, заключается в том, что параметр копируется в область лямбда-выражения, поэтому переменная немедленно становится безопасной. В ответе Скотта параметр все еще находится в той же области, поэтому он все еще может меняться между вызовом строки и выполнением функции async.
Каден Бургарт,
5

Просто используйте Task.Run

var task = Task.Run(() =>
{
    //this will already share scope with rawData, no need to use a placeholder
});

Или, если вы хотите использовать его в методе и дождаться задачи позже

public Task<T> SomethingAsync<T>()
{
    var task = Task.Run(() =>
    {
        //presumably do something which takes a few ms here
        //this will share scope with any passed parameters in the method
        return default(T);
    });

    return task;
}
Трэвис Дж.
источник
1
Просто будьте осторожны с закрытием, если вы сделаете это таким образом, for(int rawData = 0; rawData < 10; ++rawData) { Task.Run(() => { Console.WriteLine(rawData); } ) }это не будет вести себя так, как если бы rawDataбыло передано, как в примере OP StartNew.
Скотт Чемберлен
@ScottChamberlain - Это похоже на другой пример;) Я надеюсь, что большинство людей понимают, что такое закрытие по лямбда-значениям.
Трэвис Дж,
3
И если эти предыдущие комментарии не имели смысла, см. Блог Эрика Липпера по этой теме: blogs.msdn.com/b/ericlippert/archive/2009/11/12/… Это объясняет, почему это происходит очень хорошо.
Travis J
2

Неясно, была ли исходная проблема той же проблемой, что и у меня: желание максимизировать потоки ЦП при вычислениях внутри цикла с сохранением значения итератора и сохранением встроенного, чтобы избежать передачи тонны переменных в рабочую функцию.

for (int i = 0; i < 300; i++)
{
    Task.Run(() => {
        var x = ComputeStuff(datavector, i); // value of i was incorrect
        var y = ComputeMoreStuff(x);
        // ...
    });
}

Я заставил это работать, изменив внешний итератор и локализовав его значение с помощью ворот.

for (int ii = 0; ii < 300; ii++)
{
    System.Threading.CountdownEvent handoff = new System.Threading.CountdownEvent(1);
    Task.Run(() => {
        int i = ii;
        handoff.Signal();

        var x = ComputeStuff(datavector, i);
        var y = ComputeMoreStuff(x);
        // ...

    });
    handoff.Wait();
}
Харальд Дж
источник
0

Идея состоит в том, чтобы избегать использования сигнала, подобного приведенному выше. Накачка значений типа int в структуру предотвращает изменение этих значений (в структуре). У меня была следующая проблема: переменная цикла, которую я должен был изменить до вызова DoSomething (i) (значение i было увеличено в конце цикла до вызова () => DoSomething (i, i i)). Со структурами этого больше не происходит. Неприятная ошибка, которую нужно найти: DoSomething (i, i i) выглядит великолепно, но никогда не уверен, будет ли он вызван каждый раз с другим значением для i (или просто 100 раз с i = 100), следовательно -> struct

struct Job { public int P1; public int P2; }
…
for (int i = 0; i < 100; i++) {
    var job = new Job { P1 = i, P2 = i * i}; // structs immutable...
    Task.Run(() => DoSomething(job));
}
CodeDigger
источник
1
Хотя это может дать ответ на вопрос, он был помечен для проверки. Ответы без объяснения часто считаются некачественными. Прокомментируйте, почему это правильный ответ.
Дэн