Завдання. Запустити з параметрами?


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параметр і не типу .

Існує лише дві подібні речі: static Task Run(Action action)і, static Task Run(Func<Task> function)але не можна розміщувати параметри з обома.

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


Незрозуміло, яким би ви хотіли бути значення параметра. Звідки це взялося б? Якщо у вас це вже є, просто зафіксуйте його в лямбда-виразі ...
Джон Скіт

@JonSkeet rawData- це мережевий пакет даних, який має клас контейнера (наприклад, DataPacket), і я повторно використовую цей екземпляр для зменшення тиску ГХ. Отже, якщо я використовую 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()

Важлива примітка

Переважно, щоб метод, позначений, не повертав недійсним, як згадується у пов'язаній документації. Загальним винятком з цього є обробники подій, такі як клацання кнопок тощо. Вони повинні повернутися недійсними. В іншому випадку я завжди намагаюся повернути a або при використанні . Це хороша практика з багатьох причин.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, тоді як ваша відповідь створює закриття, яке зберігає посилання (якщо значення param зміниться до запуску завдання, воно також зміниться в завданні), тому ваш код зовсім не еквівалентний запитуванню . Відповідь насправді полягає в тому, що немає можливості написати це за допомогою Task.Run ().
Єгор Павлихін

2
@ Zer0 для структур Task.Run із закриттям і Task.Factory.StartNew з 2-м параметром (що не є таким самим, як Task.Run за вашим посиланням) буде поводитися інакше, оскільки в останньому випадку буде зроблена копія. Моя помилка полягала у посиланні на об’єкти загалом в оригінальному коментарі, маючи на увазі те, що вони не є повністю рівнозначними.
Єгор Павліхін

3
Читаючи статтю Туба, я виділю це речення "Ви можете використовувати перевантаження, які приймають стан об’єкта, які для чутливих до продуктивності шляхів коду можна використовувати, щоб уникнути закриття та відповідних розподілів". Я думаю, що це має на увазі @Zero, розглядаючи Task.Run над використанням StartNew.
davidcarr

35

Використовуйте змінну для захоплення для передачі параметрів.

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

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


11
+1 за врахування важливого факту, що змінна може бути змінена відразу після виклику Task.Run.
Александр Северино

1
як це допоможе? якщо ви використовуєте x всередині потоку завдань, і x є посиланням на об'єкт, і якщо об'єкт модифікується одночасно, коли запущений потік завдання, це може призвести до хаосу.
Ові,

1
@ Ovi-WanKenobi Так, але це не те, про що йшлося в цьому питанні. Це як передавати параметр. Якби ви передали посилання на об'єкт як параметр звичайній функції, у вас теж була б така сама проблема.
Скотт Чемберлен

Так, це не працює. Моє завдання не має посилання на х у виклику потоку. Я просто отримую нуль.
Девід Прайс

7

Відтепер ви також можете:

Action<int> action = (o) => Thread.Sleep(o);
int param = 10;
await new TaskFactory().StartNew(action, param)

Це найкраща відповідь, оскільки вона дозволяє пройти штат і запобігає можливій ситуації, згаданій у відповіді Кадена Бургарта . Наприклад, якщо вам потрібно передати IDisposableоб'єкт в делегат завдання, щоб вирішити попередження ReSharper "Захоплена змінна розміщена у зовнішній області" , це робить це дуже гарно. Всупереч поширеній думці, немає нічого поганого у використанні Task.Factory.StartNewзамість того, Task.Runде потрібно пройти стан. Дивіться тут .
Нео,

7

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

Питання:

Як зазначив Олександр Северіно, якщо param(у функції нижче) зміниться незабаром після виклику функції, ви можете отримати несподівану поведінку в MethodWithParameter.

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

Моє рішення:

Щоб пояснити це, я в підсумку написав щось на зразок наступного рядка коду:

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

Це дозволило мені безпечно використовувати параметр асинхронно, незважаючи на те, що параметр дуже швидко змінився після запуску завдання (що спричинило проблеми з розміщеним рішенням).

Використовуючи цей підхід, param(тип значення) передає своє значення, тому, навіть якщо метод асинхронізації запускається після paramзмін, pбуде мати те значення, яке paramбуло на момент запуску цього рядка коду.


5
Я з нетерпінням чекаю кожного, хто зможе придумати спосіб зробити це більш розбірливо і з меншими витратами. Це, правда, досить потворно.
Каден Бургарт

5
Ось:var localParam = param; await Task.Run(() => MethodWithParam(localParam));
Стівен Клірі

1
Що, до речі, Стівен вже обговорював у своїй відповіді, півтора року тому.
Серві

1
@ Серві: Насправді це була відповідь Скотта . Я не відповів на це.
Стівен Клірі

Відповідь Скотта не спрацював би для мене насправді, оскільки я запускав це у циклі for. Місцевий параметр буде скинутий під час наступної ітерації. Різниця у відповіді, яку я розмістив, полягає в тому, що параметр копіюється в область виразу лямбда-сигналу, тому змінна відразу безпечна. У відповіді Скотта параметр все ще знаходиться в тій же області, тому він все ще може змінюватися між викликом лінії та виконанням функції асинхронізації.
Каден Бургарт,

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було передано, як у прикладі StartNew OP.
Скотт Чемберлен

@ScottChamberlain - Це схоже на інший приклад;) Я б сподівався, що більшість людей розуміють про закриття над лямбда-значеннями.
Тревіс Дж

3
І якщо ці попередні коментарі не мали сенсу, перегляньте блог Еріка Ліппера на тему: blogs.msdn.com/b/ericlippert/archive/2009/11/12/… Це пояснює, чому це відбувається дуже добре.
Тревіс Дж

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 у структуру заважає цим значенням змінюватися (у структурі). У мене була така проблема: цикл var я змінювався до виклику DoSomething (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.