Compito: eseguire con i parametri?


88

Sto lavorando a un progetto di rete multi-tasking e sono nuovo Threading.Tasks. Ho implementato un semplice Task.Factory.StartNew()e mi chiedo come posso farlo Task.Run()?

Ecco il codice di base:

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

Ho esaminato System.Threading.Tasks.Tasknel browser degli oggetti e non sono riuscito a trovare un Action<T>parametro simile. C'è solo Actionche accetta voidparametri e nessun tipo .

Ci sono solo 2 cose simili: static Task Run(Action action)e static Task Run(Func<Task> function)ma non è possibile pubblicare parametri con entrambi.

Sì, so che posso creare un semplice metodo di estensione per esso, ma la mia domanda principale è: possiamo scriverlo su una riga con Task.Run()?


Non è chiaro quale sia il valore del parametro che vorresti fosse. Da dove verrebbe? Se lo hai già, catturalo nell'espressione lambda ...
Jon Skeet

@JonSkeet rawDataè un pacchetto di dati di rete che ha una classe contenitore (come DataPacket) e sto riutilizzando questa istanza per ridurre la pressione del GC. Quindi, se lo uso rawDatadirettamente in Task, può (probabilmente) essere modificato prima di Taskgestirlo. Ora, penso di poter creare un'altra byte[]istanza per questo. Penso che sia la soluzione più semplice per me.
MFatihMAR

Sì, se è necessario clonare l'array di byte, si clona l'array di byte. Avere un Action<byte[]>non cambia questo.
Jon Skeet

Ecco alcune buone soluzioni per passare parametri a un'attività.
Just Shadow

Risposte:


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

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

modificare

A grande richiesta devo notare che il Tasklancio verrà eseguito in parallelo con il thread chiamante. Assumendo l'impostazione predefinita, TaskSchedulerverrà utilizzato .NET ThreadPool. Ad ogni modo, questo significa che è necessario tenere conto di qualsiasi parametro passato a Taskcome potenzialmente accessibile da più thread contemporaneamente, rendendoli condivisi. Ciò include accedervi sul thread chiamante.

Nel mio codice sopra quel caso è del tutto discutibile. Le stringhe sono immutabili. Ecco perché li ho usati come esempio. Ma dì che non stai usando un String...

Una soluzione è usare asynce await. Questo, per impostazione predefinita, catturerà il SynchronizationContextthread chiamante e creerà una continuazione per il resto del metodo dopo la chiamata a awaite lo collegherà al creatoTask . Se questo metodo è in esecuzione sul thread della GUI WinForms, sarà di tipo WindowsFormsSynchronizationContext.

La continuazione verrà eseguita dopo essere stata postata di nuovo nell'acquisizione SynchronizationContext, di nuovo solo per impostazione predefinita. Quindi tornerai sul thread con cui hai iniziato dopo la awaitchiamata. Puoi cambiarlo in vari modi, in particolare usando ConfigureAwait. In breve, il resto di quel metodo non continuerà fino a dopo il Taskha completato su un altro thread. Ma il thread chiamante continuerà a essere eseguito in parallelo, ma non il resto del metodo.

Questa attesa per completare l'esecuzione del resto del metodo può o non può essere desiderabile. Se nulla in quel metodo accede successivamente ai parametri passati al fileTask potresti non volerlo utilizzare awaitaffatto.

O forse usi questi parametri molto più avanti nel metodo. Non c'è motivo di farlo awaitimmediatamente perché potresti continuare a lavorare in sicurezza. Ricorda, puoi memorizzare il Taskrestituito in una variabile e awaitsu di essa successivamente, anche con lo stesso metodo. Ad esempio, una volta che è necessario accedere in modo sicuro ai parametri passati dopo aver svolto un po 'di altro lavoro. Anche in questo caso, non non è necessario awaitsulla Taskdestra quando lo si esegue.

Ad ogni modo, un modo semplice per rendere questo thread-safe rispetto ai parametri passati Task.Runè farlo:

Devi prima decorare RunAsynccon async:

private async void RunAsync()

Nota importante

Preferibilmente il metodo contrassegnato non dovrebbe restituire nullo, come menzionato nella documentazione collegata. L'eccezione comune a questo è gestori di eventi come i clic sui pulsanti e simili. Devono tornare nulli. Altrimenti cerco sempre di restituire un o quando uso . È una buona pratica per diversi motivi.async TaskTask<TResult>async

Ora puoi awaiteseguire il Tasksimile di seguito. Non puoi usare awaitsenza 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

