Tâche.Exécuter avec des paramètres?

87

Je travaille sur un projet de réseau multi-tâches et je suis nouveau Threading.Tasks. J'ai implémenté un simple Task.Factory.StartNew()et je me demande comment puis-je le faire avec Task.Run()?

Voici le code de base:

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

Je regardais dans System.Threading.Tasks.Taskdans l' Explorateur d'objets et je ne pouvais pas trouver un Action<T>paramètre comme. Il n'y a que Actioncela qui prend des voidparamètres et aucun type .

Il n'y a que 2 choses similaires: static Task Run(Action action)et static Task Run(Func<Task> function)mais je ne peux pas publier de paramètre (s) avec les deux.

Oui, je sais que je peux créer une méthode d'extension simple pour cela, mais ma principale question est de pouvoir l'écrire sur une seule ligne avec Task.Run()?

MFatihMAR
la source
On ne sait pas quelle sera la valeur du paramètre. D'où viendrait-il? Si vous l'avez déjà, capturez-le simplement dans l'expression lambda ...
Jon Skeet
@JonSkeet rawDataest un paquet de données réseau qui a une classe de conteneur (comme DataPacket) et je réutilise cette instance pour réduire la pression GC. Donc, si j'utilise rawDatadirectement dans Task, il peut (probablement) être changé avant de le Taskgérer. Maintenant, je pense que je peux créer une autre byte[]instance pour cela. Je pense que c'est la solution la plus simple pour moi.
MFatihMAR
Oui, si vous devez cloner le tableau d'octets, vous clonez le tableau d'octets. Avoir un Action<byte[]>ne change rien à cela.
Jon Skeet
Voici quelques bonnes solutions pour passer des paramètres à une tâche.
Just Shadow

Réponses:

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

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

Éditer

En raison de la demande générale, je dois noter que le Tasklancement fonctionnera en parallèle avec le thread appelant. En supposant que la valeur par défaut, TaskSchedulercela utilisera le .NET ThreadPool. Quoi qu'il en soit, cela signifie que vous devez tenir compte du ou des paramètres passés au Taskcomme étant potentiellement accessibles par plusieurs threads à la fois, ce qui en fait un état partagé. Cela inclut leur accès sur le fil appelant.

Dans mon code ci-dessus, ce cas est entièrement théorique. Les chaînes sont immuables. C'est pourquoi je les ai utilisés comme exemple. Mais disons que vous n'utilisez pas de String...

Une solution consiste à utiliser asyncet await. Ceci, par défaut, capturera le SynchronizationContextthread appelant et créera une continuation pour le reste de la méthode après l'appel à awaitet l'attachera au fichier Task. Si cette méthode s'exécute sur le thread d'interface graphique WinForms, elle sera de type WindowsFormsSynchronizationContext.

La continuation s'exécutera après avoir été publiée sur le capturé SynchronizationContext- encore une fois par défaut. Vous serez donc de retour sur le fil de discussion avec lequel vous avez commencé après l' awaitappel. Vous pouvez modifier cela de différentes manières, notamment en utilisant ConfigureAwait. En bref, le reste de cette méthode ne se poursuivra pas tant que le ne sera pas Taskterminé sur un autre thread. Mais le thread appelant continuera à s'exécuter en parallèle, mais pas le reste de la méthode.

Cette attente pour terminer l'exécution du reste de la méthode peut être souhaitable ou non. Si rien dans cette méthode n'accède plus tard aux paramètres passés à, Taskvous ne voudrez peut-être pas du tout utiliser await.

Ou peut-être que vous utiliserez ces paramètres beaucoup plus tard dans la méthode. Aucune raison de le faire awaitimmédiatement car vous pouvez continuer à travailler en toute sécurité. N'oubliez pas que vous pouvez stocker le Taskretourné dans une variable et awaitdessus plus tard - même dans la même méthode. Par exemple, une fois que vous avez besoin d'accéder aux paramètres passés en toute sécurité après avoir effectué un tas d'autres travaux. Encore une fois, vous n'avez pas besoin de awaitle faire sur la Taskdroite lorsque vous l'exécutez.

Quoi qu'il en soit, un moyen simple de rendre ce thread-safe par rapport aux paramètres passés à Task.Runest de le faire:

Vous devez d'abord décorer RunAsyncavec async:

private async void RunAsync()

Note importante

De préférence, la méthode marquée ne doit pas retourner void, comme le mentionne la documentation liée. L'exception courante à cela concerne les gestionnaires d'événements tels que les clics sur les boutons, etc. Ils doivent revenir nuls. Sinon, j'essaie toujours de retourner un ou lors de l'utilisation . C'est une bonne pratique pour plusieurs raisons.async TaskTask<TResult>async

