Görev.Parametreler ile Çalıştırılsın mı?


88

Çok görevli bir ağ projesi üzerinde çalışıyorum ve bu konuda yeniyim Threading.Tasks. Basit bir uyguladım Task.Factory.StartNew()ve bunu nasıl yapabilirim acaba Task.Run()?

İşte temel kod:

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

Ben içine baktım System.Threading.Tasks.Taskiçinde nesne tarayıcı ve bir bulamadık Action<T>gibi parametre. Sadece parametre Actionalır voidve tür yoktur .

Sadece 2 benzer şey vardır: static Task Run(Action action)ve static Task Run(Func<Task> function)her ikisiyle parametre (ler) gönderemez.

Evet, bunun için basit bir uzantısı yöntemi oluşturabilir ancak biliyorum benim ana soru tek bir satırda yazabilirsin edilir ile Task.Run()?


Parametrenin değerinin ne olmasını istediğiniz belli değil . Nereden gelir? Zaten sahipseniz, onu lambda ifadesinde yakalayın ...
Jon Skeet

@JonSkeet rawData, kapsayıcı sınıfına (DataPacket gibi) sahip bir ağ veri paketidir ve bu örneği GC basıncını azaltmak için yeniden kullanıyorum. Yani, rawDatadoğrudan içinde kullanırsam Task, (muhtemelen) işlemeden önce değiştirilebilir Task. Şimdi, bunun için başka bir byte[]örnek oluşturabileceğimi düşünüyorum . Bunun benim için en basit çözüm olduğunu düşünüyorum.
MFatihMAR

Evet, bayt dizisini klonlamanız gerekiyorsa, bayt dizisini klonlarsınız. Sahip olmak Action<byte[]>bunu değiştirmez.
Jon Skeet

İşte bir göreve parametre aktarmak için bazı iyi çözümler .
Just Shadow

Yanıtlar:


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

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

Düzenle

Popüler talep nedeniyle, Taskbaşlatılanın arayan iş parçacığı ile paralel çalışacağını not etmeliyim . Varsayılanı varsayarsak TaskSchedulerbu .NET'i kullanacaktır ThreadPool. Her neyse, bu Task, potansiyel olarak aynı anda birden fazla iş parçacığı tarafından erişilen parametre (ler) i hesaba katmanız gerektiği anlamına gelir, bu da onları paylaşılan durum haline getirir. Bu, onlara çağrı iş parçacığı üzerinden erişmeyi içerir.

Yukarıdaki kodumda bu durum tamamen tartışmalı. Dizeler değişmezdir. Bu yüzden örnek olarak kullandım. Ama kullanmadığınızı söyleyin String...

Bir çözüm kullanmaktır asyncve await. Bu, varsayılan olarak, SynchronizationContextçağıran iş parçacığının yakalanmasını sağlar ve çağrıldıktan sonra yöntemin geri kalanı için bir devamlılık awaitoluşturur ve onu oluşturulana ekler Task. Bu yöntem WinForms GUI iş parçacığında çalışıyorsa, türünde olacaktır WindowsFormsSynchronizationContext.

Devam etme, yakalanan kişiye geri gönderildikten sonra çalıştırılacaktır SynchronizationContext- yine yalnızca varsayılan olarak. Böylece, awaitaramadan sonra başladığınız konuya geri döneceksiniz . Bunu, özellikle kullanarak çeşitli şekillerde değiştirebilirsiniz ConfigureAwait. Kısacası bu yöntemin geri kalanı kadar devam etmeyecek sonraTask başka iş parçacığı üzerinde tamamladı. Ancak çağıran iş parçacığı, yöntemin geri kalanı değil, paralel olarak çalışmaya devam edecektir.

Yöntemin geri kalanının tamamlanmasını beklemek istenebilir veya istenmeyebilir. Bu yöntemde hiçbir şey daha sonra Tasksize iletilen parametrelere erişmezse , hiç kullanmak istemeyebilirsiniz await.

