Task.Run ด้วยพารามิเตอร์?


88

Threading.Tasksผมทำงานในโครงการเครือข่ายแบบมัลติทาสกิ้และฉันใหม่ ฉันใช้งานง่ายTask.Factory.StartNew()และฉันสงสัยว่าจะทำได้Task.Run()อย่างไร?

นี่คือรหัสพื้นฐาน:

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

ฉันตรวจสอบSystem.Threading.Tasks.TaskในObject Browserแล้วไม่พบAction<T>พารามิเตอร์ like มีเพียงActionที่ใช้voidพารามิเตอร์และไม่มีประเภท

มีเพียง 2 สิ่งที่คล้ายกัน: static Task Run(Action action)และstatic Task Run(Func<Task> function)แต่ไม่สามารถโพสต์พารามิเตอร์ด้วยทั้งสองอย่าง

ใช่ฉันรู้ว่าฉันสามารถสร้างวิธีการขยายแบบง่าย ๆ ได้ แต่คำถามหลักของฉันคือเราสามารถเขียนเป็นบรรทัดเดียวด้วยTask.Run()?


ไม่ชัดเจนว่าคุณต้องการให้ค่าของพารามิเตอร์เป็นเท่าใด มันมาจากไหน? หากคุณมีอยู่แล้วเพียงจับมันในนิพจน์แลมด้า ...
Jon Skeet

@JonSkeet rawDataเป็นแพ็กเก็ตข้อมูลเครือข่ายที่มีคลาสคอนเทนเนอร์ (เช่น DataPacket) และฉันใช้อินสแตนซ์นี้ซ้ำเพื่อลดแรงกดดันของ GC ดังนั้นถ้าฉันใช้rawDataในโดยตรงTaskมันสามารถ (อาจ) ถูกเปลี่ยนก่อนที่จะTaskจัดการ ตอนนี้ฉันคิดว่าฉันสามารถสร้างbyte[]อินสแตนซ์อื่นสำหรับมันได้ ฉันคิดว่ามันเป็นทางออกที่ง่ายที่สุดสำหรับฉัน
MFatihMAR

ใช่ถ้าคุณต้องการโคลนอาร์เรย์ไบต์คุณจะต้องโคลนอาร์เรย์ไบต์ การมีAction<byte[]>ไม่ได้เปลี่ยนสิ่งนั้น
Jon Skeet

ต่อไปนี้เป็นวิธีแก้ปัญหาที่ดีในการส่งผ่านพารามิเตอร์ไปยังงาน
Just Shadow

คำตอบ:


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

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

แก้ไข

เนื่องจากความต้องการที่เป็นที่นิยมฉันต้องทราบว่า Taskเปิดตัวจะทำงานควบคู่ไปกับเธรดการโทร สมมติว่าเริ่มต้นนี้จะใช้สุทธิTaskScheduler ThreadPoolอย่างไรก็ตามนี่หมายความว่าคุณต้องคำนึงถึงพารามิเตอร์ใด ๆ ที่ส่งผ่านไปยังTaskเธรดหลายเธรดที่อาจเข้าถึงได้พร้อมกันทำให้เป็นสถานะที่ใช้ร่วมกัน ซึ่งรวมถึงการเข้าถึงบนเธรดการโทร

ในรหัสด้านบนของฉันกรณีนี้ถูกสร้างขึ้นทั้งหมด สตริงไม่เปลี่ยนรูป นั่นเป็นเหตุผลที่ฉันใช้เป็นตัวอย่าง แต่บอกว่าคุณไม่ได้ใช้String...

ทางออกหนึ่งคือการใช้asyncและawait. โดยค่าเริ่มต้นจะจับSynchronizationContextเธรดการโทรและจะสร้างความต่อเนื่องสำหรับส่วนที่เหลือของวิธีการหลังจากการโทรไปที่awaitTaskและแนบไปกับที่สร้างขึ้น หากวิธีนี้ทำงานอยู่ใน WinForms GUI WindowsFormsSynchronizationContextที่ด้ายมันจะเป็นประเภท

