ข้าม​ไป​ยัง​เนื้อหา

เปิด analyzer ครบ​ทุก​ตัว​แล้ว await ใน foreach ก็​ยัง​ผ่าน​ด่าน​โดย​ไม่มี​เสียง​เตือน

คุณ​เขียน foreach ที่​ยิง HTTP ที​ละ​รายการ แล้ว​เปิด pull request

ไม่มี​ใคร​ทัก บิ​ลด์​เขียว analyzer เงียบ CI ผ่าน หน้า​นั้น​ขึ้น production แล้ว​ช้า​กว่า​ที่​ควร เป็น​เท่า​ตัว จนกว่า​จะ​มี​คน​ไป​เปิด​ดู​เอง

ความ​เชื่อ​ที่​ทำให้​เรื่อง​นี้​เกิด​ซ้ำ​ได้​เรื่อย ๆ คือ “ถ้า​มัน​แย่​จริง เครื่อง​มือ​คง​เตือน​แล้ว” หน้า​นี้​เอา​ความ​เชื่อ​นั้น​ไป​วัด

ผล​คือ​เครื่อง​มือ มี​อะไร​จะ​พูด กับ code นั้น​เยอะ​มาก มัน​พ่น 17 diagnostic ใส่ file สอง​ใบ​ที่​มี​ตัว​วน และ​ใน​นั้น 0 ตัว​ที่​พูด​ถึง​ตัว​วน

วิธี​วัด เปิด​ทุก​สวิตช์​ที่ SDK ยอม​ให้​ตั้ง แล้ว​นับ​สิ่ง​ที่​มัน​พูด

หัวข้อ​ที่​มีชื่อ​ว่า “วิธี​วัด เปิด​ทุก​สวิตช์​ที่ SDK ยอม​ให้​ตั้ง แล้ว​นับ​สิ่ง​ที่​มัน​พูด”

ข้อ​อ้าง​ว่า “ไม่มี​ใคร​เตือน” ไม่มี​ความ​หมาย​เลย ถ้า​เรา​เป็น​คน​ปิดปาก​เครื่อง​มือ​เอง

ตัว​วัด​บิ​ลด์ file สี่​ใบ​ใน compilation เดียวกัน ด้วย​สวิตช์​ชุด​เดียวกัน บน SDK 10.0.302

fileมี​อะไร​อยู่​ใน​นั้นหน้าที่
Sequential.csawait ใน foreach for และ while ปลาย​ทาง​เป็น delegateตัว​ที่​ถูก​วัด
Realistic.csรูป​เดียวกัน ปลาย​ทาง​เป็น HttpClientตัว​ที่​ถูก​วัด
Control.csของ​ที่​ผิด​ใน​เรื่อง​ที่​เครื่อง​มือ​จับ​ได้​แน่ ๆช่อง​ควบคุม
Nearby.csstackalloc ใน​ตัว​วนrule ที่​เข้า​ใกล้​ที่สุด

สวิตช์​ที่​เปิด​คือ AnalysisLevel=latest-all AnalysisMode=All EnforceCodeStyleInBuild และ WarningLevel=9999 ซึ่ง​เป็น​ชุด​ที่​แรง​ที่สุด​ที่​ตั้ง​ได้​โดย​ไม่​ต้อง​ลง package เพิ่ม

ใน​บิ​ลด์​นั้น​มี analyzer ทำงาน​จริง 377 ตัว ครอบคลุม rule id 447 ตัว

ทำไม​ต้อง​มี​ช่อง​ควบคุม​สอง​ชั้น ไม่ใช่​ชั้น​เดียว

“ไม่มี diagnostic” เป็น​ผล​ที่​ได้​ฟรี​ถ้า​เครื่อง​มือ​ไม่​ได้​ทำงาน​ตั้งแต่​แรก Control.cs จึง​ต้อง ส่ง​เสียง​ทั้ง​สอง​ชั้น​ใน​บิ​ลด์​เดียวกัน​นั้น

ชั้น compiler คือ CS4014 — เรียก​ของ​ที่​คืน Task ใน​สาย async แล้ว​ทิ้ง​ผล​ไป ชั้น analyzer คือ CA1849 — บล็อก​ด้วย .Result ใน​สาย async