Ya da belki bu parametreleri yöntemde çok daha sonra kullanırsınız. awaitGüvenli bir şekilde çalışmaya devam edebileceğiniz için hemen gerek yok . Unutmayın, Taskdöndürülenleri bir değişkende ve awaitdaha sonra aynı yöntemde bile saklayabilirsiniz . Örneğin, bir sürü başka iş yaptıktan sonra aktarılan parametrelere güvenli bir şekilde erişmeniz gerektiğinde. Yine, do not gerekir awaitüzerinde Taskbunu çalıştırdığınızda sağ.

Her neyse, bu iş parçacığını, iletilen parametrelere göre güvenli hale getirmenin basit bir yolu Task.Run, bunu yapmaktır:

Önce RunAsyncşununla süslemelisin async:

private async void RunAsync()

Önemli Not

Bağlantılı belgelerde bahsedildiği gibi, işaretlenen yöntem tercihen geçersiz hale gelmemelidir . Bunun genel istisnası, düğme tıklamaları gibi olay işleyicileridir. Boşuna dönmeleri gerekir. Aksi takdirde her zaman a veya kullanırken dönmeye çalışırım . Birkaç nedenden dolayı iyi bir uygulamadır.async TaskTask<TResult>async

Şimdi aşağıdaki gibi awaitçalıştırabilirsiniz Task. awaitOnsuz kullanamazsınız 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

Bu nedenle, genel olarak, eğer awaitgörev sizseniz, parametrelerde iletilen bir şeyi aynı anda birden çok iş parçacığından değiştirmenin tüm tuzaklarına sahip, potansiyel olarak paylaşılan bir kaynak olarak ele almaktan kaçınabilirsiniz. Ayrıca, kapanışlara dikkat edin . Bunları derinlemesine ele almayacağım ama bağlantılı makale harika bir iş çıkarıyor.

Kenar notu

Biraz konu dışı, ancak ile işaretlendiğinden WinForms GUI iş parçacığında herhangi bir "engelleme" türü kullanırken dikkatli olun [STAThread]. Kullanmak awaithiç engellemez, ancak bazen bir tür engelleme ile birlikte kullanıldığını görüyorum.

"Blok" tırnak içinde çünkü teknik olarak WinForms GUI iş parçacığını engelleyemezsiniz . Evet, kullanmak durumunda lockWinForms GUI üzerinde o iplik olacak hala en "engellenmiş" düşüncesiyle rağmen iletileri pompalamak. Değil.

Bu, çok nadir durumlarda tuhaf sorunlara neden olabilir. lockÖrneğin, resim yaparken asla kullanmak istememenizin nedenlerinden biri . Ancak bu uç ve karmaşık bir durumdur; ancak bunun çılgın sorunlara neden olduğunu gördüm. Bu yüzden bütünlük uğruna not ettim.


23
Sen bekleyen değildir Task.Run(() => MethodWithParameter(param));. Hangi eğer vasıta paramolarak alır sonraTask.Run , size beklenmedik sonuçlar olabilir MethodWithParameter.
Alexandre Severino

8
Yanlış olduğunda bu neden kabul edilen bir cevap? Geçiş durum nesnesine hiç de eşdeğer değildir.
Egor Pavlikhin

7
@ Zer0 bir durum nesnesi, Task.Factory.StartNew msdn.microsoft.com/en-us/library/dd321456(v=vs.110).aspx içindeki ikinci parametredir ve nesnenin değerini o anda kaydeder. StartNew'e çağrı yapın, cevabınız referansı tutan bir kapanış yaratırken (görev çalıştırılmadan önce param'ın değeri değişirse görevde de değişir), bu nedenle kodunuz sorunun sorduğu şeye hiç denk değildir . Cevap gerçekten de bunu Task.Run () ile yazmanın bir yolu olmadığıdır.
Egor Pavlikhin