ความต่อเนื่องจะทำงานหลังจากที่โพสต์กลับไปที่บันทึกSynchronizationContext- อีกครั้งตามค่าเริ่มต้นเท่านั้น ดังนั้นคุณจะกลับมาที่เธรดที่คุณเริ่มต้นหลังจากการawaitโทร ConfigureAwaitคุณสามารถเปลี่ยนได้ในความหลากหลายของรูปแบบที่สะดุดตาใช้ ในระยะสั้นส่วนที่เหลือของวิธีการที่จะไม่ดำเนินต่อไปจนถึงหลังTaskได้เสร็จสิ้นในหัวข้ออื่น แต่เธรดการเรียกจะยังคงทำงานแบบขนานไม่ใช่ส่วนที่เหลือของเมธอด

การรอเพื่อรันวิธีที่เหลือนี้ให้เสร็จสมบูรณ์อาจเป็นที่ต้องการหรือไม่ก็ได้ หากไม่มีสิ่งใดในวิธีการนั้นในภายหลังเข้าถึงพารามิเตอร์ที่ส่งไปยังTaskคุณอาจไม่ต้องการใช้awaitเลย

หรือบางทีคุณอาจใช้พารามิเตอร์เหล่านั้นในภายหลังในวิธีการ ไม่มีเหตุผลที่จะawaitทันทีที่คุณสามารถทำงานต่อไปได้อย่างปลอดภัย โปรดจำไว้ว่าคุณสามารถจัดเก็บสิ่งที่Taskส่งคืนในตัวแปรและเก็บawaitไว้ในภายหลังได้แม้จะใช้วิธีเดียวกันก็ตาม ตัวอย่างเช่นเมื่อคุณต้องการเข้าถึงพารามิเตอร์ที่ส่งผ่านอย่างปลอดภัยหลังจากทำงานอื่น ๆ อีกครั้งคุณไม่จำเป็นต้องawaitอยู่Taskทางด้านขวาเมื่อคุณเรียกใช้

อย่างไรก็ตามวิธีง่ายๆในการทำให้เธรดนี้ปลอดภัยโดยคำนึงถึงพารามิเตอร์ที่ส่งไปให้Task.Runคือการทำสิ่งนี้:

ก่อนอื่นคุณต้องตกแต่งRunAsyncด้วยasync:

private async void RunAsync()

โน๊ตสำคัญ

โดยเฉพาะอย่างยิ่งวิธีการที่ทำเครื่องหมายไว้ไม่ควรคืนค่าเป็นโมฆะตามที่เอกสารที่เชื่อมโยงกล่าวถึง ข้อยกเว้นทั่วไปสำหรับกรณีนี้คือตัวจัดการเหตุการณ์เช่นการคลิกปุ่มเป็นต้น พวกเขาจะต้องคืนความว่างเปล่า มิฉะนั้นฉันจะพยายามส่งคืนหรือasync TaskTask<TResult>asyncเมื่อใช้ เป็นแนวทางปฏิบัติที่ดีด้วยเหตุผลบางประการ

ตอนนี้คุณสามารถawaitเรียกใช้สิ่งที่Taskคล้ายกันด้านล่าง คุณไม่สามารถใช้โดยไม่ต้องawaitasync

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

ดังนั้นโดยทั่วไปหากคุณawaitทำงานคุณสามารถหลีกเลี่ยงการปฏิบัติต่อพารามิเตอร์ที่ส่งผ่านในฐานะทรัพยากรที่ใช้ร่วมกันได้โดยมีข้อผิดพลาดทั้งหมดของการแก้ไขบางสิ่งจากหลายเธรดพร้อมกัน นอกจากนี้ระวังของการปิด ฉันจะไม่พูดถึงสิ่งเหล่านั้นในเชิงลึก แต่บทความที่เชื่อมโยงนั้นทำได้ดีมาก

หมายเหตุด้านข้าง

นอกเรื่องเล็กน้อย แต่ระวังการใช้ "การบล็อก" ประเภทใด ๆ บนเธรด WinForms GUI เนื่องจากมีการทำเครื่องหมายด้วย [STAThread]เพราะมันถูกทำเครื่องหมายด้วย การใช้awaitจะไม่บล็อกเลย แต่บางครั้งฉันเห็นว่าใช้ร่วมกับการบล็อกบางประเภท