ถ้า​ตรวจ​แค่​ชั้น​เดียว อีก​ชั้น​ดับ​ไป​ทั้ง​ชั้น​แล้ว​เรา​จะ​ไม่รู้​เลย และ​จะ​รายงาน​ความ​เงียบ​นั้น เป็น​ข้อ​ค้น​พบ ทั้ง​ที่​มัน​คือ​เครื่อง​มือ​วัด​พัง

เครื่อง​มือ​มี​อะไร​จะ​พูด​เยอะ​มาก และ​ไม่มี​ข้อ​ไหน​เป็น​เรื่อง​ตัว​วน

หัวข้อ​ที่​มีชื่อ​ว่า “เครื่อง​มือ​มี​อะไร​จะ​พูด​เยอะ​มาก และ​ไม่มี​ข้อ​ไหน​เป็น​เรื่อง​ตัว​วน”

ทุก diagnostic ที่​ออก​มา​เป็น​เรื่อง​อื่น​ทั้งหมด และ​หนึ่ง​ใน​นั้น​ชี้​ตรง​ที่ await ตัว​ที่​เป็น​ปัญหา​พอดี

diagnostic ทั้ง 17 ตัว​ที่​ตก​บน file สอง​ใบ​นั้น แยก​ได้​เป็น​สาม rule

ruleกี่​ตัวมัน​บ่น​เรื่อง​อะไร
CA106210ยัง​ไม่​ได้​ตรวจ​ว่า parameter เป็น null หรือ​ไม่
CA20075ควร​เรียก ConfigureAwait ต่อ​ท้าย task ที่ await
CA22342ควร​ส่ง Uri แทน string

CA2007 คือ​ตัว​ที่​น่า​สนใจ​ที่สุด​ใน​ตาราง มัน​ไม่​ได้​บ่น​แบบ​เหมา​รวม​ทั้ง method มันชี้​ไป​ที่ token await ตัว​นั้น​เป๊ะ ตัว​เดียว​กับ​ที่​ทำให้​ทั้ง​ตัว​วน​เดิน​ที​ละ​รอบ

ข้อความ​ของ​มัน​คือ "Consider calling ConfigureAwait on the awaited task"

เครื่อง​มือ​เห็น await ตัว​นั้น มี​ความ​เห็น​เรื่อง ConfigureAwait ของ​มัน แล้ว​ไม่มี​ความ​เห็น เรื่อง foreach ที่​ครอบ​มัน​อยู่

ตั้ง warning เป็น error แล้ว​บิ​ลด์​ก็​ล้ม แต่​ล้ม​ด้วย​เรื่อง​อื่น

หัวข้อ​ที่​มีชื่อ​ว่า “ตั้ง warning เป็น error แล้ว​บิ​ลด์​ก็​ล้ม แต่​ล้ม​ด้วย​เรื่อง​อื่น”

เพิ่ม TreatWarningsAsErrors=true เข้าไป​ใน​โปร​เจ​กต์​ที่​มี​แต่ file สอง​ใบ​นั้น บิ​ลด์​ล้ม​ทันที ด้วย 3 rule คือ CA1062 · CA2007 · CA2234

ไม่มี​ตัว​ไหน​ใน​นั้น​เกี่ยว​กับ​ตัว​วน​เลย ทั้ง​สาม​เป็น​เรื่อง null เรื่อง ConfigureAwait และ​เรื่อง Uri

ทีนี้​ปิด​เสียง​เฉพาะ​สาม​ตัว​นั้น ซึ่ง​เป็น​สิ่ง​ที่​ทีม​จริง​ทำ​กัน​เป็น​ปกติ เพราะ​มัน​ไม่​เกี่ยว​กับ บั๊ก​ที่​เรา​กำลัง​หา แล้ว​วัด​ซ้ำ

Terminal window
dotnet build --no-incremental \
-p:TreatWarningsAsErrors=true \
-p:NoWarn="CA1062;CA2007;CA2234"

บิ​ลด์​ผ่าน เหลือ 0 diagnostic

code ที่​ยิง​เน็ต​ที​ละ​รายการ​ติดต่อ​กัน ผ่าน​ด่าน​ที่​แรง​ที่สุด​ที่ .NET ตั้ง​ได้ โดย​สะอาด​หมดจด

เครื่อง​มือ​มอง​เห็น​ตัว​วน​ได้ มัน​แค่​ไม่มี rule สำหรับ​รูป​นี้