Vous pouvez maintenant awaitexécuter la Taskméthode ci-dessous. Vous ne pouvez pas utiliser awaitsans 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

Donc, en général, si vous faites awaitla tâche, vous pouvez éviter de traiter les paramètres passés comme une ressource potentiellement partagée avec tous les pièges de la modification de quelque chose à partir de plusieurs threads à la fois. Méfiez-vous également des fermetures . Je ne les couvrirai pas en profondeur, mais l'article lié fait un excellent travail.

Note d'accompagnement

Un peu hors sujet, mais soyez prudent en utilisant n'importe quel type de "blocage" sur le thread de l'interface graphique WinForms car il est marqué par [STAThread]. L'utilisation awaitne bloquera pas du tout, mais je la vois parfois utilisée en conjonction avec une sorte de blocage.

"Bloquer" est entre guillemets car vous ne pouvez techniquement pas bloquer le thread d'interface graphique WinForms . Oui, si vous utilisez locksur le fil GUI WinForms , il va encore pomper des messages, malgré vous pensant qu'il est « bloqué ». Ce n'est pas.

Cela peut provoquer des problèmes bizarres dans de très rares cas. L'une des raisons pour lesquelles vous ne voulez jamais utiliser de lockpeinture, par exemple. Mais c'est un cas marginal et complexe; cependant je l'ai vu causer des problèmes insensés. Je l'ai donc noté par souci d'exhaustivité.