Quindi, in generale, se si awaitesegue l'attività, è possibile evitare di trattare i parametri passati come una risorsa potenzialmente condivisa con tutte le insidie ​​di modificare qualcosa da più thread contemporaneamente. Inoltre, attenzione alle chiusure . Non li tratterò in modo approfondito, ma l'articolo collegato fa un ottimo lavoro.

Nota a margine

Un po 'fuori tema, ma fai attenzione a usare qualsiasi tipo di "blocco" sul thread della GUI di WinForms perché è contrassegnato con [STAThread]. Utilizzandoawait non bloccherà affatto, ma a volte lo vedo usato insieme a una sorta di blocco.

"Block" è tra virgolette perché tecnicamente non è possibile bloccare il thread della GUI di WinForms . Sì, se usi lockil thread della GUI di WinForms continuerà a pompare messaggi, nonostante tu pensi che sia "bloccato". Non è.

Ciò può causare problemi bizzarri in casi molto rari. Uno dei motivi per cui non vuoi mai usare un lockquando dipingi, per esempio. Ma questo è un caso marginale e complesso; tuttavia l'ho visto causare problemi folli. Quindi l'ho annotato per completezza.


23
Non stai aspettando Task.Run(() => MethodWithParameter(param));. Ciò significa che se paramviene modificato dopo il Task.Run, potresti avere risultati imprevisti nel file MethodWithParameter.
Alexandre Severino

8
Perché questa è una risposta accettata quando è sbagliata. Non è affatto equivalente al passaggio di un oggetto di stato.
Egor Pavlikhin

7
@ Zer0 un oggetto di stato è il secondo paremeter in Task.Factory.StartNew msdn.microsoft.com/en-us/library/dd321456(v=vs.110).aspx e salva il valore dell'oggetto al momento della chiama a StartNew, mentre la tua risposta crea una chiusura, che mantiene il riferimento (se il valore di param cambia prima che l'attività venga eseguita cambierà anche nell'attività), quindi il tuo codice non è affatto equivalente a ciò che la domanda stava chiedendo . La risposta è davvero che non c'è modo di scriverlo con Task.Run ().
Egor Pavlikhin

3
@ Zer0 Forse si dovrebbe leggere il codice sorgente. Uno passa l'oggetto di stato, l'altro no. Che è quello che ho detto dall'inizio. Task.Run non è un'abbreviazione per Task.Factory.StartNew. La versione dell'oggetto di stato esiste per motivi legacy, ma è ancora lì e a volte si comporta in modo diverso, quindi le persone dovrebbero esserne consapevoli.
Egor Pavlikhin

3
Leggendo l'articolo di Toub evidenzierò questa frase "Puoi usare overload che accettano lo stato dell'oggetto, che per percorsi di codice sensibili alle prestazioni possono essere usati per evitare chiusure e le allocazioni corrispondenti". Penso che questo sia ciò che @Zero sta implicando quando si considera l'utilizzo di Task.Run su StartNew.
davidcarr

34

Usa la cattura delle variabili per "passare" i parametri.

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

Puoi anche usarlo rawDatadirettamente ma devi stare attento, se modifichi il valore di rawDataal di fuori di un'attività (ad esempio un iteratore in un forciclo) cambierà anche il valore all'interno dell'attività.


11
+1 per prendere in considerazione il fatto importante che la variabile potrebbe essere cambiata subito dopo la chiamata Task.Run.
Alexandre Severino

1
come questo aiuterà? se si utilizza x all'interno del thread dell'attività e x è un riferimento a un oggetto, e se l'oggetto viene modificato nello stesso momento in cui il thread dell'attività è in esecuzione, può causare il caos.
Ovi

1
@ Ovi-WanKenobi Sì, ma non era di questo che si trattava. Era come passare un parametro. Se passaste un riferimento a un oggetto come parametro di una normale funzione avreste lo stesso identico problema anche lì.
Scott Chamberlain

Sì, questo non funziona. Il mio compito non ha alcun riferimento a x nel thread chiamante. Ottengo solo null.
David Price

Scott Chamberlain, Il passaggio delle argomentazioni tramite l'acquisizione viene fornito con i suoi problemi. In particolare, c'è il problema della perdita di memoria e della pressione della memoria. In particolare quando provi a scalare. (vedere "8 modi per causare perdite di memoria" per maggiori dettagli).
RashadRivera

7

Da ora puoi anche:

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

