web-dev-qa-db-fra.com

Task.Run avec paramètre (s)?

Je travaille sur un projet de réseau multitâche et je suis nouveau sur 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);

J'ai regardé dans System.Threading.Tasks.Task dans Navigateur d'objets et je ne pouvais pas trouver un paramètre semblable à Action<T>. Il n'y a que Action qui prend le paramètre void et aucun type .

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

Oui, je sais que je peux créer une méthode d'extension simple pour celle-ci mais ma question principale est: pouvons-nous l'écrire sur une seule ligne avec Task.Run()?

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

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

Modifier

En raison de la demande populaire, je dois noter que le Task lancé s'exécutera en parallèle avec le thread appelant. En supposant que TaskScheduler par défaut, cela utilisera le .NET ThreadPool. Quoi qu'il en soit, cela signifie que vous devez prendre en compte le ou les paramètres transmis à Task comme potentiellement accessibles par plusieurs threads à la fois, ce qui en fait un état partagé. Cela inclut leur accès sur le fil d'appel.

Dans mon code ci-dessus, cette affaire est entièrement théorique. Les cordes sont immuables. C'est pourquoi je les ai utilisés comme exemple. Mais disons que vous n'utilisez pas un String...

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

La suite sera exécutée après avoir été postée sur SynchronizationContext capturé - à nouveau uniquement par défaut. Vous serez donc de retour sur le fil que vous avez commencé après l'appel await. Vous pouvez changer cela de différentes manières, notamment en utilisant ConfigureAwait . En bref, le reste de cette méthode ne continuera pas avant après la Task terminée 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 d’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 transmis à Task, vous pouvez ne pas vouloir utiliser await du tout.

Ou peut-être que vous utiliserez ces paramètres beaucoup plus tard dans la méthode. Aucune raison de await immédiatement car vous pouvez continuer à travailler en toute sécurité. N'oubliez pas que vous pouvez stocker le Task renvoyé dans une variable et le await dessus plus tard, même de la même manière. Par exemple, une fois que vous devez accéder aux paramètres transmis en toute sécurité après avoir effectué un autre travail. Encore une fois, vous n'avez pas besoin de await sur le Task à droite lorsque vous l'exécutez.

Quoi qu'il en soit, un moyen simple de rendre ce thread sûr en ce qui concerne les paramètres passés à Task.Run consiste à procéder comme suit:

Vous devez d’abord décorer RunAsync avec async:

private async void RunAsync()

Remarque importante

De préférence, la méthode marquée async ne doit pas renvoyer une valeur nulle comme le mentionne la documentation liée. Les gestionnaires d'événements tels que les clics sur les boutons, etc., constituent une exception à cette règle. Ils doivent retourner nuls. Sinon, j'essaie toujours de renvoyer un Task ou Task<TResult> lorsque j'utilise async. C'est un bon entraînement pour plusieurs raisons.

Maintenant, vous pouvez await exécuter Task comme ci-dessous. Vous ne pouvez pas utiliser await sans 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 await la tâche, vous pouvez éviter de traiter les paramètres transmis comme une ressource potentiellement partagée avec tous les pièges de la modification de quelque chose à partir de plusieurs threads à la fois. Aussi, méfiez-vous de fermetures . Je ne couvrirai pas ceux-ci en profondeur mais l'article lié fait un excellent travail.

Note latérale

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

"Block" est entre guillemets car techniquement, vous ne pouvez pas bloquer le thread d'interface graphique WinForms . Oui, si vous utilisez lock sur le thread d'interface graphique WinForms, il continuera à pomper les messages, même si vous pensez qu'il est "bloqué". Ce n'est pas.

Cela peut provoquer des problèmes étranges dans de très rares cas. Une des raisons pour lesquelles vous ne voulez jamais utiliser un lock en peignant, par exemple. Mais c'est un cas marginal et complexe; Cependant, je l'ai vu causer des problèmes fous. Je l'ai donc noté par souci d'exhaustivité.

87
Zer0

Utilisez la capture de variable pour "transmettre" les paramètres.

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

Vous pouvez également utiliser rawData directement, mais vous devez faire attention. Si vous modifiez la valeur de rawData en dehors d'une tâche (par exemple, un itérateur dans une boucle for, il modifiera également valeur à l'intérieur de la tâche.

26

Je sais que c'est un vieux fil de discussion, mais je voulais partager une solution que j'ai finalement dû utiliser car le message accepté pose 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 risquez d'obtenir un comportement inattendu dans MethodWithParameter.

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

Ma solution:

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

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

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

En utilisant cette approche, param (type de valeur) obtient sa valeur transmise. Ainsi, même si la méthode asynchrone s'exécute après que param ait changé, p aura la valeur que param aurait quand cette ligne de code a couru.

5
Kaden Burgart

A partir de maintenant vous pouvez aussi:

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

Il suffit d'utiliser 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;
}
3
Travis J

Il est difficile de savoir si le problème initial était identique à celui que j'avais: vouloir maximiser le nombre de threads de processeur lors du calcul dans une boucle tout en préservant la valeur de l'itérateur et rester en ligne pour éviter de transmettre 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 réussi à changer 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();
}
1
Harald J