หัวข้อ​ที่​มีชื่อ​ว่า “เครื่อง​มือ​มอง​เห็น​ตัว​วน​ได้ มัน​แค่​ไม่มี rule สำหรับ​รูป​นี้”

ความ​เงียบ​นี้​ไม่​ได้​แปล​ว่า analyzer อ่าน​ตัว​วน​ไม่​ออก มี rule ที่​อ่าน​ออก​อยู่ และ​มัน​ดัง​ใน​บิ​ลด์​เดียวกัน

ข้อ​สรุป​ที่​ง่าย​เกิน​ไป​คือ “analyzer มัน​โง่” ซึ่ง​ไม่​จริง และ​ตัว​วัด​พิสูจน์​ว่า​ไม่​จริง

Nearby.cs มี stackalloc อยู่​ใน​ตัว​วน แล้ว CA2014 ก็​ดัง​ใน​บิ​ลด์​เดียวกัน​นั้น ข้อความ​ของ​มัน​คือ "Potential stack overflow. Move the stackalloc out of the loop."

rule ตัว​นั้นอ่าน​ออก​ว่า​อะไร​อยู่​ใน​ตัว​วน ความ​สามารถ​นั้น​มี​อยู่​แล้ว​ใน​กล่อง สิ่ง​ที่​ไม่มี​คือ rule ที่​เอา​ความ​สามารถ​นั้น​มา​ส่อง​ดู await

แล้ว rule ที่​ชื่อ Task.WhenAll ที่​มี​อยู่​ล่ะ

ใน roster มี CA1842 กับ CA1843 ซึ่ง​ตั้ง​ชื่อ​ตาม Task.WhenAll จริง แต่​เปิด​เอกสาร​ดู​แล้ว​มัน​คนละ​เรื่อง​กับ​หน้า​นี้

ช่อง Cause ของ CA1842 เขียน​ว่า “WhenAll is called with a single task” และ​หมวด​ของ​มัน​คือ Performance มัน​คือ rule ที่​ดัก​ตอน​คุณ​เรียก WhenAll ด้วย task ตัว​เดียว ซึ่ง​เป็น​ความ​ซ้ำ​ซ้อน

มัน​ดัก​กรณี​ที่​คุณ ใช้ WhenAll เกิน​จำเป็น ไม่​ได้​ดัก​กรณี​ที่​คุณ ไม่​ได้​ใช้ มัน​ทั้ง​ที่​ควร​ใช้ สอง​อย่าง​นี้​เป็น​คนละ​คำถาม และ​ใน​กล่อง​มี​แต่​อย่าง​แรก

ขอบเขต​ของ​ข้อ​อ้าง​นี้

ตัวเลข​ทุก​ตัว​ข้าง​บน​วัด​จาก .NET SDK ตาม​ที่​มัน​มา​ใน​กล่อง ไม่​ได้​ลง analyzer package ของ​เจ้า​อื่น​เพิ่ม การ​วัด​นี้​จึง​ไม่​ได้​พูด​แทน Roslynator, SonarAnalyzer หรือ package อื่น​ที่​คุณ​ลง​เอง​ได้

และ​จำนวน diagnostic ขึ้น​กับ​รุ่น​ของ SDK เพราะ Microsoft เพิ่ม rule ใหม่​ทุก​รุ่น ตัวเลข​ใน​หน้า​นี้​จึง​มา​พร้อม​ป้าย 10.0.302 เสมอ ส่วน​ด่าน​ที่ CI รัน​ตรวจ รูป​ของ​ผล ไม่​ได้​ตรึง​จำนวน​ไว้​ที​ละ​ตัว

และ​รูป​ของ​ผล​นั้น​ข้าม SDK ได้​จริง ด่าน​เดียวกัน​นี้​รัน​บน runner ของ CI ที่​ใช้ SDK 10.0.400 ซึ่ง​มี analyzer 378 ตัว​และ rule id 448 ตัว มากกว่า​เครื่อง​ผู้​เขียน​อย่าง​ละ​หนึ่ง

ผล​ที่​ได้​คือ 17 diagnostic และ 0 ตัว​ที่​พูด​ถึง​ตัว​วน ด้วย rule id ชุด​เดิม​ทั้ง​สาม​ตัว

8 รายการ รายการ​ละ 60 มิลลิ​วินาที ต่าง​กัน 8 เท่า

