Task.Run with Parameter (s)?

87

Pracuję nad wielozadaniowym projektem sieciowym i jestem nowy Threading.Tasks. Zaimplementowałem prosty Task.Factory.StartNew()i zastanawiam się, jak mogę to zrobić Task.Run()?

Oto podstawowy kod:

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

Spojrzałem System.Threading.Tasks.Taskw Object Browser i nie mogłem znaleźć Action<T>takiego parametru. Jest tylko Actiontaki, który przyjmuje voidparametr, a nie typ .

Są tylko dwie podobne rzeczy: static Task Run(Action action)i static Task Run(Func<Task> function)ale nie można wysyłać parametrów z obydwoma.

Tak, wiem, że mogę stworzyć dla niego prostą metodę rozszerzenia, ale moje główne pytanie brzmi: czy możemy napisać to w jednej linii za pomocą Task.Run()?

MFatihMAR
źródło
Nie jest jasne, jaka ma być wartość parametru. Skąd by się to wzięło? Jeśli już to masz, po prostu zarejestruj to w wyrażeniu lambda ...
Jon Skeet
@JonSkeet rawDatato sieciowy pakiet danych, który ma klasę kontenera (np. DataPacket) i ponownie używam tej instancji, aby zmniejszyć ciśnienie GC. Tak więc, jeśli używam rawDatabezpośrednio w Task, można to (prawdopodobnie) zmienić przed Taskobsługą. Teraz myślę, że mogę stworzyć byte[]dla niego inną instancję. Myślę, że to dla mnie najprostsze rozwiązanie.
MFatihMAR
Tak, jeśli chcesz sklonować tablicę bajtów, klonujesz tablicę bajtów. Posiadanie Action<byte[]>tego nie zmienia.
Jon Skeet
Oto kilka dobrych rozwiązań przekazywania parametrów do zadania.
Just Shadow

Odpowiedzi:

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

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

Edytować

Ze względu na popularne zapotrzebowanie muszę zauważyć, że Taskuruchomiony będzie działał równolegle z wątkiem wywołującym. Zakładając, że domyślnie TaskSchedulerbędzie używany .NET ThreadPool. W każdym razie oznacza to, że musisz wziąć pod uwagę wszelkie parametry przekazywane do elementu Taskjako potencjalnie dostępne dla wielu wątków jednocześnie, co czyni je współdzielonymi. Obejmuje to dostęp do nich w wątku wywołującym.

W moim powyższym kodzie sprawa ta jest całkowicie dyskusyjna. Ciągi znaków są niezmienne. Dlatego użyłem ich jako przykładu. Ale powiedz, że nie używasz String...

Jednym z rozwiązań jest użycie asynci await. To domyślnie spowoduje przechwycenie SynchronizationContextwątku wywołującego i utworzy kontynuację dla pozostałej części metody po wywołaniu awaiti dołączeniu go do utworzonego Task. Jeśli ta metoda jest uruchomiona w wątku GUI WinForms, będzie to typ WindowsFormsSynchronizationContext.

Kontynuacja zostanie uruchomiona po wysłaniu z powrotem do przechwyconych SynchronizationContext- ponownie tylko domyślnie. Więc po awaitrozmowie wrócisz do wątku, od którego zacząłeś . Możesz to zmienić na różne sposoby, w szczególności za pomocą ConfigureAwait. W skrócie, reszta tej metody nie będą kontynuowane aż poTask zakończył w innym wątku. Ale wątek wywołujący będzie nadal działał równolegle, ale nie reszta metody.

To oczekiwanie na zakończenie wykonywania pozostałej części metody może być pożądane lub nie. Jeśli nic w tej metodzie później nie uzyskuje dostępu do parametrów przekazanych do Task, możesz w ogóle nie chcieć ich używać await.

A może użyjesz tych parametrów znacznie później w metodzie. Nie ma powodu, by awaitod razu kontynuować pracę. Pamiętaj, że możesz przechowywać Taskzwracaną wartość w zmiennej i awaitna niej później - nawet w ten sam sposób. Na przykład, gdy potrzebujesz bezpiecznego dostępu do przekazanych parametrów po wykonaniu kilku innych czynności. Ponownie, nie musisz awaitwchodzić w Taskprawo, gdy go uruchamiasz.

W każdym razie prostym sposobem na zapewnienie bezpieczeństwa wątku w odniesieniu do parametrów przekazywanych do Task.Runjest wykonanie tego:

Najpierw trzeba ozdobić RunAsyncz async:

private async void RunAsync()

Ważna uwaga

Najlepiej, aby oznaczona metoda nie zwracała wartości void, jak wspomniano w powiązanej dokumentacji. Typowym wyjątkiem są programy obsługi zdarzeń, takie jak kliknięcia przycisków i tym podobne. Muszą wrócić nieważne. W przeciwnym razie zawsze staram się zwrócić lub podczas używania . Jest to dobra praktyka z kilku powodów.async TaskTask<TResult>async

Teraz możesz awaituruchomić Taskjak poniżej. Nie możesz używać awaitbez 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