"บล็อก" อยู่ในเครื่องหมายคำพูดเนื่องจากในทางเทคนิคแล้วคุณไม่สามารถบล็อกเธรด WinForms GUIได้ ใช่ถ้าคุณใช้lockกับเธรด WinForms GUI มันจะยังคงปั๊มข้อความแม้ว่าคุณจะคิดว่า "ถูกบล็อก" ก็ตาม มันไม่ใช่.

ซึ่งอาจทำให้เกิดปัญหาแปลกประหลาดในบางกรณีที่เกิดขึ้นได้ยาก เหตุผลประการหนึ่งที่คุณไม่ต้องการใช้lockเมื่อวาดภาพเป็นต้น แต่นั่นเป็นกรณีที่ซับซ้อนและซับซ้อน อย่างไรก็ตามฉันเห็นว่ามันทำให้เกิดปัญหาที่บ้าคลั่ง ดังนั้นฉันจึงตั้งข้อสังเกตเพื่อความสมบูรณ์


23
Task.Run(() => MethodWithParameter(param));คุณยังไม่ได้รอ ซึ่งหมายความว่าถ้าparamได้รับการแก้ไขหลังจากคุณอาจมีผลที่ไม่คาดคิดในTask.Run MethodWithParameter
Alexandre Severino

8
เหตุใดจึงเป็นคำตอบที่ยอมรับได้เมื่อมันผิด มันไม่เทียบเท่ากับการส่งผ่านสถานะวัตถุเลย
Egor Pavlikhin

7
@ Zer0 ออบเจ็กต์สถานะเป็น paremeter ตัวที่สองใน Task.Factory.StartNew msdn.microsoft.com/en-us/library/dd321456(v=vs.110).aspxและจะบันทึกค่าของวัตถุในช่วงเวลาของ โทรไปที่ StartNew ในขณะที่คำตอบของคุณสร้างการปิดซึ่งจะเก็บข้อมูลอ้างอิงไว้ (หากค่าของพารามิเตอร์เปลี่ยนแปลงก่อนที่จะรันงานก็จะเปลี่ยนไปในงานด้วย) ดังนั้นรหัสของคุณจะไม่เทียบเท่ากับสิ่งที่ถาม . คำตอบคือไม่มีวิธีเขียนด้วย Task.Run ()
Egor Pavlikhin

3
@ Zer0 บางทีคุณควรอ่านซอร์สโค้ด หนึ่งส่งผ่านวัตถุสถานะอีกอันไม่ได้ ซึ่งเป็นสิ่งที่ฉันพูดตั้งแต่เริ่มต้น Task.Run ไม่ใช่มือสั้นสำหรับ Task.Factory.StartNew เวอร์ชันของวัตถุสถานะนั้นมีเหตุผลที่เป็นมรดก แต่ยังคงมีอยู่และทำงานแตกต่างกันในบางครั้งดังนั้นผู้คนควรตระหนักถึงสิ่งนั้น
Egor Pavlikhin

3
การอ่านบทความของ Toub ฉันจะเน้นประโยคนี้ "คุณต้องใช้โอเวอร์โหลดที่ยอมรับสถานะอ็อบเจ็กต์ซึ่งสำหรับพา ธ โค้ดที่ไวต่อประสิทธิภาพสามารถใช้เพื่อหลีกเลี่ยงการปิดและการจัดสรรที่เกี่ยวข้อง" ฉันคิดว่านี่คือสิ่งที่ @Zero หมายถึงเมื่อพิจารณา Task เรียกใช้การใช้งาน StartNew
davidcarr

34

ใช้การจับตัวแปรเพื่อ "ส่งผ่าน" พารามิเตอร์

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

นอกจากนี้คุณยังสามารถใช้ได้rawDataโดยตรง แต่คุณต้องระวังถ้าคุณเปลี่ยนค่าของrawDataภายนอกงาน (เช่นตัววนซ้ำในforลูป) มันจะเปลี่ยนค่าภายในงานด้วย


11
+1 Task.Runสำหรับคำนึงถึงความจริงที่สำคัญว่าตัวแปรที่อาจจะมีการเปลี่ยนแปลงทางด้านขวาหลังจากเรียก
Alexandre Severino

1
วิธีนี้จะช่วยได้อย่างไร หากคุณใช้ x ภายในเธรดงานและ x เป็นการอ้างอิงถึงอ็อบเจ็กต์และหากอ็อบเจ็กต์ถูกแก้ไขในเวลาเดียวกันเมื่อเธรดงานกำลังรันอยู่อาจทำให้เกิดความเสียหายได้
Ovi