Questa è la risposta migliore in quanto consente il passaggio di uno stato e impedisce la possibile situazione menzionata nella risposta di Kaden Burgart . Ad esempio, se è necessario passare un IDisposableoggetto al delegato dell'attività per risolvere l'avviso ReSharper "La variabile acquisita è disposta nell'ambito esterno" , questo lo fa molto bene. Contrariamente alla credenza popolare, non c'è niente di sbagliato nell'usare Task.Factory.StartNewinvece di Task.Rundove devi passare lo stato. Vedi qui .
Neo

7

So che questo è un vecchio thread, ma volevo condividere una soluzione che ho finito per dover usare poiché il post accettato ha ancora un problema.

Il problema:

Come sottolineato da Alexandre Severino, se param(nella funzione sotto) cambia poco dopo la chiamata della funzione, potresti ottenere un comportamento imprevisto in MethodWithParameter.

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

La mia soluzione:

Per tenere conto di ciò, ho finito per scrivere qualcosa di più simile alla seguente riga di codice:

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

Ciò mi ha permesso di utilizzare il parametro in modo sicuro in modo asincrono nonostante il fatto che il parametro sia cambiato molto rapidamente dopo l'avvio dell'attività (che ha causato problemi con la soluzione pubblicata).

Utilizzando questo approccio, param(tipo di valore) ottiene il suo valore passato, quindi anche se il metodo asincrono viene eseguito dopo le parammodifiche, pavrà qualunque valore paramaveva quando è stata eseguita questa riga di codice.


5
Attendo con impazienza chiunque riesca a pensare a un modo per farlo in modo più leggibile con meno spese generali. Questo è certamente piuttosto brutto.
Kaden Burgart

5
Ecco qui:var localParam = param; await Task.Run(() => MethodWithParam(localParam));
Stephen Cleary

1
Cosa che, a proposito, Stephen ha già discusso nella sua risposta, un anno e mezzo fa.
Servy

1
@Servy: Questa è stata la risposta di Scott , in realtà. Non ho risposto a questo.
Stephen Cleary

La risposta di Scott non avrebbe funzionato per me in realtà, dato che lo stavo eseguendo in un ciclo for. Il parametro locale sarebbe stato ripristinato nella successiva iterazione. La differenza nella risposta che ho pubblicato è che il parametro viene copiato nell'ambito dell'espressione lambda, quindi la variabile è immediatamente sicura. Nella risposta di Scott, il parametro è ancora nello stesso ambito, quindi potrebbe ancora cambiare tra la chiamata della linea e l'esecuzione della funzione asincrona.
Kaden Burgart

5

Usa semplicemente Task.Run

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

Oppure, se desideri utilizzarlo in un metodo e attendere l'attività in un secondo momento

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
Fai solo attenzione alle chiusure se lo fai in questo modo for(int rawData = 0; rawData < 10; ++rawData) { Task.Run(() => { Console.WriteLine(rawData); } ) }non si comporterà come se rawDatafosse passato come nell'esempio StartNew dell'OP.
Scott Chamberlain

@ScottChamberlain - Sembra un esempio diverso;) Spero che la maggior parte delle persone capisca la chiusura sui valori lambda.
Travis J

3
E se quei commenti precedenti non avevano senso, per favore guarda il blog di Eric Lipper sull'argomento: blogs.msdn.com/b/ericlippert/archive/2009/11/12/… Spiega perché questo accade molto bene.
Travis J

2

Non è chiaro se il problema originale fosse lo stesso problema che ho avuto: voler massimizzare i thread della CPU sul calcolo all'interno di un ciclo preservando il valore dell'iteratore e rimanere in linea per evitare di passare una tonnellata di variabili a una funzione di lavoro.

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

L'ho fatto funzionare cambiando l'iteratore esterno e localizzando il suo valore con un cancello.

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

L'idea è evitare di utilizzare un segnale come sopra. Il pompaggio di valori int in una struttura impedisce la modifica di tali valori (nella struttura). Ho avuto il seguente problema: loop var i cambierebbe prima che DoSomething (i) fosse chiamato (i è stato incrementato alla fine del loop prima che () => DoSomething (i, i i) fosse chiamato). Con gli struct non succede più. Bug sgradevole da trovare: DoSomething (i, i i) sembra fantastico, ma non sono mai sicuro che venga chiamato ogni volta con un valore diverso per i (o solo 100 volte con i = 100), quindi -> 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
Sebbene questo possa rispondere alla domanda, è stato contrassegnato per la revisione. Le risposte senza spiegazione sono spesso considerate di bassa qualità. Fornisci qualche commento sul motivo per cui questa è la risposta corretta.
Dan
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.