Tak więc, ogólnie rzecz biorąc, jeśli wykonujesz awaitzadanie, możesz uniknąć traktowania przekazanych w parametrach jako potencjalnie współdzielonego zasobu ze wszystkimi pułapkami modyfikowania czegoś z wielu wątków jednocześnie. Uważaj także na zamknięcia . Nie będę ich szczegółowo omawiać, ale artykuł, do którego prowadzi łącze, świetnie sobie z tym radzi.

Dygresja

Trochę poza tematem, ale bądź ostrożny przy używaniu wszelkiego rodzaju "blokowania" wątku GUI WinForms, ponieważ jest on oznaczony [STAThread]. Używanie w awaitogóle nie blokuje, ale czasami widzę, że jest używane w połączeniu z jakimś rodzajem blokowania.

„Blok” jest w cudzysłowie, ponieważ technicznie nie można zablokować wątku GUI WinForms . Tak, jeśli używasz lockw wątku GUI WinForms, nadal będzie on pompował komunikaty, mimo że myślisz, że jest „zablokowany”. To nie jest.

W bardzo rzadkich przypadkach może to powodować dziwne problemy. Na przykład jeden z powodów, dla których nigdy nie chcesz używać a lockpodczas malowania. Ale to jest marginalna i złożona sprawa; jednak widziałem, że powoduje to szalone problemy. Więc zanotowałem to dla kompletności.

Zer0
źródło
21
Nie czekasz Task.Run(() => MethodWithParameter(param));. Co oznacza, że ​​jeśli paramzostanie zmodyfikowany po rozszerzeniu Task.Run, możesz mieć nieoczekiwane wyniki na MethodWithParameter.
Alexandre Severino
7
Dlaczego jest to akceptowana odpowiedź, kiedy jest błędna. Nie jest to wcale odpowiednik przekazywania obiektu stanu.
Egor Pavlikhin
6
@ Zer0 obiekt stanu jest drugim parametrem w Task.Factory.StartNew msdn.microsoft.com/en-us/library/dd321456(v=vs.110).aspx i zapisuje wartość obiektu w momencie wywołanie StartNew, podczas gdy twoja odpowiedź tworzy zamknięcie, które zachowuje referencję (jeśli wartość parametru zmieni się przed uruchomieniem zadania, zmieni się również w zadaniu), więc twój kod w ogóle nie jest równoważny temu, o co pytano . Odpowiedź naprawdę jest taka, że ​​nie ma sposobu, aby napisać go za pomocą Task.Run ().
Egor Pavlikhin
2
@ Zer0 na elemencie Task.Run z zamknięciem i Task.Factory.StartNew z 2 parametru (co nie jest takie samo jak Task.Run za link) będzie zachowywać się inaczej, ponieważ w tym drugim przypadku będzie kopią. Mój błąd polegał na tym, że w pierwotnym komentarzu odnosiłem się ogólnie do przedmiotów, miałem na myśli to, że nie są one w pełni równoważne.
Egor Pavlikhin
3
Czytając artykuł Touba, podkreślę to zdanie: „Używasz przeciążeń akceptujących stan obiektu, co w przypadku ścieżek kodu wrażliwych na wydajność można wykorzystać do uniknięcia zamknięć i odpowiadających im alokacji”. Myślę, że to właśnie sugeruje @Zero, rozważając użycie Task.Run over StartNew.
davidcarr
35

Użyj zmiennej przechwytywania, aby „przekazać” parametry.

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

Możesz również użyć rawDatabezpośrednio, ale musisz być ostrożny, jeśli zmienisz wartość rawDatapoza zadaniem (na przykład iterator w forpętli), zmieni to również wartość wewnątrz zadania.

Scott Chamberlain
źródło
11
+1 za wzięcie pod uwagę ważnego faktu, że zmienna może zostać zmieniona zaraz po wywołaniu Task.Run.
Alexandre Severino
1
jak to pomoże? jeśli użyjesz x wewnątrz wątku zadania, a x jest odwołaniem do obiektu, a jeśli obiekt zostanie zmodyfikowany w tym samym czasie, gdy działa wątek zadania, może to doprowadzić do spustoszenia.
Ovi,
1
@ Ovi-WanKenobi Tak, ale nie o to chodziło w tym pytaniu. To było jak przekazać parametr. Jeśli przekazałeś referencję do obiektu jako parametr do normalnej funkcji, miałbyś dokładnie ten sam problem.
Scott Chamberlain
Tak, to nie działa. Moje zadanie nie ma odniesienia z powrotem do x w wątku wywołującym. Po prostu tracę wartość.
David Price
7

Od teraz możesz również:

Action<int> action = (o) => Thread.Sleep(o);
int param = 10;
await new TaskFactory().StartNew(action, param)
Arnaud F.
źródło
To najlepsza odpowiedź, ponieważ umożliwia przekazanie stanu i zapobiega możliwej sytuacji wspomnianej w odpowiedzi Kadena Burgarta . Na przykład, jeśli musisz przekazać IDisposableobiekt do delegata zadania, aby rozwiązać ostrzeżenie ReSharper „Przechwycona zmienna jest umieszczana w zewnętrznym zasięgu” , robi to bardzo dobrze. Wbrew powszechnemu przekonaniu nie ma nic złego w używaniu Task.Factory.StartNewzamiast tego, Task.Rungdzie musisz przejść stan. Zobacz tutaj .
Neo
7

Wiem, że to stary wątek, ale chciałem udostępnić rozwiązanie, z którego musiałem skorzystać, ponieważ zaakceptowany post nadal zawiera problem.

Problem:

Jak zauważył Alexandre Severino, jeśli param(w poniższej funkcji) zmieni się wkrótce po wywołaniu funkcji, możesz uzyskać nieoczekiwane zachowanie w programie MethodWithParameter.

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

Moje rozwiązanie:

Aby to wyjaśnić, napisałem coś bardziej podobnego do następującego wiersza kodu:

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

Pozwoliło mi to bezpiecznie korzystać z parametru asynchronicznie, mimo że parametr zmieniał się bardzo szybko po uruchomieniu zadania (co powodowało problemy z opublikowanym rozwiązaniem).

Korzystając z tego podejścia, param(typ wartości) pobiera swoją wartość przekazaną, więc nawet jeśli metoda asynchroniczna jest uruchamiana po paramzmianach, pbędzie miała dowolną wartość, paramjaką miał, gdy ten wiersz kodu został uruchomiony.

Kaden Burgart
źródło
5
Z niecierpliwością czekam na każdego, kto może wymyślić sposób, aby zrobić to bardziej czytelnie przy mniejszych kosztach. To jest dość brzydkie.
Kaden Burgart
5
Proszę bardzo:var localParam = param; await Task.Run(() => MethodWithParam(localParam));
Stephen Cleary
1
O czym zresztą Stephen omawiał już w swojej odpowiedzi półtora roku temu.
Servy
1
@Servy: Właściwie to była odpowiedź Scotta . Nie odpowiedziałem na to.
Stephen Cleary
Odpowiedź Scotta właściwie nie zadziałałaby dla mnie, ponieważ uruchamiałem to w pętli for. Parametr lokalny zostałby zresetowany w następnej iteracji. Różnica w odpowiedzi, którą zamieściłem, polega na tym, że parametr zostaje skopiowany do zakresu wyrażenia lambda, więc zmienna jest natychmiast bezpieczna. W odpowiedzi Scotta parametr nadal znajduje się w tym samym zakresie, więc nadal może się zmieniać między wywołaniem linii a wykonaniem funkcji asynchronicznej.
Kaden Burgart
5

Po prostu użyj Task.Run

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

Lub, jeśli chcesz użyć go w metodzie i poczekaj na zadanie później

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.
źródło
1
Po prostu uważaj na zamknięcia, jeśli zrobisz to w ten sposób for(int rawData = 0; rawData < 10; ++rawData) { Task.Run(() => { Console.WriteLine(rawData); } ) }, nie będą zachowywać się tak samo, jak gdyby rawDatazostało przekazane, jak w przykładzie StartNew OP.
Scott Chamberlain
@ScottChamberlain - To wydaje się być innym przykładem;) Mam nadzieję, że większość ludzi rozumie zamykanie wartości lambda.
Travis J
3
A jeśli te poprzednie komentarze nie miały sensu, zajrzyj na blog Erica Lippera na ten temat: blogs.msdn.com/b/ericlippert/archive/2009/11/12/ ... To wyjaśnia, dlaczego dzieje się to bardzo dobrze.
Travis J
2

Nie jest jasne, czy pierwotny problem był tym samym problemem, który miałem: chęć maksymalnego zwiększenia liczby wątków procesora podczas obliczeń wewnątrz pętli, przy jednoczesnym zachowaniu wartości iteratora i zachowaniu inline, aby uniknąć przekazywania tony zmiennych do funkcji roboczej.

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

Mam to do pracy, zmieniając zewnętrzny iterator i lokalizując jego wartość za pomocą bramki.

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.
źródło
0

Pomysł polega na unikaniu używania sygnału jak powyżej. Pompowanie wartości int do struktury zapobiega zmianie tych wartości (w strukturze). Miałem następujący problem: zmienna pętla zmieniłabym się przed wywołaniem DoSomething (i) (i został zwiększony na końcu pętli przed wywołaniem () => DoSomething (i, i i)). Ze strukturami to się już nie zdarza. Paskudny błąd do znalezienia: DoSomething (i, i i) wygląda świetnie, ale nigdy nie jestem pewien, czy zostanie wywołany za każdym razem z inną wartością dla i (lub tylko 100 razy z i = 100), stąd -> 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
źródło
1
Chociaż może to stanowić odpowiedź na pytanie, zostało zgłoszone do sprawdzenia. Odpowiedzi bez wyjaśnienia są często uważane za niskiej jakości. Proszę o komentarz, dlaczego jest to prawidłowa odpowiedź.
Dan,