Async กำลังรอการเลือก linq


180

ฉันต้องการแก้ไขโปรแกรมที่มีอยู่และมันมีรหัสต่อไปนี้:

var inputs = events.Select(async ev => await ProcessEventAsync(ev))
                   .Select(t => t.Result)
                   .Where(i => i != null)
                   .ToList();

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

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

ฉันควรเขียนโค้ดด้านบนเหมือนดังต่อไปนี้ตามคำตอบอื่นโดย Stephen Cleary :

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

และมันเหมือนกันหมดอย่างนี้หรือไม่?

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
                                       .Where(result => result != null).ToList();

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

ProcessEventsAsync มีลักษณะเช่นนี้:

async Task<InputResult> ProcessEventAsync(InputEvent ev) {...}

ProceesEventAsync ประเภทใดที่ส่งคืน?
tede24

@ tede24 มันTask<InputResult>มีInputResultเป็นชั้นเอง
Alexander Derck

เวอร์ชันของคุณอ่านง่ายกว่ามากในความคิดของฉัน แต่คุณลืมผลที่ได้จากงานก่อนของคุณSelect Where
สูงสุด

และ InputResult มีคุณสมบัติผลลัพธ์ใช่ไหม
tede24

@ tede24 ผลลัพธ์เป็นคุณสมบัติของงานไม่ใช่คลาสของฉัน และ @Max การรอคอยควรทำให้แน่ใจว่าฉันได้รับผลลัพธ์โดยไม่ต้องเข้าถึงResultคุณสมบัติของงาน
Alexander Derck

คำตอบ:


185
var inputs = events.Select(async ev => await ProcessEventAsync(ev))
                   .Select(t => t.Result)
                   .Where(i => i != null)
                   .ToList();

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

การโทรถึงSelectถูกต้อง สองบรรทัดนี้เหมือนกันเป็นหลัก:

events.Select(async ev => await ProcessEventAsync(ev))
events.Select(ev => ProcessEventAsync(ev))

(มีความแตกต่างเล็กน้อยเกี่ยวกับวิธีโยนข้อยกเว้นแบบซิงโครนัProcessEventAsyncส แต่ในบริบทของรหัสนี้มันไม่สำคัญเลย)

จากนั้นเลือกที่สองซึ่งเลือกผลลัพธ์ นี่ไม่ได้หมายความว่างานนั้นไม่ได้เป็นแบบอะซิงก์เลยและดำเนินการแบบซิงโครนัส (ไม่มีความพยายามอะไรเลย) หรือจะทำงานแบบอะซิงโครนัสและเมื่อมีการดำเนินการแบบสอบถามที่เหลือ

หมายความว่าแบบสอบถามกำลังบล็อก ดังนั้นมันไม่ได้เป็นแบบอะซิงโครนัสจริงๆ

ทำลายมันลง:

var inputs = events.Select(async ev => await ProcessEventAsync(ev))

จะเริ่มการทำงานแบบอะซิงโครนัสสำหรับแต่ละเหตุการณ์ก่อน จากนั้นบรรทัดนี้:

                   .Select(t => t.Result)

จะรอให้การดำเนินการเหล่านั้นเสร็จสิ้นทีละครั้ง (ก่อนจะรอการดำเนินการของเหตุการณ์แรกจากนั้นดำเนินการถัดไปจากนั้นดำเนินต่อไป ฯลฯ )

นี้เป็นส่วนหนึ่งที่ผมไม่สนใจเพราะบล็อกมันและยังจะตัดข้อยกเว้นใด ๆ AggregateExceptionใน

และมันเหมือนกันหมดอย่างนี้หรือไม่?

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
                                       .Where(result => result != null).ToList();

ใช่ทั้งสองตัวอย่างนี้เทียบเท่ากัน พวกเขาทั้งสองเริ่มการทำงานแบบอะซิงโครนัสทั้งหมด ( events.Select(...)) จากนั้นรอแบบอะซิงโครนัสเพื่อให้การดำเนินการทั้งหมดเสร็จสมบูรณ์ในลำดับใด ๆ ( await Task.WhenAll(...)) จากนั้นดำเนินการกับส่วนที่เหลือของงาน ( Where...)

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


ไชโยสำหรับการล้างนั่น! ดังนั้นแทนที่จะมีข้อยกเว้นพันเป็นAggregateExceptionฉันจะได้รับการยกเว้นแยกหลายรายการในรหัสที่สอง?
Alexander Derck

1
@AlexanderDerck: ไม่ทั้งในรหัสเก่าและใหม่มีเพียงข้อยกเว้นแรกเท่านั้นที่จะถูกยกขึ้น แต่ด้วยความที่มันจะถูกห่อในResult AggregateException
สตีเฟ่นเคลียร์

ฉันได้รับการหยุดชะงักใน ASP.NET MVC Controller ของฉันโดยใช้รหัสนี้ ฉันแก้ไขมันด้วย Task.Run (…) ฉันไม่มีความรู้สึกที่ดีเกี่ยวกับมัน อย่างไรก็ตามมันจะเสร็จสิ้นทันทีเมื่อทำงานในการทดสอบ async xUnit เกิดอะไรขึ้น?
SuperJMN

2
@SuperJMN: แทนที่stuff.Select(x => x.Result);ด้วยawait Task.WhenAll(stuff)
Stephen Cleary

1
@DanielS: พวกเขากำลังหลักเดียวกัน มีความแตกต่างบางอย่างเช่นเครื่องจักรของรัฐการบันทึกบริบทพฤติกรรมของข้อยกเว้นแบบซิงโครนัส ข้อมูลเพิ่มเติมที่blog.stephencleary.com/2016/12/eliding-async-await.html
Stephen Cleary

25

