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()?


Непонятно, каким должно быть значение параметра. Откуда это взялось? Если он у вас уже есть, просто запишите его в лямбда-выражение ...
Джон Скит,

@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, например, при рисовании. Но это крайний и сложный случай; однако я видел, как это вызывает сумасшедшие проблемы. Поэтому я отметил это для полноты картины.


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));
}

1
Хотя это может дать ответ на вопрос, он был помечен для проверки. Ответы без объяснения часто считаются некачественными. Прокомментируйте, почему это правильный ответ.
Дэн
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.