ตัว​วัด​ยิง​งาน​ที่​ใช้​เวลา 60 ms เท่า​กัน​ทุก​ตัว จำนวน 8 ตัว ปลาย​ทาง​เป็น Task.Delay ล้วน ไม่ใช่​งาน CPU เพราะ​สิ่ง​ที่​วัด​คือ เวลา​ที่​รอ​ต่อ​กัน​เป็น​ทอด ๆ ไม่ใช่​ปริมาณ​งาน

ท่าเวลา​ที่​ใช้
await ใน foreach489 ms
await Task.WhenAll(...)61 ms

ฝั่ง foreach ใช้​เวลา​เท่ากับ​ผล​รวม​ของ​ทุก​ตัว คือ​ราว 480 ms ส่วน​ฝั่ง WhenAll ใช้​เวลา​เท่ากับ​ตัว​ที่​ช้า​ที่สุด​ตัว​เดียว

ตัวเลข​นี้​เป็น​ของ​เครื่อง​ที่​รัน (.NET 10.0.10) สิ่ง​ที่​ไม่​ขึ้น​กับ​เครื่อง​คือ​อัตราส่วน

ทาง​แก้​ที่​ทุก​คน​บอก เปลี่ยน​พฤติกรรม​ของ exception ไป​ด้วย

หัวข้อ​ที่​มีชื่อ​ว่า “ทาง​แก้​ที่​ทุก​คน​บอก เปลี่ยน​พฤติกรรม​ของ exception ไป​ด้วย”

await Task.WhenAll โยน exception ตัว​แรก​ตัว​เดียว ไม่ใช่ AggregateException

ครึ่ง​นี้​สำคัญ​กว่า​ครึ่ง​แรก เพราะ​มัน​เปลี่ยน​สิ่ง​ที่​คุณ​เห็น​ตอน​ของ​พัง

ตัว​วัด​สร้าง task ที่ fault พร้อม​กัน 5 ตัว แล้ว await Task.WhenAll ทับ​มัน 2,000 รอบ

สิ่ง​ที่​ถามผล​จาก 2,000 รอบ
await โยน​อะไร​ออก​มาInvalidOperationException
กี่​รอบ​ที่​ได้ AggregateException0
property .Exception ของ task เป็น​อะไรAggregateException
ใน .Exception มี inner กี่​ตัว5
.Wait() โยน​อะไรAggregateException

AggregateException มี​อยู่​จริง และ​มัน​ครบ​ทั้ง 5 ตัว แต่​มัน​อยู่​ใน property .Exception ของ task ไม่​ได้​ถูก​โยน​ออก​มา​ทาง await

await แกะ AggregateException ออก​แล้ว​โยน​ตัว​แรก​ใบ​เดียว นี่​คือ​พฤติกรรม​ที่​ตั้งใจ ไม่ใช่​บั๊ก แต่​ผล​ของ​มัน​คือ

try
{
await Task.WhenAll(tasks);
}
catch (AggregateException ex) // ❌ block นี้ไม่เคยทำงาน
{
Log(ex.InnerExceptions);
}

catch ก้อน​นั้น​ไม่​เคย​เข้า​เลย​สัก​รอบ​จาก 2,000 รอบ คน​เขียน​เห็น​ของ​พัง 1 ใน 5 แล้ว​สรุป​ว่า​มี​ปัญหา​จุด​เดียว

ถ้า​อยาก​ได้​ครบ​ทุก​ตัว ต้อง​หยิบ​จาก task โดยตรง

var all = Task.WhenAll(tasks);
try
{
await all;
}
catch
{
// all.Exception คือ AggregateException ที่ครบทุกตัว
Log(all.Exception!.InnerExceptions);
}
ครึ่ง​ที่​ยัง​ถูก​อยู่ ห้าม​แก้​ทิ้ง​ไป​ด้วย

Task.WhenAll รอ​ให้​ครบ​ทุก​ตัว​จริง ก่อน​ที่​ความ​ผิด​พลาด​จะ​ออก​มา ตัว​วัด​ยืนยัน​ว่า​งาน​ทั้ง 5 ตัว​จบ​ก่อน exception จะ​โผล่

ประโยค​ที่​ผิด​คือ “แล้ว​ค่อย​โยน AggregateException” ไม่ใช่ “รอ​ครบ​ก่อน”