รหัสที่มีอยู่ใช้งานได้ แต่กำลังบล็อกเธรด

.Select(async ev => await ProcessEventAsync(ev))

สร้างงานใหม่สำหรับทุกเหตุการณ์ แต่

.Select(t => t.Result)

บล็อกเธรดที่รอให้แต่ละภารกิจใหม่จบ

ในทางกลับกันโค้ดของคุณจะให้ผลลัพธ์เหมือนกัน แต่จะเก็บแบบอะซิงโครนัส

เพียงแค่ความคิดเห็นเดียวในรหัสแรกของคุณ สายนี้

var tasks = await Task.WhenAll(events...

จะสร้างภารกิจเดียวดังนั้นตัวแปรควรตั้งชื่อเป็นเอกเทศ

ในที่สุดรหัสสุดท้ายของคุณให้เหมือนกัน แต่รวบรัดมากขึ้น

สำหรับการอ้างอิง: Task.Wait / Task.WhenAll


ในความเป็นจริงแล้วการบล็อกรหัสแรกจะถูกดำเนินการพร้อมกันหรือไม่
Alexander Derck

1
ใช่เพราะการเข้าถึงผลลัพธ์จะสร้างการรอซึ่งบล็อกเธรด ในทางกลับกันเมื่อสร้างงานใหม่คุณสามารถรอได้
tede24

1
กลับมาที่คำถามนี้และดูที่คำพูดของคุณเกี่ยวกับชื่อของtasksตัวแปรคุณพูดถูก ทางเลือกที่น่ากลัวพวกเขาไม่ได้ทำงานตามที่พวกเขารอคอยทันที ฉันจะทิ้งคำถามไว้เหมือนกัน
Alexander Derck

13

ด้วยวิธีการปัจจุบันที่มีอยู่ใน Linq มันดูน่าเกลียดมาก:

var tasks = items.Select(
    async item => new
    {
        Item = item,
        IsValid = await IsValid(item)
    });
var tuples = await Task.WhenAll(tasks);
var validItems = tuples
    .Where(p => p.IsValid)
    .Select(p => p.Item)
    .ToList();

หวังว่าการติดตาม. NET ในเวอร์ชันต่อ ๆ ไปจะมีเครื่องมือที่หรูหรากว่าเพื่อจัดการกับคอลเลกชันของงานและงานของคอลเลกชัน


12

ฉันใช้รหัสนี้:

public static async Task<IEnumerable<TResult>> SelectAsync<TSource,TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> method)
{
      return await Task.WhenAll(source.Select(async s => await method(s)));
}

แบบนี้:

var result = await sourceEnumerable.SelectAsync(async s=>await someFunction(s,other params));

5
นี่เป็นเพียงการตัดการทำงานที่มีอยู่ในลักษณะคลุมเครือมากขึ้น
อเล็กซานเดอร์เดอร์ก

ทางเลือกคือ var result = await Task.WhenAll (sourceEnumerable.Select (async s => กำลังรอ someFunction (s, params อื่น ๆ )) มันใช้งานได้เช่นกัน แต่ไม่ใช่ LINQy
Siderite Zackwehdex

ไม่ควรFunc<TSource, Task<TResult>> methodมีรหัสที่other paramsกล่าวถึงในบิตที่สอง
matramos

2
พารามิเตอร์พิเศษเป็นภายนอกขึ้นอยู่กับฟังก์ชั่นที่ฉันต้องการดำเนินการซึ่งไม่เกี่ยวข้องในบริบทของวิธีการขยาย
Siderite Zackwehdex

4
นั่นเป็นวิธีการต่อที่น่ารัก ไม่แน่ใจว่าทำไมมันจึงถูกมองว่า "คลุมเครือมากกว่า" - มันมีความหมายคล้ายกับซิงโครนัSelect()สจึงเป็นดรอปอินที่สง่างาม
nullPainter

11

ฉันชอบวิธีนี้เป็นวิธีเสริม:

public static async Task<IEnumerable<T>> WhenAll<T>(this IEnumerable<Task<T>> tasks)
{
    return await Task.WhenAll(tasks);
}

เพื่อให้สามารถใช้งานได้โดยการโยงวิธี:

var inputs = await events
  .Select(async ev => await ProcessEventAsync(ev))
  .WhenAll()

1
คุณไม่ควรเรียกวิธีนี้Waitเมื่อไม่ได้รอ มันกำลังสร้างงานที่เสร็จสมบูรณ์เมื่องานทั้งหมดเสร็จสมบูรณ์ เรียกว่าWhenAllเช่นเดียวกับTaskวิธีการเลียนแบบ asyncนอกจากนี้ยังเป็นจุดหมายสำหรับวิธีการที่จะ เพียงแค่โทรWhenAllและทำได้ด้วย
Servy

เสื้อคลุมเล็กน้อยไร้ประโยชน์ในความคิดของฉันเมื่อมันเพิ่งเรียกวิธีการเดิม
Alexander Derck

@Servy fair point แต่ฉันไม่ชอบชื่อตัวเลือกใด ๆ WhenAll ทำให้ดูเหมือนเป็นเหตุการณ์ที่ไม่มาก
Daryl

3
@AlexanderDerck ข้อได้เปรียบคือคุณสามารถใช้มันในวิธีการผูกมัด
Daryl

1
@Daryl เนื่องจากWhenAllส่งคืนรายการที่ประเมิน (มันไม่ได้ประเมินอย่างขี้เกียจ) คุณสามารถสร้างอาร์กิวเมนต์เพื่อใช้Task<T[]>ประเภทส่งคืนเพื่อแสดงว่า เมื่อรอคอยสิ่งนี้จะยังคงสามารถใช้ Linq ได้ แต่ยังสื่อสารด้วยว่ามันไม่ขี้เกียจ
JAD
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.