3
@ Zer0 Belki sen kaynak kodu okumalısınız. Biri durum nesnesine geçer, diğeri geçmez. Ben de baştan beri söyledim. Task.Run, Task.Factory.StartNew için kısa bir el değildir . Durum nesnesi sürümü eski nedenlerden dolayı oradadır, ancak hala oradadır ve bazen farklı davranır, bu nedenle insanlar bunun farkında olmalıdır.
Egor Pavlikhin

3
Toub'un makalesini okurken bu cümleyi vurgulayacağım "Nesne durumunu kabul eden aşırı yüklemeleri kullanacaksınız, performansa duyarlı kod yolları için kapanmaları ve karşılık gelen tahsisleri önlemek için kullanılabilir". Sanırım @ Zero'nun Task.Run üzerinde StartNew kullanımı düşünüldüğünde ima ettiği şey bu.
davidcarr

34

Parametreleri "geçirmek" için değişken yakalamayı kullanın.

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

rawDataDoğrudan da kullanabilirsiniz, ancak dikkatli olmalısınız, rawDatabir görevin dışındaki değeri değiştirirseniz (örneğin bir fordöngüdeki bir yineleyici ), görevin içindeki değeri de değiştirecektir.


11
Değişkenin arandıktan hemen sonra değiştirilebileceği gerçeğini dikkate almak için +1 Task.Run.
Alexandre Severino

1
bu nasıl yardımcı olacak? x'i görev dizisi içinde kullanırsanız ve x bir nesneye başvuruyorsa ve nesne, görev iş parçacığı çalışırken aynı anda değiştirilirse, hasara yol açabilir.
Ovi

1
@ Ovi-WanKenobi Evet, ancak bu sorunun konusu bu değildi. Bir parametrenin nasıl geçirileceğiydi. Normal bir işleve parametre olarak bir nesneye bir referans iletirseniz, orada da aynı sorunu yaşarsınız.
Scott Chamberlain

Evet, bu işe yaramıyor. Görevimin çağıran iş parçacığındaki x'e geri referansı yok. Sadece boş alıyorum.
David Price

Scott Chamberlain, Ele geçirme yoluyla tartışmayı geçmek kendi sorunlarıyla birlikte gelir. Özellikle bellek sızıntısı ve bellek baskısı sorunu var. Özellikle ölçek büyütmeye çalıştığınızda. (daha fazla ayrıntı için bkz. "Bellek Sızıntılarına Neden Olabileceğiniz 8 Yol").
RashadRivera

7

Şu andan itibaren şunları da yapabilirsiniz:

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

Bu en iyi cevaptır çünkü bir devletin geçmesine izin verir ve Kaden Burgart'ın cevabında bahsedilen olası durumu engeller . Örneğin IDisposable, ReSharper uyarısını çözmek için görev temsilcisine bir nesne iletmeniz gerekiyorsa, "Yakalanan değişken dış kapsamda düzenlenmiştir" , bu çok iyi yapar. Popüler inancın aksine, devleti geçmeniz gereken yer Task.Factory.StartNewyerine kullanmanın yanlış bir Task.Runtarafı yoktur. Buraya bakın .
Neo

7

Bunun eski bir konu olduğunu biliyorum, ancak kabul edilen gönderide hala bir sorun olduğu için kullanmak zorunda kaldığım bir çözümü paylaşmak istedim.

Sorun:

Alexandre Severino'nun belirttiği gibi, eğer param(aşağıdaki fonksiyonda) fonksiyon çağrısından kısa bir süre sonra değişirse, bazı beklenmedik davranışlarla karşılaşabilirsiniz MethodWithParameter.

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

Çözümüm:

Bunu hesaba katmak için, aşağıdaki kod satırına daha benzer bir şey yazdım:

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

Bu, parametrenin göreve başladıktan sonra çok hızlı değişmesine (bu da yayınlanan çözümle ilgili sorunlara neden olduğu) rağmen, parametreyi eşzamansız olarak güvenle kullanmamı sağladı.