WhenAll ไม่ใช่​คำ​ตอบ​เสมอ​ไป และ​นี่​คือ​สาม​กรณี​ที่​มัน​ทำ code พัง

หัวข้อ​ที่​มีชื่อ​ว่า “WhenAll ไม่ใช่​คำ​ตอบ​เสมอ​ไป และ​นี่​คือ​สาม​กรณี​ที่​มัน​ทำ code พัง”

หน้าที่​บอก​ว่า “ใช้ WhenAll สิ” ทุก​ครั้ง ผิด​พอ ๆ กับ​ความ​เชื่อ​ที่​หน้า​นี้​เพิ่ง​รื้อ​ทิ้ง

await ใน​ตัว​วน​ไม่​ได้​เป็น​บั๊ก​โดย​ตัว​มัน​เอง มัน​เป็น​บั๊ก​เมื่อ​งาน​แต่ละ​ชิ้น ไม่​เกี่ยวข้อง​กัน เมื่อไร​ที่​มัน​เกี่ยวข้อง​กัน การ​ย้าย​ไป WhenAll คือ​การ​เปลี่ยน​ผลลัพธ์ ไม่ใช่​การ​เร่ง​ความเร็ว

สอง​กรณี​แรก​ข้าง​ล่าง​เป็น ผล​แน่นอน ตัวเลข​ออก​มา​เป็น 0 กับ​เต็ม​จำนวน​ทุก​ช่อง ทุกร​อบ และ​เหมือน​กัน​บน​ทุก​เครื่อง​ที่​วัด

กรณี​ที่​สาม​ไม่ใช่ และ​นั่น​คือ​กรณี​ที่​อันตราย​กว่า

ตัว​วัด​สั่ง​งาน 5 ชิ้น โดย​ให้​ชิ้น​แรก​ช้า​ที่สุด ไล่​ลง​มา​เป็น 100 · 80 · 60 · 40 · 20 มิลลิ​วินาที แล้ว​บันทึก​ลำดับ​ที่​แต่ละ​ชิ้น​จบ ทำ​ซ้ำ 10 รอบ

ท่าจำนวน​รอบ​ที่​ลำดับ​ยัง​เหมือน​ที่​สั่ง
await ใน foreach10 จาก 10
await Task.WhenAll(...)0 จาก 10

ฝั่ง WhenAll จบ​เรียง​ตาม ระยะ​เวลา​ของ​งาน ซึ่ง​ใน​การ​วัด​นี้​คือ​กลับ​หัว​พอดี

ถ้า​ตัว​วน​ของ​คุณ​เขียน audit log ตัด​สต็​อก​ตาม​ลำดับ​คิว หรือ​ส่ง event ที่​ปลาย​ทาง​เรียง​ลำดับ foreach คือ​คำ​ตอบ​ที่​ถูก ไม่ใช่​บั๊ก

สอง เมื่อ​ของ​ที่​ใช้​ร่วม​กัน​ปฏิเสธ​การ​ถูก​ใช้​ซ้อน

หัวข้อ​ที่​มีชื่อ​ว่า “สอง เมื่อ​ของ​ที่​ใช้​ร่วม​กัน​ปฏิเสธ​การ​ถูก​ใช้​ซ้อน”

ทรัพยากร​จำนวน​มาก​ไม่​ยอม​ให้​ใช้​ซ้อน​กัน แล้ว​โยน​ทิ้ง​ทันที ตัว​วัด​ใช้​ยาม​ที่​เขียน​เอง​เพื่อ​ให้​ผล เป็น​ของ​แน่นอน ทำ​ซ้ำ 200 รอบ

ท่าจำนวน​รอบ​ที่​ชน​ยาม
await ใน foreach0 จาก 200
await Task.WhenAll(...)200 จาก 200

ตัว​ที่​คน​เขียน .NET เจอ​บ่อย​ที่สุด​คือ DbContext ของ EF Core เอกสาร​ของ​มัน​เขียน​ไว้​ตรง​ตัว ว่า EF Core ไม่​รองรับ operation หลาย​ตัว​ที่​วิ่ง​ขนาน​กัน​บน DbContext ตัว​เดียว

เมื่อ​มัน​ตรวจ​เจอ คุณ​จะ​ได้ InvalidOperationException ที่​ขึ้น​ต้น​ว่า “A second operation started on this context before a previous operation completed”