Zer0
la source
21
Vous n'attendez pas Task.Run(() => MethodWithParameter(param));. Ce qui signifie que s'il paramest modifié après le Task.Run, vous pourriez avoir des résultats inattendus sur le MethodWithParameter.
Alexandre Severino
7
Pourquoi est-ce une réponse acceptée quand elle est fausse. Ce n'est pas du tout l'équivalent de passer un objet d'état.
Egor Pavlikhin
6
@ Zer0 un objet d'état est le deuxième paramètre de Task.Factory.StartNew msdn.microsoft.com/en-us/library/dd321456(v=vs.110).aspx et il enregistre la valeur de l'objet au moment de la appel à StartNew, tandis que votre réponse crée une fermeture, qui conserve la référence (si la valeur de param change avant que la tâche ne soit exécutée, elle changera également dans la tâche), donc votre code n'est pas du tout équivalent à ce que la question posait . La réponse est vraiment qu'il n'y a aucun moyen de l'écrire avec Task.Run ().
Egor Pavlikhin
2
@ Zer0 pour les structures Task.Run avec fermeture et Task.Factory.StartNew avec le 2ème paramètre (qui n'est pas le même que Task.Run par votre lien) se comporteront différemment, puisque dans ce dernier cas une copie sera faite. Mon erreur a été de faire référence aux objets en général dans le commentaire original, ce que je voulais dire, c'est qu'ils ne sont pas totalement équivalents.
Egor Pavlikhin
3
En lisant l'article de Toub, je mettrai en évidence cette phrase "Vous pouvez utiliser des surcharges qui acceptent l'état d'objet, qui pour les chemins de code sensibles aux performances peuvent être utilisés pour éviter les fermetures et les allocations correspondantes". Je pense que c'est ce que @Zero implique lors de l'examen de Task.Run sur l'utilisation de StartNew.
davidcarr
35

Utilisez la capture de variables pour «transmettre» les paramètres.

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

Vous pouvez également utiliser rawDatadirectement mais vous devez faire attention, si vous changez la valeur de l' rawDataextérieur d'une tâche (par exemple un itérateur dans une forboucle) cela changera également la valeur à l'intérieur de la tâche.

Scott Chamberlain
la source
11
+1 pour prendre en compte le fait important que la variable peut être modifiée juste après l'appel Task.Run.
Alexandre Severino
1
comment cela va-t-il aider? si vous utilisez x à l'intérieur du thread de tâche, et x est une référence à un objet, et si l'objet est modifié en même temps lorsque le thread de tâche est en cours d'exécution, cela peut entraîner des ravages.
Ovi du
1
@ Ovi-WanKenobi Oui, mais ce n'est pas le sujet de cette question. C'était comment passer un paramètre. Si vous passiez une référence à un objet en tant que paramètre d'une fonction normale, vous auriez exactement le même problème là aussi.
Scott Chamberlain
Ouais, cela ne fonctionne pas. Ma tâche n'a aucune référence à x dans le thread appelant. Je suis juste nul.
David Price
7

À partir de maintenant, vous pouvez également:

Action<int> action = (o) => Thread.Sleep(o);
int param = 10;
await new TaskFactory().StartNew(action, param)
Arnaud F.
la source
C'est la meilleure réponse car elle permet de passer un état et empêche la situation possible évoquée dans la réponse de Kaden Burgart . Par exemple, si vous devez passer un IDisposableobjet dans le délégué de tâche pour résoudre l'avertissement ReSharper «La variable capturée est supprimée dans la portée externe» , cela le fait très bien. Contrairement à la croyance populaire, il n'y a rien de mal à utiliser Task.Factory.StartNewau lieu de l' Task.Runendroit où vous devez passer l'état. Regardez ici .
Néo
7

Je sais que c'est un vieux fil de discussion, mais je voulais partager une solution que j'ai fini par devoir utiliser car le message accepté a toujours un problème.

Le problème:

Comme l'a souligné Alexandre Severino, si param(dans la fonction ci-dessous) change peu de temps après l'appel de la fonction, vous pourriez avoir un comportement inattendu dans MethodWithParameter.

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

Ma solution:

Pour en tenir compte, j'ai fini par écrire quelque chose de plus comme la ligne de code suivante:

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

Cela m'a permis d'utiliser en toute sécurité le paramètre de manière asynchrone malgré le fait que le paramètre a changé très rapidement après le démarrage de la tâche (ce qui a causé des problèmes avec la solution publiée).

En utilisant cette approche, param(type valeur) obtient sa valeur transmise, donc même si la méthode asynchrone s'exécute après les parammodifications, elle paura la valeur que paramj'avais lorsque cette ligne de code a été exécutée.

Kaden Burgart
la source
5
J'attends avec impatience tous ceux qui peuvent penser à un moyen de le faire plus lisiblement avec moins de frais généraux. C'est certes plutôt moche.
Kaden Burgart
5
Ici vous allez:var localParam = param; await Task.Run(() => MethodWithParam(localParam));
Stephen Cleary
1
Ce dont Stephen a déjà parlé dans sa réponse il y a un an et demi.
Servy
1
@Servy: C'était la réponse de Scott , en fait. Je n'ai pas répondu à celui-ci.
Stephen Cleary
La réponse de Scott n'aurait pas fonctionné pour moi en fait, car je l'exécutais dans une boucle for. Le paramètre local aurait été réinitialisé lors de la prochaine itération. La différence dans la réponse que j'ai publiée est que le paramètre est copié dans la portée de l'expression lambda, de sorte que la variable est immédiatement sûre. Dans la réponse de Scott, le paramètre est toujours dans la même portée, il peut donc toujours changer entre l'appel de la ligne et l'exécution de la fonction async.
Kaden Burgart
5

Utilisez simplement Task.Run

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

Ou, si vous souhaitez l'utiliser dans une méthode et attendre la tâche plus tard

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;
}
Travis J
la source
1
Faites juste attention aux fermetures si vous le faites de cette façon, for(int rawData = 0; rawData < 10; ++rawData) { Task.Run(() => { Console.WriteLine(rawData); } ) }elles ne se comporteront pas comme si elles avaient rawDataété transmises comme dans l'exemple StartNew de l'OP.
Scott Chamberlain
@ScottChamberlain - Cela semble être un exemple différent;) J'espère que la plupart des gens comprendront la fermeture des valeurs lambda.
Travis J
3
Et si ces commentaires précédents n'avaient aucun sens, veuillez consulter le blog d'Eric Lipper sur le sujet: blogs.msdn.com/b/ericlippert/archive/2009/11/12/ ... Cela explique pourquoi cela se passe très bien.
Travis J
2

On ne sait pas si le problème d'origine était le même problème que moi: vouloir maximiser les threads du processeur sur le calcul à l'intérieur d'une boucle tout en préservant la valeur de l'itérateur et en restant en ligne pour éviter de passer une tonne de variables à une fonction de travail.

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

J'ai fait fonctionner cela en changeant l'itérateur externe et en localisant sa valeur avec une porte.

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();
}
Harald J
la source
0

L'idée est d'éviter d'utiliser un signal comme ci-dessus. Le pompage de valeurs int dans une structure empêche ces valeurs de changer (dans la structure). J'ai eu le problème suivant: loop var je changerais avant l'appel de DoSomething (i) (j'ai été incrémenté à la fin de la boucle avant () => DoSomething (i, i i)). Avec les structures, cela n'arrive plus. Nasty bug à trouver: DoSomething (i, i i) a l'air génial, mais jamais sûr s'il est appelé à chaque fois avec une valeur différente pour i (ou juste 100 fois avec i = 100), d'où -> 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
la source
1
Bien que cela puisse répondre à la question, il a été signalé pour examen. Les réponses sans explication sont souvent considérées comme de mauvaise qualité. Veuillez expliquer pourquoi c'est la bonne réponse.
Dan