1
@ Ovi-WanKenobi ใช่ แต่นั่นไม่ใช่คำถามเกี่ยวกับเรื่องนี้ เป็นวิธีการส่งผ่านพารามิเตอร์ หากคุณส่งการอ้างอิงไปยังออบเจ็กต์เป็นพารามิเตอร์ไปยังฟังก์ชันปกติคุณจะมีปัญหาเดียวกันที่นั่นเช่นกัน
Scott Chamberlain

ใช่สิ่งนี้ใช้ไม่ได้ งานของฉันไม่มีการอ้างอิงกลับไปที่ x ในเธรดการโทร ฉันได้โมฆะ
David Price

สก็อตต์แชมเบอร์เลนการโต้แย้งผ่านการจับกุมมาพร้อมกับประเด็นของตัวเอง โดยเฉพาะอย่างยิ่งมีปัญหาของหน่วยความจำรั่วและความดันหน่วยความจำ โดยเฉพาะอย่างยิ่งเมื่อคุณพยายามขยายขนาด (ดู "8 วิธีที่คุณสามารถทำให้หน่วยความจำรั่ว" สำหรับรายละเอียดเพิ่มเติม)
RashadRivera

7

จากนี้คุณยังสามารถ:

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

นี่คือคำตอบที่ดีที่สุดที่จะช่วยให้รัฐที่จะผ่านในและป้องกันไม่ให้สถานการณ์ที่เป็นไปได้ที่กล่าวถึงในคำตอบของ Kaden Burgart ตัวอย่างเช่นหากคุณต้องการส่งผ่านIDisposableวัตถุไปยังผู้รับมอบหมายงานเพื่อแก้ไขคำเตือน ReSharper "ตัวแปรที่จับไว้ถูกกำจัดในขอบเขตภายนอก"สิ่งนี้จะทำได้ดีมาก ตรงกันข้ามกับความเชื่อที่ได้รับความนิยมไม่มีอะไรผิดในการใช้Task.Factory.StartNewแทนTask.Runตำแหน่งที่คุณต้องผ่าน ดูที่นี่ .
นีโอ

7

ฉันรู้ว่านี่เป็นกระทู้เก่า แต่ฉันต้องการแบ่งปันวิธีแก้ปัญหาที่ฉันต้องใช้เนื่องจากโพสต์ที่ได้รับการยอมรับยังคงมีปัญหา

ปัญหา:

ในฐานะที่เป็นแหลมออกโดย Alexandre Severino ถ้าparam(ในการทำงานดังต่อไปนี้) MethodWithParameterการเปลี่ยนแปลงในไม่ช้าหลังจากการเรียกฟังก์ชั่นที่คุณอาจได้รับพฤติกรรมที่ไม่คาดคิดบางอย่างใน

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

ทางออกของฉัน:

เพื่ออธิบายสิ่งนี้ฉันลงเอยด้วยการเขียนโค้ดบรรทัดต่อไปนี้:

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

สิ่งนี้ทำให้ฉันสามารถใช้พารามิเตอร์แบบอะซิงโครนัสได้อย่างปลอดภัยแม้ว่าพารามิเตอร์จะเปลี่ยนเร็วมากหลังจากเริ่มงาน (ซึ่งทำให้เกิดปัญหากับโซลูชันที่โพสต์)

เมื่อใช้วิธีนี้param(ชนิดค่า) จะได้รับค่าที่ส่งผ่านดังนั้นแม้ว่าเมธอด async จะทำงานหลังจากparamการเปลี่ยนแปลงpจะมีค่าใดก็ตามที่paramมีเมื่อโค้ดบรรทัดนี้ทำงาน


5
ฉันรอคอยทุกคนที่สามารถคิดวิธีทำสิ่งนี้ได้อย่างชัดเจนมากขึ้นโดยมีค่าใช้จ่ายน้อยลง นี่เป็นที่ยอมรับค่อนข้างน่าเกลียด
Kaden Burgart

5
ได้ที่นี่:var localParam = param; await Task.Run(() => MethodWithParam(localParam));
Stephen Cleary