ตัว​วน​ที่​เรียก repository ตัว​เดียวกัน​ที​ละ​รายการ จึง​เป็น code ที่​ถูก​อยู่​แล้ว การ​เปลี่ยน​เป็น WhenAll โดย​ไม่​แตะ​อย่าง​อื่น คือ​การ​แลก​ความ​ช้า​ที่​เห็น​ได้ กับ exception ตอน​ตี​สาม

กรณี​ที่​แย่​กว่า​กรณี​ที่​สอง คือ​กรณี​ที่​ไม่มี​ใคร​โยน​อะไร​ให้​เห็น

List<T> ไม่​ปลอดภัย​ต่อ thread และ​ไม่มี​อะไร​ตรวจ​ให้ ตัว​วัด​เขียน​ลง list ใบ​เดียวกัน 2,000 รายการ ทำ​ซ้ำ 20 รอบ

ตาราง​นี้​ต่าง​จาก​สอง​ตาราง​บน เพราะ​มัน​มี​สอง​เครื่อง

เครื่องforeach ผิด​กี่​รอบWhenAll ผิด​กี่​รอบ
เครื่อง​ผู้​เขียน · Arm64 · 18 core · SDK 10.0.3020 จาก 2020 จาก 20
runner ของ CI · ubuntu-latest (x64) · SDK 10.0.4000 จาก 200 จาก 20

code ชุด​เดียวกัน ข้อมูล​ชุด​เดียวกัน คำ​สั่ง​เดียวกัน ฝั่ง WhenAll พัง​ทุกร​อบ​บน​เครื่อง​หนึ่ง และ​ไม่​พัง​เลย​สัก​รอบ​บน​อีก​เครื่อง​หนึ่ง

ฝั่ง foreach ถูก​ทุกร​อบ​ทั้ง​สอง​เครื่อง เพราะ​มัน​ไม่มี concurrency ให้​ผิด​ตั้งแต่​แรก

นี่​คือ​เหตุผล​ที่ 'เครื่อง​ผม​รัน​แล้ว​ไม่​พัง' ไม่ใช่​หลัก​ฐาน

ตาราง​บน​เป็น​ตัวเลข​จริง​จาก​รัน​จริง​สอง​เครื่อง ไม่ใช่​การ​เปรียบ​เปรย

บั๊ก​ชนิด​นี้​ผ่าน code review ได้ ผ่าน​เทสต์​ใน​เครื่อง​ได้ ผ่าน CI ได้​ครบ​ทุก​ด่าน แล้วไป​โผล่ ที่ production ซึ่ง​มี​คอร์​มากกว่า​และ​มี​โหลด​จริง จำ​นว​น​คอร์​กับ​จังหวะ​ของ scheduler เป็น​ตัว​ตัดสิน​ว่า​คุณ​จะ​เห็น​มัน​วัน​นี้​หรือ​อีก​หก​เดือน

สังเกต​ว่า​กรณี​ที่​สอง — ทรัพยากร​ที่​มี​ยาม — โยน exception ทุกร​อบ​ทั้ง​สอง​เครื่อง ยาม​ตัว​นั้น​คือ​ของ​ขวัญ มัน​เปลี่ยน​บั๊ก​ที่​ซ่อน​ตัว​ได้ ให้​เป็น​บั๊ก​ที่​ต้อง​แก้​วัน​นี้

ของ​ที่​ไม่มี​ยาม ไม่​ได้​ปลอดภัย​กว่า มัน​แค่​เงียบ​กว่า

ผล​ผิด​ใน​ที่​นี้​คือ​จำนวน​สมาชิก​ไม่​ครบ หรือ list โยน exception ออก​มาก​ลาง​ทาง ทั้ง​สอง​แบบ ไม่มี diagnostic ตัว​ไหน​เตือน​ล่วง​หน้า​เช่น​กัน

ด่าน​ของ​หน้า​นี้​เคย​ผูก​ความ​เขียว​ไว้​กับ race

ตอน​แรก​ตัว​วัด​บังคับ​ว่า​ฝั่ง WhenAll ต้อง​ผิด​อย่าง​น้อยห​นึ่ง​รอบ ถึง​จะ​ถือว่า​ผ่าน