Bu yaklaşımı kullanarak, param(değer türü) değerini iletilir, bu nedenle zaman uyumsuz yöntem paramdeğişikliklerden sonra çalışsa bile, bu kod satırı çalıştığında psahip olduğu değere paramsahip olacaktır .


5
Bunu daha okunaklı ve daha az masrafla yapmanın bir yolunu düşünen herkesi sabırsızlıkla bekliyorum. Bu kuşkusuz oldukça çirkin.
Kaden Burgart

5
İşte gidiyorsun:var localParam = param; await Task.Run(() => MethodWithParam(localParam));
Stephen Cleary

1
Bu arada, Stephen bir buçuk yıl önce cevabında zaten tartışmıştı.
2016

1
@Servy: Aslında Scott'ın cevabı buydu . Buna cevap vermedim.
Stephen Cleary

Bunu bir for döngüsünde çalıştırdığım için Scott'ın cevabı aslında benim için işe yaramazdı. Yerel param bir sonraki yinelemede sıfırlanmış olacaktı. Gönderdiğim cevaptaki fark, paramın lambda ifadesinin kapsamına kopyalanmasıdır, bu nedenle değişken hemen güvenlidir. Scott'ın cevabında, parametre hala aynı kapsamdadır, bu nedenle hattı çağırmakla zaman uyumsuz işlevi yürütmek arasında hala değişebilir.
Kaden Burgart

5

Sadece Task.Run kullanın

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

Veya bir yöntemde kullanmak ve görevi daha sonra beklerseniz

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
Sadece kapatmalara dikkat edin, bu şekilde yaparsanız , OP'nin StartNew örneğindeki gibi geçirilmiş gibi for(int rawData = 0; rawData < 10; ++rawData) { Task.Run(() => { Console.WriteLine(rawData); } ) }davranmaz rawData.
Scott Chamberlain

@ScottChamberlain - Bu farklı bir örnek gibi görünüyor;) Umarım çoğu insan lambda değerlerini kapatmayı anlar.
Travis J

3
Ve bu önceki yorumların bir anlamı yoksa, lütfen Eric Lipper'ın konuyla ilgili bloguna bakın: blogs.msdn.com/b/ericlippert/archive/2009/11/12/… Bunun neden çok iyi olduğunu açıklıyor.
Travis J

2

Orijinal sorunun benim sahip olduğum sorunla aynı olup olmadığı belli değil: yineleyicinin değerini korurken bir döngü içinde hesaplamada CPU iş parçacığını en üst düzeye çıkarmak istemek ve bir ton değişkeni bir çalışan işlevine geçirmekten kaçınmak için satır içi tutmak.

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

Bunu, dış yineleyiciyi değiştirerek ve değerini bir kapı ile yerelleştirerek çalıştırdım.

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

Fikir, yukarıdaki gibi bir Sinyal kullanmaktan kaçınmaktır. İnt değerlerini bir yapıya pompalamak, bu değerlerin (yapı içinde) değişmesini engeller. Aşağıdaki Problemi yaşadım: döngü var i DoSomething (i) çağrılmadan önce değişirdi (döngünün sonunda () => DoSomething (i, i) çağrılmadan önce artırıldı ). Yapılar ile artık gerçekleşmiyor. Bulmak için kötü hata: DoSomething (i, i i) harika görünüyor, ancak her seferinde i için farklı bir değerle (veya i = 100 ile sadece 100 kez) çağrılıp çağrılmadığından asla emin olun, bu nedenle -> 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
Bu, soruyu yanıtlayabilir ancak incelenmek üzere işaretlenmiştir. Açıklaması olmayan cevaplar genellikle düşük kaliteli olarak kabul edilir. Lütfen bunun neden doğru cevap olduğuna dair biraz yorum yapın.
Dan
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.