1
ซึ่งอย่างไรก็ตามสตีเฟนได้พูดคุยในคำตอบของเขาแล้วเมื่อหนึ่งปีครึ่งที่แล้ว
Servy

1
@Servy: นั่นคือคำตอบของ Scottจริงๆ อันนี้ผมไม่ตอบ
Stephen Cleary

คำตอบของสก็อตต์คงไม่ได้ผลสำหรับฉันจริง ๆ เพราะฉันวิ่งวนไปวนมา พารามิเตอร์ภายในเครื่องจะถูกรีเซ็ตในการทำซ้ำครั้งถัดไป ความแตกต่างในคำตอบที่ฉันโพสต์คือพารามิเตอร์จะถูกคัดลอกไปยังขอบเขตของนิพจน์แลมบ์ดาดังนั้นตัวแปรจึงปลอดภัยทันที ในคำตอบของ Scott พารามิเตอร์ยังคงอยู่ในขอบเขตเดิมดังนั้นจึงยังคงสามารถเปลี่ยนแปลงได้ระหว่างการเรียกบรรทัดและการเรียกใช้ฟังก์ชัน async
Kaden Burgart

5

เพียงแค่ใช้ Task.Run

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

หรือหากคุณต้องการใช้ในวิธีการใดและรองานในภายหลัง

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
โปรดระวังการปิดถ้าคุณทำแบบนั้นfor(int rawData = 0; rawData < 10; ++rawData) { Task.Run(() => { Console.WriteLine(rawData); } ) }จะไม่ทำงานเหมือนกับว่าrawDataถูกส่งผ่านไปเหมือนในตัวอย่าง StartNew ของ OP
Scott Chamberlain

@ScottChamberlain - ดูเหมือนจะเป็นตัวอย่างที่แตกต่าง;) ฉันหวังว่าคนส่วนใหญ่จะเข้าใจเกี่ยวกับการปิดค่าแลมด้า
Travis J

3
และหากความคิดเห็นก่อนหน้านั้นไม่สมเหตุสมผลโปรดดูบล็อกของ Eric Lipper ในหัวข้อ: blogs.msdn.com/b/ericlippert/archive/2009/11/12/…ซึ่งจะอธิบายว่าเหตุใดจึงเกิดเหตุการณ์นี้ได้
Travis J

2

ไม่ชัดเจนว่าปัญหาเดิมเป็นปัญหาเดียวกันกับที่ฉันมีหรือไม่: ต้องการเพิ่มเธรด CPU สูงสุดในการคำนวณภายในลูปในขณะที่รักษาค่าของตัววนซ้ำและรักษาอินไลน์เพื่อหลีกเลี่ยงการส่งผ่านตัวแปรจำนวนมากไปยังฟังก์ชันผู้ปฏิบัติงาน

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

ฉันทำให้สิ่งนี้ใช้งานได้โดยการเปลี่ยนตัววนรอบด้านนอกและกำหนดค่าของมันด้วยเกต

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

แนวคิดคือหลีกเลี่ยงการใช้ Signal เหมือนข้างบน การปั๊มค่า int ลงในโครงสร้างจะป้องกันไม่ให้ค่าเหล่านั้นเปลี่ยนแปลง (ในโครงสร้าง) ฉันมีปัญหาต่อไปนี้: loop var ฉันจะเปลี่ยนก่อนที่จะเรียก DoSomething (i) (ฉันถูกเพิ่มขึ้นที่ส่วนท้ายของลูปก่อน () => DoSomething (i, i i) ถูกเรียก) ด้วยโครงสร้างจะไม่เกิดขึ้นอีกต่อไป จุดบกพร่องที่น่าค้นหา: DoSomething (i, i i) ดูดี แต่ไม่แน่ใจว่าถูกเรียกทุกครั้งด้วยค่าที่แตกต่างกันสำหรับ i (หรือแค่ 100 ครั้งกับ i = 100) ดังนั้น -> 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
แม้ว่าจะตอบคำถามได้ แต่ก็ถูกตั้งค่าสถานะให้ตรวจสอบ คำตอบที่ไม่มีคำอธิบายมักถือว่ามีคุณภาพต่ำ โปรดให้ความเห็นว่าเหตุใดจึงเป็นคำตอบที่ถูกต้อง
แดน
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.