มัน​ผ่าน​บน​เครื่อง​ผู้​เขียน แล้ว​ทำให้ CI แดง​ทันที​ที่​รัน​บน runner ตัว​จริง — ไม่ใช่​เพราะ ข้อ​อ้าง​ผิด แต่​เพราะ race ไม่มี​วัน​นิ่ง​พอ​จะ​เป็น​เงื่อนไข​ของ​ด่าน

ตอน​นี้​ตัวเลข​ฝั่ง WhenAll ถูก​รายงาน​และ​ถูก​เก็บ​ลง file ข้อมูล แต่​ไม่​ได้​ตัดสิน​ว่า​ด่าน เขียว​หรือ​แดง ส่วน​ฝั่ง foreach ที่​เป็น​ผล​แน่นอน​จริง ยัง​เป็น​เงื่อนไข​อยู่

คำถาม​ที่​ต้อง​ตอบ​ก่อน​แก้ ไม่ใช่ “เร็ว​ขึ้น​ไหม” แต่​คือ “งาน​แต่ละ​ชิ้น​เกี่ยวข้อง​กัน​หรือ​เปล่า”

ถาม​สาม​ข้อ​นี้​กับ​ตัว​วน​ที่​กำลัง​จะ​แก้

ลำดับ​ของ​การ​เรียก​มี​ความ​หมาย​ไหม ถ้า​มี ปล่อย foreach ไว้ แล้ว​เขียน​คอม​เมน​ต์​บอก​ว่า ทำไม เพราะ​คน​ถัด​ไป​จะ​มา​แก้​เป็น WhenAll ให้​คุณ

ตัว loop body แตะ​ของ​ที่​ใช้​ร่วม​กัน​หรือ​เปล่า ถ้า​แตะ ต้อง​แยก​ของ​นั้น​ให้​เป็น​คนละ​ตัว​ต่อ งาน​หนึ่ง​ชิ้น​ก่อน ไม่ใช่​แค่​ห่อ​ด้วย WhenAll

ปลาย​ทาง​รับ concurrency ได้​แค่​ไหน WhenAll ที่​จำนวน​งาน​มา​จาก input ของ​ผู้​ใช้ คือ การ​เปิด​ให้​ใคร​ก็ได้​สั่ง​ยิง​ปลาย​ทาง​กี่​ครั้ง​ก็ได้​ใน​คำขอ​เดียว เพดาน​มาตรฐาน​คือ SemaphoreSlim

เมื่อ​ทั้ง​สาม​ข้อ​ผ่าน​แล้ว WhenAll คือ​คำ​ตอบ และ​อย่า​ลืม​ว่า catch (AggregateException) รอบ​มัน​ไม่​ทำงาน

หน้า​นี้​ไม่​ได้​จบ​ด้วย “ใช้ WhenAll สิ” เพราะ​ตัวเลข​ไม่​ได้​บอก​อย่าง​นั้น

ตัวเลข​บอก​ว่า ไม่มี​ใคร​เตือน​คุณ​ทั้ง​สอง​ทาง ตอน​คุณ​เขียน foreach ที่​ควร​จะ​ขนาน ไม่มี diagnostic ตัว​ไหน​ดัง และ​ตอน​คุณ​เปลี่ยน​เป็น WhenAll ทับ​ตัว​วน​ที่​ลำดับ​มี​ความ​หมาย หรือ​ที่ loop body แตะ​ของ​ร่วม​กัน ก็​ไม่มี diagnostic ตัว​ไหน​ดัง​เหมือน​กัน

เครื่อง​มือ​ไม่​ได้​อยู่​ข้าง​ใด​ข้าง​หนึ่ง มัน​ไม่​อยู่​ใน​บท​สนทนา​นี้​เลย คน​ที่​ต้อง​รู้​ว่า​งาน​แต่ละ​ชิ้น เกี่ยวข้อง​กัน​หรือ​ไม่ คือ​คน​ที่​เขียน​มัน และ​นั่น​คือ​เหตุผล​ที่​ต้อง​เขียน​คอม​เมน​ต์​ทิ้ง​ไว้

หน้า​หนึ่ง​บน​ไซต์​นี้​เคย​เขียน​ข้อ​นี้​ผิด

คอร์ส Rust async บท​ที่ 7 เคย​เขียน​ว่า “ต่าง​กัน​แค่​ว่า​ใน C# บาง​ครั้ง analyzer ยัง​พอ​เตือน ได้​บ้าง” และ​เขียน​ซ้ำ​อีก​ครั้ง​ใน​ตาราง​เทียบ C# กับ Rust ท้าย​บท

ไม่มี​ใคร​วัด​ประโยค​นั้น​ก่อน​เขียน และ​หน้า​นี้​คือ​การ​วัด​มัน ผล​คือ​ประโยค​นั้น​ผิด — ฝั่ง C# ไม่​ได้ “พอ​เตือน​ได้​บ้าง” มัน​เงียบ​สนิท​เท่ากับ​ฝั่ง Rust ต่าง​กัน​ที่​ฝั่ง C# มี rule อยู่​ใกล้ ๆ สอง​ตัว​ที่​ไม่​ได้​ต่อ​กัน

บท​เรียน​นั้น​ถูก​แก้​ตาม​ผล​การ​วัด​นี้​แล้ว

สอง​คำ​สั่ง หนึ่ง​วัด​ใหม่​ทั้ง​ชุด อีก​หนึ่ง​พิสูจน์​ว่า​ตัว​ตรวจ​แดง​ได้​จริง

ตัว​วัด​ทั้ง​ชุด​ใช้ .NET SDK อย่าง​เดียว ไม่​ต้อง​มี Docker ไม่​ต้อง​มี​เน็ต และ​ไม่มี PackageReference สัก​ตัว

Terminal window
# วัดใหม่ทั้งชุดแล้วตรวจข้ออ้างของหน้านี้ — อยู่ในเกต unit ของ CI
python3 scripts/await-foreach/probe.py --check
# พิสูจน์ว่าด่านแดงได้ ยิงข้อมูลที่ทำให้ผิดทีละแบบใส่ตัวตรวจ
python3 scripts/await-foreach/probe.py --selftest

--check ไม่​ได้​เทียบ​กับ​ตัวเลข​ที่ commit ไว้​เฉย ๆ มัน​บิ​ลด์​ใหม่ รัน​ใหม่ แล้ว​ตรวจ​ว่า​รูป ของ​ผล​ยัง​เป็น​จริง​ไหม — ช่อง​ควบคุม​ยัง​ดัง​ทั้ง​สอง​ชั้น CA2014 ยัง​ดัง จำนวน diagnostic ที่​พูด​ถึง​ตัว​วน​ยัง​เป็น 0 และ await Task.WhenAll ยัง​ไม่​โยน AggregateException

ตัวเลข​ฝั่ง Task.WhenAll ไม่​ได้​วัด​ใหม่​ที่​นี่ มัน​มา​จาก scripts/whenall/ ซึ่ง​เป็น harness ที่​คอร์ส Rust async ใช้​อยู่​แล้ว probe รัน​โปร​เจ​กต์​นั้น​แล้ว​อ่าน​ผล​ของ​มัน เพื่อ​ไม่​ให้​ตัวเลข สอง​ชุด​ค่อย ๆ ห่าง​กัน​โดย​ไม่มี​อะไร​ฟ้อง

🔗 อ้างอิง​ต้นทาง
  • CA2007 — Do not directly await a Task — rule ที่​ชี้​ตรง​ที่ await ตัว​ที่​เป็น​ปัญหา แต่​พูด​เรื่อง ConfigureAwait (ตรวจ​แล้ว 2026-08-22)
  • CA2014 — Do not use stackalloc in loops — หลัก​ฐาน​ว่า analyzer อ่าน​ออก​ว่า​อะไร​อยู่​ใน​ตัว​วน (ตรวจ​แล้ว 2026-08-22)
  • CA1842 — Do not use WhenAll with a single task — rule ที่​ตั้ง​ชื่อ​ตาม Task.WhenAll ซึ่ง​อยู่​ใน roster (ตรวจ​แล้ว 2026-08-22)
  • Configure code analysis rules — ความ​หมาย​ของ AnalysisLevel และ AnalysisMode (ตรวจ​แล้ว 2026-08-22)
  • Task.WhenAll — พฤติกรรม​ของ task ที่​คืน​ออก​มา​เมื่อ​มี​ตัว​ที่ fault (ตรวจ​แล้ว 2026-08-22)
  • EF Core — Avoiding DbContext threading issues — ที่มา​ของ​ข้อความ “A second operation started on this context” (ตรวจ​แล้ว 2026-08-22)