.NET: เครื่องมือสำหรับการทำงานกับมัลติเธรดและอะซิงโครนัส ส่วนที่ 1

ฉันกำลังเผยแพร่บทความต้นฉบับเกี่ยวกับ Habr ซึ่งมีการแปลซึ่งโพสต์ไว้ในองค์กร โพสต์บล็อก.

ความจำเป็นในการทำบางสิ่งบางอย่างแบบอะซิงโครนัส โดยไม่ต้องรอผลลัพธ์ที่นี่และเดี๋ยวนี้ หรือการแบ่งงานใหญ่ออกเป็นหลายหน่วยที่ทำสิ่งนั้น นั้นมีมาก่อนการถือกำเนิดของคอมพิวเตอร์ ความต้องการนี้ก็จับต้องได้ชัดเจนมาก ตอนนี้ในปี 2019 ฉันกำลังพิมพ์บทความนี้บนแล็ปท็อปที่มีโปรเซสเซอร์ Intel Core 8-core ซึ่งมีกระบวนการมากกว่าหนึ่งร้อยกระบวนการทำงานแบบขนานและยังมีเธรดอีกมากมาย บริเวณใกล้เคียงมีโทรศัพท์โทรมเล็กน้อยซื้อมาเมื่อสองสามปีที่แล้วมีโปรเซสเซอร์ 8 คอร์อยู่บนเครื่อง แหล่งข้อมูลเฉพาะเรื่องเต็มไปด้วยบทความและวิดีโอที่ผู้เขียนชื่นชมสมาร์ทโฟนเรือธงประจำปีนี้ซึ่งมีโปรเซสเซอร์ 16 คอร์ MS Azure มอบเครื่องเสมือนที่มีโปรเซสเซอร์ 20 คอร์และ RAM ขนาด 128 TB ในราคาต่ำกว่า 2 ดอลลาร์ต่อชั่วโมง น่าเสียดายที่เป็นไปไม่ได้ที่จะดึงเอาค่าสูงสุดออกมาและควบคุมพลังนี้โดยไม่สามารถจัดการการโต้ตอบของเธรดได้

คำศัพท์

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

คำอุปมา

ไม่ใช่คำจำกัดความทั้งหมดจะดีและบางคำจำกัดความต้องการคำอธิบายเพิ่มเติม ดังนั้น ฉันจะเพิ่มคำอุปมาเกี่ยวกับการทำอาหารอาหารเช้าให้กับคำศัพท์ที่ใช้อย่างเป็นทางการ การทำอาหารเช้าตามคำอุปมานี้เป็นกระบวนการ

ขณะเตรียมอาหารเช้าในตอนเช้าฉัน (ซีพียู) ฉันมาที่ครัว (คอมพิวเตอร์). ฉันมี 2 มือ (แกน). ในครัวมีอุปกรณ์หลายอย่าง (IO): เตาอบ กาต้มน้ำ เครื่องปิ้งขนมปัง ตู้เย็น ฉันเปิดแก๊ส ตั้งกระทะแล้วเทน้ำมันลงไปโดยไม่ต้องรอให้ร้อน (แบบอะซิงโครนัส, ไม่ปิดกั้น-IO-รอ) ฉันนำไข่ออกจากตู้เย็นแล้วแบ่งเป็นจานแล้วตีด้วยมือเดียว (กระทู้#1) และวินาที (กระทู้#2) ถือจาน (Shared Resource) ตอนนี้ฉันอยากจะเปิดกาต้มน้ำแต่มือมีไม่พอ (ความอดอยากด้าย) ระหว่างนี้กระทะจะร้อนขึ้น (กำลังประมวลผลผลลัพธ์) ซึ่งฉันเทสิ่งที่ฉันวิปปิ้งลงไป ฉันเอื้อมไปหยิบกาต้มน้ำแล้วเปิดดูน้ำเดือดในนั้นอย่างโง่เขลา (การบล็อก-IO-รอ) แม้ว่าในช่วงเวลานี้เขาสามารถล้างจานที่เขาตีไข่เจียวได้

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

อุปมาอุปไมยต่อไป:

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

เครื่องมือ.NET

.NET ทำงานกับเธรดได้ดี เช่นเดียวกับสิ่งอื่นๆ อีกมากมาย ในแต่ละเวอร์ชันใหม่ จะมีการแนะนำเครื่องมือใหม่ๆ มากขึ้นเรื่อยๆ สำหรับการทำงานร่วมกับพวกเขา ซึ่งเป็นเลเยอร์ใหม่ของสิ่งที่เป็นนามธรรมบนเธรด OS เมื่อทำงานกับการสร้างนามธรรม นักพัฒนาเฟรมเวิร์กใช้แนวทางที่ทิ้งโอกาส เมื่อใช้นามธรรมระดับสูง เพื่อลงไปที่ระดับด้านล่างอย่างน้อยหนึ่งระดับ ส่วนใหญ่มักไม่จำเป็น อันที่จริงเป็นการเปิดประตูให้ยิงตัวเองด้วยปืนลูกซอง แต่บางครั้งในบางกรณีที่พบไม่บ่อยนัก อาจเป็นวิธีเดียวที่จะแก้ปัญหาที่ไม่ได้รับการแก้ไขในระดับนามธรรมในปัจจุบัน .

โดยเครื่องมือ ฉันหมายถึงทั้ง Application Programming Interfaces (API) ที่จัดทำโดยเฟรมเวิร์กและแพ็คเกจของบุคคลที่สาม รวมถึงโซลูชันซอฟต์แวร์ทั้งหมดที่ช่วยลดความยุ่งยากในการค้นหาปัญหาใด ๆ ที่เกี่ยวข้องกับโค้ดแบบมัลติเธรด

การเริ่มเธรด

คลาส Thread เป็นคลาสพื้นฐานที่สุดใน .NET สำหรับการทำงานกับเธรด ตัวสร้างยอมรับหนึ่งในสองคนที่ได้รับมอบหมาย:

  • ThreadStart — ไม่มีพารามิเตอร์
  • ParametrizedThreadStart - มีพารามิเตอร์ประเภทวัตถุหนึ่งรายการ

ผู้รับมอบสิทธิ์จะถูกดำเนินการในเธรดที่สร้างขึ้นใหม่หลังจากเรียกใช้เมธอด Start หากผู้รับมอบสิทธิ์ประเภท ParametrizedThreadStart ถูกส่งผ่านไปยัง Constructor ดังนั้นวัตถุจะต้องส่งผ่านไปยังเมธอด Start กลไกนี้จำเป็นในการถ่ายโอนข้อมูลท้องถิ่นไปยังสตรีม เป็นที่น่าสังเกตว่าการสร้างเธรดเป็นการดำเนินการที่มีราคาแพง และตัวเธรดเองก็เป็นอ็อบเจ็กต์ที่มีน้ำหนักมาก อย่างน้อยก็เพราะมันจัดสรรหน่วยความจำ 1MB บนสแต็กและต้องมีการโต้ตอบกับ OS API

new Thread(...).Start(...);

คลาส ThreadPool แสดงถึงแนวคิดของพูล ใน .NET เธรดพูลถือเป็นงานวิศวกรรมชิ้นหนึ่ง และนักพัฒนาของ Microsoft ได้ใช้ความพยายามอย่างมากเพื่อให้แน่ใจว่าเธรดพูลจะทำงานได้อย่างเหมาะสมในสถานการณ์ที่หลากหลาย

แนวคิดทั่วไป:

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

เมื่อต้องการใช้เธรดจากพูล มีเมธอด QueueUserWorkItem ที่ยอมรับผู้รับมอบสิทธิ์ประเภท WaitCallback ซึ่งมีลายเซ็นเดียวกันกับ ParametrizedThreadStart และพารามิเตอร์ที่ส่งผ่านไปยังจะทำหน้าที่เดียวกัน

ThreadPool.QueueUserWorkItem(...);

เมธอดพูลเธรดที่ไม่ค่อยมีคนรู้จัก RegisterWaitForSingleObject ใช้เพื่อจัดระเบียบการดำเนินการ IO ที่ไม่บล็อก ผู้รับมอบสิทธิ์ที่ส่งผ่านไปยังเมธอดนี้จะถูกเรียกเมื่อ WaitHandle ที่ส่งผ่านไปยังเมธอดนี้คือ "Released"

ThreadPool.RegisterWaitForSingleObject(...)

.NET มีตัวจับเวลาเธรด และแตกต่างจากตัวจับเวลา WinForms/WPF ตรงที่ตัวจัดการจะถูกเรียกบนเธรดที่นำมาจากพูล

System.Threading.Timer

นอกจากนี้ยังมีวิธีที่ค่อนข้างแปลกใหม่ในการส่งผู้รับมอบสิทธิ์เพื่อดำเนินการไปยังเธรดจากพูล - วิธี BeginInrigg

DelegateInstance.BeginInvoke

ฉันต้องการที่จะอาศัยฟังก์ชันสั้น ๆ ซึ่งสามารถเรียกวิธีการข้างต้นได้หลายวิธี - CreateThread จาก Kernel32.dll Win32 API มีวิธีเรียกใช้ฟังก์ชันนี้ด้วยกลไกของวิธีการภายนอก ฉันเคยเห็นการโทรดังกล่าวเพียงครั้งเดียวในตัวอย่างที่แย่มากของรหัสดั้งเดิม และแรงจูงใจของผู้เขียนที่ทำสิ่งนี้ยังคงเป็นปริศนาสำหรับฉัน

Kernel32.dll CreateThread

การดูและการดีบักเธรด

เธรดที่คุณสร้างขึ้น ส่วนประกอบของบริษัทอื่นทั้งหมด และพูล .NET สามารถดูได้ในหน้าต่างเธรดของ Visual Studio หน้าต่างนี้จะแสดงข้อมูลเธรดเมื่อแอปพลิเคชันอยู่ระหว่างการดีบักและอยู่ในโหมดพักเท่านั้น ที่นี่คุณสามารถดูชื่อสแต็กและลำดับความสำคัญของแต่ละเธรดได้อย่างสะดวก และสลับการดีบักไปยังเธรดที่ต้องการได้ การใช้คุณสมบัติลำดับความสำคัญของคลาส Thread คุณสามารถตั้งค่าลำดับความสำคัญของเธรดได้ ซึ่ง OC และ CLR จะมองว่าเป็นคำแนะนำเมื่อแบ่งเวลาตัวประมวลผลระหว่างเธรด

.NET: เครื่องมือสำหรับการทำงานกับมัลติเธรดและอะซิงโครนัส ส่วนที่ 1

งานไลบรารีแบบขนาน

Task Parallel Library (TPL) เปิดตัวใน .NET 4.0 ตอนนี้มันเป็นมาตรฐานและเครื่องมือหลักในการทำงานกับอะซิงโครนัส โค้ดใดๆ ที่ใช้วิธีการแบบเก่าจะถือว่าเป็นโค้ดดั้งเดิม หน่วยพื้นฐานของ TPL คือคลาสงานจากเนมสเปซ System.Threading.Tasks งานคือสิ่งที่เป็นนามธรรมเหนือเธรด ด้วยภาษา C# เวอร์ชันใหม่ เรามีวิธีที่ยอดเยี่ยมในการทำงานกับ Tasks - ตัวดำเนินการ async/await แนวคิดเหล่านี้ทำให้สามารถเขียนโค้ดแบบอะซิงโครนัสได้ราวกับว่าเป็นแบบเรียบง่ายและซิงโครนัส ทำให้แม้แต่ผู้ที่มีความเข้าใจเพียงเล็กน้อยเกี่ยวกับการทำงานภายในของเธรดก็สามารถเขียนแอปพลิเคชันที่ใช้งานแอปพลิเคชันเหล่านั้น ซึ่งเป็นแอปพลิเคชันที่ไม่หยุดทำงานเมื่อดำเนินการเป็นเวลานาน การใช้ async/await เป็นหัวข้อสำหรับบทความหนึ่งหรือหลายบทความ แต่ฉันจะพยายามทำความเข้าใจสาระสำคัญในประโยคสองสามประโยค:

  • async เป็นตัวดัดแปลงของวิธีการส่งคืนงานหรือเป็นโมฆะ
  • และ await เป็นตัวดำเนินการรองานที่ไม่มีการปิดกั้น

อีกครั้ง: ในกรณีทั่วไป await โอเปอเรเตอร์ (มีข้อยกเว้น) จะปล่อยเธรดการดำเนินการปัจจุบันเพิ่มเติม และเมื่องานเสร็จสิ้นการดำเนินการ และเธรด (อันที่จริง มันจะถูกต้องมากกว่าหากพูดบริบท แต่จะเพิ่มเติมในภายหลัง) จะดำเนินการวิธีการต่อไป ภายใน .NET กลไกนี้ถูกนำไปใช้ในลักษณะเดียวกับผลตอบแทนผลตอบแทน เมื่อวิธีการเขียนกลายเป็นทั้งคลาส ซึ่งเป็นเครื่องสถานะและสามารถดำเนินการแยกส่วนได้ ขึ้นอยู่กับสถานะเหล่านี้ ใครก็ตามที่สนใจสามารถเขียนโค้ดง่ายๆ โดยใช้ asynс/await คอมไพล์และดูแอสเซมบลีโดยใช้ JetBrains dotPeek โดยเปิดใช้งาน Compiler Generated Code

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

using TCO = System.Threading.Tasks.TaskCreationOptions;

public static async void VoidAsyncMethod() {
    var cancellationSource = new CancellationTokenSource();

    await Task.Factory.StartNew(
        // Code of action will be executed on other context
        () => Thread.Sleep(10000),
        cancellationSource.Token,
        TCO.LongRunning | TCO.AttachedToParent | TCO.PreferFairness,
        scheduler
    );

    //  Code after await will be executed on captured context
}

งานจะถูกสร้างขึ้นโดยมีตัวเลือกต่างๆ มากมาย:

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

พารามิเตอร์ตัวที่สองที่ส่งผ่านไปยังเมธอดคือ CancellationToken เพื่อจัดการกับการยกเลิกการดำเนินการอย่างถูกต้องหลังจากที่เริ่มต้นแล้ว รหัสที่กำลังดำเนินการจะต้องเต็มไปด้วยเช็คสำหรับสถานะ CancellationToken หากไม่มีการตรวจสอบ เมธอด Cancel ที่เรียกบนออบเจ็กต์ CancellationTokenSource จะสามารถหยุดการทำงานของงานได้ก่อนที่จะเริ่มเท่านั้น

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

ตัวดำเนินการ await ใช้กับงานที่สร้างขึ้น ซึ่งหมายความว่าโค้ดที่เขียนหลังจากนั้น (หากมี) จะถูกดำเนินการในบริบทเดียวกัน (ซึ่งมักจะหมายถึงในเธรดเดียวกัน) เช่นเดียวกับโค้ดก่อน await

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

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

เรามาดูรายละเอียดเพิ่มเติมอีกหน่อยว่าคุณจะรอให้งานเสร็จสิ้นได้อย่างไร ด้านล่างนี้เป็นตัวอย่างของโค้ด พร้อมความคิดเห็นว่าเมื่อใดที่ความคาดหวังเสร็จสิ้นด้วยดีแบบมีเงื่อนไข และเมื่อใดที่ทำได้ไม่ดีตามเงื่อนไข

public static async void AnotherMethod() {

    int result = await AsyncMethod(); // good

    result = AsyncMethod().Result; // bad

    AsyncMethod().Wait(); // bad

    IEnumerable<Task> tasks = new Task[] {
        AsyncMethod(), OtherAsyncMethod()
    };

    await Task.WhenAll(tasks); // good
    await Task.WhenAny(tasks); // good

    Task.WaitAll(tasks.ToArray()); // bad
}

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

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

ข้อเสียอีกประการหนึ่งของวิธีนี้คือการจัดการข้อผิดพลาดที่ซับซ้อน ความจริงก็คือข้อผิดพลาดในโค้ดอะซิงโครนัสเมื่อใช้ async/await นั้นง่ายต่อการจัดการ - ข้อผิดพลาดเหล่านั้นมีพฤติกรรมเหมือนกับว่าโค้ดเป็นแบบซิงโครนัส แม้ว่าเราจะใช้การไล่ผีแบบรอแบบซิงโครนัสกับงาน ข้อยกเว้นดั้งเดิมจะกลายเป็น AggregateException กล่าวคือ ในการจัดการกับข้อยกเว้น คุณจะต้องตรวจสอบประเภท InnerException และเขียน if chain ด้วยตัวคุณเองภายใน catch block เดียว หรือใช้ catch เมื่อสร้าง แทนที่จะเป็นลูกโซ่ของ catch block ที่คุ้นเคยมากกว่าในโลก C#

ตัวอย่างที่สามและสุดท้ายก็ถูกทำเครื่องหมายว่าไม่ดีด้วยเหตุผลเดียวกันและมีปัญหาเดียวกันทั้งหมด

เมธอด WhenAny และ WhenAll นั้นสะดวกอย่างยิ่งสำหรับการรอกลุ่มของงาน โดยรวมกลุ่มของงานเป็นหนึ่งเดียว ซึ่งจะเริ่มทำงานเมื่อมีการทริกเกอร์งานจากกลุ่มเป็นครั้งแรก หรือเมื่อทั้งหมดเสร็จสิ้นการดำเนินการแล้ว

กำลังหยุดเธรด

ด้วยเหตุผลหลายประการ อาจจำเป็นต้องหยุดโฟลว์หลังจากที่เริ่มต้นแล้ว มีหลายวิธีในการทำเช่นนี้ คลาส Thread มีสองวิธีที่มีชื่ออย่างเหมาะสม: ทำแท้ง и ขัดขวาง. อันแรกไม่แนะนำให้ใช้อย่างยิ่งเพราะว่า หลังจากเรียกมันในช่วงเวลาสุ่มใด ๆ ในระหว่างการประมวลผลคำสั่งใด ๆ ข้อยกเว้นจะถูกส่งออกไป ThreadAbortedException. คุณไม่คาดหวังว่าจะมีข้อยกเว้นดังกล่าวเกิดขึ้นเมื่อเพิ่มตัวแปรจำนวนเต็มใช่ไหม และเมื่อใช้วิธีนี้นี่เป็นสถานการณ์จริงมาก หากคุณต้องการป้องกันไม่ให้ CLR สร้างข้อยกเว้นดังกล่าวในบางส่วนของโค้ด คุณสามารถล้อมมันไว้ในการโทรได้ Thread.BeginCriticalRegion, Thread.EndCriticalRegion. รหัสใด ๆ ที่เขียนในบล็อกสุดท้ายจะถูกรวมไว้ในการโทรดังกล่าว ด้วยเหตุนี้ ในส่วนลึกของโค้ดเฟรมเวิร์ก คุณจะพบบล็อกที่มีการลองว่าง แต่สุดท้ายก็ไม่ว่างเปล่า Microsoft ไม่สนับสนุนวิธีนี้มากจนไม่ได้รวมไว้ใน. net core

วิธีการขัดจังหวะทำงานได้อย่างคาดเดาได้มากขึ้น มันสามารถขัดจังหวะเธรดด้วยข้อยกเว้น เธรดInterruptedException เฉพาะช่วงเวลาที่เธรดอยู่ในสถานะรอเท่านั้น จะเข้าสู่สถานะนี้ขณะหยุดทำงานขณะรอ WaitHandle ล็อก หรือหลังจากเรียก Thread.Sleep

ตัวเลือกทั้งสองที่อธิบายไว้ข้างต้นนั้นไม่ดีเนื่องจากไม่สามารถคาดเดาได้ วิธีแก้ไขคือการใช้โครงสร้าง โทเค็นการยกเลิก และชั้นเรียน แหล่งที่มาของการยกเลิกโทเค็น. ประเด็นก็คือ: มีการสร้างอินสแตนซ์ของคลาส CancellationTokenSource และมีเพียงผู้ที่เป็นเจ้าของเท่านั้นที่สามารถหยุดการดำเนินการได้โดยการเรียกเมธอด ยกเลิก. เฉพาะ CancellationToken เท่านั้นที่จะถูกส่งผ่านไปยังการดำเนินการ เจ้าของ CancellationToken ไม่สามารถยกเลิกการดำเนินการได้ด้วยตนเอง แต่สามารถตรวจสอบได้ว่าการดำเนินการถูกยกเลิกเท่านั้น มีคุณสมบัติบูลีนสำหรับสิ่งนี้ มีการร้องขอการยกเลิก และวิธีการ ThrowIfCancelRequested. หลังจะโยนข้อยกเว้น งาน CancelledException ถ้ามีการเรียกใช้เมธอด Cancel บนอินสแตนซ์ CancellationToken ที่กำลังถูกนกแก้ว และนี่คือวิธีที่ผมแนะนำให้ใช้ นี่คือการปรับปรุงตัวเลือกก่อนหน้านี้โดยได้รับการควบคุมเต็มรูปแบบ ณ จุดใดที่สามารถยกเลิกการดำเนินการข้อยกเว้นได้

ตัวเลือกที่โหดร้ายที่สุดสำหรับการหยุดเธรดคือการเรียกใช้ฟังก์ชัน Win32 API TerminateThread ลักษณะการทำงานของ CLR หลังจากการเรียกใช้ฟังก์ชันนี้อาจคาดเดาไม่ได้ ใน MSDN มีการเขียนเกี่ยวกับฟังก์ชันนี้ดังนี้: “TerminateThread เป็นฟังก์ชันอันตรายที่ควรใช้ในกรณีที่ร้ายแรงที่สุดเท่านั้น “

การแปลง API ดั้งเดิมเป็นงานตามโดยใช้วิธี FromAsync

หากคุณโชคดีพอที่จะได้ทำงานในโปรเจ็กต์ที่เริ่มต้นหลังจากที่มีการเปิดตัว Tasks และหยุดสร้างความสยองขวัญเงียบ ๆ สำหรับนักพัฒนาส่วนใหญ่ คุณจะไม่ต้องจัดการกับ API เก่า ๆ มากมาย ทั้งจากบุคคลที่สามและทีมของคุณ เคยถูกทรมานในอดีต โชคดีที่ทีมงาน .NET Framework ดูแลเรา แม้ว่าเป้าหมายอาจเป็นการดูแลตัวเองก็ตาม อย่างไรก็ตาม .NET มีเครื่องมือมากมายสำหรับการแปลงโค้ดที่เขียนด้วยการเขียนโปรแกรมแบบอะซิงโครนัสแบบเก่าไปเป็นโค้ดใหม่อย่างง่ายดาย หนึ่งในนั้นคือวิธี FromAsync ของ TaskFactory ในตัวอย่างโค้ดด้านล่าง ฉันรวมวิธีอะซิงโครนัสเก่าของคลาส WebRequest ไว้ในงานโดยใช้วิธีนี้

object state = null;
WebRequest wr = WebRequest.CreateHttp("http://github.com");
await Task.Factory.FromAsync(
    wr.BeginGetResponse,
    we.EndGetResponse
);

นี่เป็นเพียงตัวอย่างและไม่น่าจะต้องทำสิ่งนี้กับประเภทที่มีอยู่แล้วภายใน แต่โปรเจ็กต์เก่าๆ เต็มไปด้วยเมธอด BeginDoSomething ที่ส่งคืนเมธอด IAsyncResult และ EndDoSomething ที่ได้รับ

แปลง API เดิมเป็นงานโดยใช้คลาส TaskCompletionSource

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

คุณจะบอกว่าฉันได้พูดคุยเกี่ยวกับวิธี FromAsync ของคลาส TaskFactory ที่มีไว้สำหรับวัตถุประสงค์เหล่านี้แล้ว ที่นี่เราจะต้องจดจำประวัติทั้งหมดของการพัฒนาโมเดลอะซิงโครนัสใน .net ที่ Microsoft นำเสนอในช่วง 15 ปีที่ผ่านมา: ก่อนที่ Task-Based Asynchronous Pattern (TAP) จะมี Asynchronous Programming Pattern (APP) ซึ่ง เป็นเกี่ยวกับวิธีการ เริ่มต้นทำอะไรบางอย่างกลับมา IAsyncResult และวิธีการ ปลายทำบางสิ่งบางอย่างที่ยอมรับและสำหรับมรดกของปีเหล่านี้ วิธีการ FromAsync นั้นสมบูรณ์แบบ แต่เมื่อเวลาผ่านไป มันก็ถูกแทนที่ด้วยรูปแบบอะซิงโครนัสตามเหตุการณ์ (EAP) ซึ่งสันนิษฐานว่าเหตุการณ์จะเกิดขึ้นเมื่อการดำเนินการแบบอะซิงโครนัสเสร็จสิ้น

TaskCompletionSource เหมาะอย่างยิ่งสำหรับการรวมงานและ API เดิมที่สร้างขึ้นรอบโมเดลเหตุการณ์ สาระสำคัญของงานมีดังนี้: วัตถุของคลาสนี้มีคุณสมบัติสาธารณะประเภทงานซึ่งสถานะสามารถควบคุมได้ผ่านเมธอด SetResult, SetException ฯลฯ ของคลาส TaskCompletionSource ในตำแหน่งที่มีการนำตัวดำเนินการ await มาใช้กับงานนี้ งานจะถูกดำเนินการหรือล้มเหลวโดยมีข้อยกเว้น ขึ้นอยู่กับวิธีการที่ใช้กับ TaskCompletionSource หากยังไม่ชัดเจน ลองดูตัวอย่างโค้ดนี้ โดยที่ EAP API เก่าบางตัวถูกรวมไว้ในงานโดยใช้ TaskCompletionSource: เมื่อเหตุการณ์เริ่มทำงาน งานจะถูกโอนไปยังสถานะเสร็จสมบูรณ์ และวิธีการที่ใช้ตัวดำเนินการ await งานนี้จะกลับมาดำเนินการต่อเมื่อได้รับวัตถุแล้ว ผล.

public static Task<Result> DoAsync(this SomeApiInstance someApiObj) {

    var completionSource = new TaskCompletionSource<Result>();
    someApiObj.Done += 
        result => completionSource.SetResult(result);
    someApiObj.Do();

    result completionSource.Task;
}

เคล็ดลับและเทคนิคแหล่งที่มาของงานเสร็จสมบูรณ์

การห่อ API เก่าไม่ใช่ทั้งหมดที่สามารถทำได้โดยใช้ TaskCompletionSource การใช้คลาสนี้เปิดโอกาสให้มีความเป็นไปได้ที่น่าสนใจในการออกแบบ API ต่างๆ บนงานที่ไม่ได้ใช้เธรด อย่างที่เราจำได้ว่าสตรีมนั้นเป็นทรัพยากรที่มีราคาแพงและมีจำนวนจำกัด (โดยหลักคือจำนวน RAM) ข้อจำกัดนี้สามารถทำได้ง่ายๆ โดยการพัฒนา เช่น เว็บแอปพลิเคชันที่โหลดซึ่งมีตรรกะทางธุรกิจที่ซับซ้อน ลองพิจารณาความเป็นไปได้ที่ฉันกำลังพูดถึงเมื่อใช้เคล็ดลับเช่น Long-Polling

กล่าวโดยสรุป สาระสำคัญของเคล็ดลับคือ: คุณต้องได้รับข้อมูลจาก API เกี่ยวกับเหตุการณ์บางอย่างที่เกิดขึ้นในฝั่งของมัน ในขณะที่ API ไม่สามารถรายงานเหตุการณ์ได้ด้วยเหตุผลบางประการ แต่สามารถคืนสถานะได้เท่านั้น ตัวอย่างของสิ่งเหล่านี้คือ API ทั้งหมดที่สร้างขึ้นบน HTTP ก่อนเวลาของ WebSocket หรือเมื่อเป็นไปไม่ได้ที่จะใช้เทคโนโลยีนี้ด้วยเหตุผลบางประการ ลูกค้าสามารถถามเซิร์ฟเวอร์ HTTP เซิร์ฟเวอร์ HTTP ไม่สามารถเริ่มต้นการสื่อสารกับไคลเอนต์ได้ วิธีแก้ปัญหาง่ายๆ คือการสำรวจเซิร์ฟเวอร์โดยใช้ตัวจับเวลา แต่สิ่งนี้จะสร้างโหลดเพิ่มเติมบนเซิร์ฟเวอร์และความล่าช้าเพิ่มเติมโดยเฉลี่ย TimerInterval / 2 เพื่อหลีกเลี่ยงปัญหานี้ มีการคิดค้นเคล็ดลับที่เรียกว่า Long Polling ซึ่งเกี่ยวข้องกับการชะลอการตอบสนองจาก เซิร์ฟเวอร์จนกว่าการหมดเวลาจะหมดลงหรือเหตุการณ์จะเกิดขึ้น หากเหตุการณ์เกิดขึ้น ก็จะได้รับการประมวลผล หากไม่เกิดขึ้น คำขอจะถูกส่งอีกครั้ง

while(!eventOccures && !timeoutExceeded)  {

  CheckTimout();
  CheckEvent();
  Thread.Sleep(1);
}

แต่วิธีแก้ปัญหาดังกล่าวจะแย่มากทันทีที่จำนวนลูกค้าที่รองานเพิ่มขึ้น เพราะ... ไคลเอนต์ดังกล่าวแต่ละรายใช้เธรดทั้งหมดเพื่อรอเหตุการณ์ ใช่ และเราได้รับความล่าช้าเพิ่มเติม 1 มิลลิวินาทีเมื่อเหตุการณ์ถูกทริกเกอร์ ซึ่งส่วนใหญ่มักไม่มีนัยสำคัญ แต่เหตุใดจึงทำให้ซอฟต์แวร์แย่ลงกว่าเดิม หากเราลบ Thread.Sleep(1) ออก เราจะโหลดคอร์โปรเซสเซอร์หนึ่งคอร์ที่ไม่ได้ใช้งาน 100% โดยการหมุนแบบไร้ประโยชน์ การใช้ TaskCompletionSource ช่วยให้คุณสามารถสร้างโค้ดนี้ใหม่และแก้ไขปัญหาทั้งหมดที่ระบุไว้ข้างต้นได้อย่างง่ายดาย:

class LongPollingApi {

    private Dictionary<int, TaskCompletionSource<Msg>> tasks;

    public async Task<Msg> AcceptMessageAsync(int userId, int duration) {

        var cs = new TaskCompletionSource<Msg>();
        tasks[userId] = cs;
        await Task.WhenAny(Task.Delay(duration), cs.Task);
        return cs.Task.IsCompleted ? cs.Task.Result : null;
    }

    public void SendMessage(int userId, Msg m) {

        if (tasks.TryGetValue(userId, out var completionSource))
            completionSource.SetResult(m);
    }
}

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

เมื่อเราได้รับคำขอข้อความ เราจะสร้างและวาง TaskCompletionSource ไว้ในพจนานุกรม จากนั้นรอสิ่งที่เกิดขึ้นก่อน: ช่วงเวลาที่ระบุหมดอายุหรือได้รับข้อความ

ValueTask: ทำไมและอย่างไร

ตัวดำเนินการ async/await เช่นเดียวกับตัวดำเนินการคืนผลตอบแทน จะสร้างเครื่องสถานะจากวิธีการ และนี่คือการสร้างออบเจ็กต์ใหม่ ซึ่งแทบจะไม่มีความสำคัญเสมอไป แต่ในบางกรณีที่เกิดขึ้นไม่บ่อยนัก ก็สามารถสร้างปัญหาได้ กรณีนี้อาจจะเป็นวิธีการที่เรียกกันบ่อยจริง ๆ เรากำลังพูดถึงการโทรหลายหมื่นครั้งต่อวินาที หากวิธีการดังกล่าวถูกเขียนในลักษณะที่โดยส่วนใหญ่แล้วจะส่งกลับผลลัพธ์โดยข้ามวิธีการรอทั้งหมด ดังนั้น .NET จึงมีเครื่องมือในการเพิ่มประสิทธิภาพสิ่งนี้ - โครงสร้าง ValueTask เพื่อให้ชัดเจน มาดูตัวอย่างการใช้งานกันดีกว่า มีแคชที่เราเข้าบ่อยมาก มีค่าบางอย่างอยู่ในนั้น จากนั้นเราก็ส่งคืนค่าเหล่านั้น หากไม่ใช่ เราก็ไปที่ IO ที่ช้าเพื่อรับค่าเหล่านั้น ฉันต้องการทำอย่างหลังแบบอะซิงโครนัส ซึ่งหมายความว่าวิธีการทั้งหมดกลายเป็นแบบอะซิงโครนัส ดังนั้นวิธีเขียนวิธีนี้ที่ชัดเจนจึงเป็นดังนี้:

public async Task<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return val;
    return await RequestById(id);
}

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

public Task<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return Task.FromResult(val);
    return RequestById(id);
}

แท้จริงแล้ว ทางออกที่ดีที่สุดในกรณีนี้คือการปรับฮอตพาธให้เหมาะสม กล่าวคือ รับค่าจากพจนานุกรมโดยไม่มีการจัดสรรที่ไม่จำเป็นและโหลดบน GC ในขณะที่ในกรณีที่พบไม่บ่อยเหล่านั้นเมื่อเรายังคงต้องไปที่ IO เพื่อดูข้อมูล ทุกอย่างจะยังคงบวก/ลบแบบเดิม:

public ValueTask<string> GetById(int id) {

    if (cache.TryGetValue(id, out string val))
        return new ValueTask<string>(val);
    return new ValueTask<string>(RequestById(id));
}

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

TaskSchedulers: การจัดการกลยุทธ์การเปิดตัวงาน

API ถัดไปที่ฉันต้องการพิจารณาคือคลาส กำหนดการงาน และอนุพันธ์ของมัน ฉันได้กล่าวไปแล้วข้างต้นว่า TPL มีความสามารถในการจัดการกลยุทธ์ในการกระจายงานข้ามเธรด กลยุทธ์ดังกล่าวถูกกำหนดไว้ในลูกหลานของคลาส TaskScheduler เกือบทุกกลยุทธ์ที่คุณอาจต้องการมีอยู่ในห้องสมุด Parallel Extensionsพิเศษพัฒนาโดย Microsoft แต่ไม่ใช่ส่วนหนึ่งของ .NET แต่จัดทำเป็นแพ็คเกจ Nuget ลองดูบางส่วนโดยย่อ:

  • CurrentThreadTaskScheduler — รันงานบนเธรดปัจจุบัน
  • ConcurrencyLevelTaskScheduler ที่จำกัด — จำกัดจำนวนของงานที่ดำเนินการพร้อมกันโดยพารามิเตอร์ N ซึ่งเป็นที่ยอมรับในตัวสร้าง
  • ตัวกำหนดเวลางานที่ได้รับคำสั่ง — ถูกกำหนดให้เป็น LimitedConcurrencyLevelTaskScheduler(1) ดังนั้นงานต่างๆ จะถูกดำเนินการตามลำดับ
  • WorkStealingTaskScheduler - ดำเนินการ การขโมยงาน แนวทางการกระจายงาน โดยพื้นฐานแล้วมันเป็น ThreadPool ที่แยกจากกัน แก้ไขปัญหาที่ใน .NET ThreadPool เป็นคลาสแบบคงที่ คลาสหนึ่งสำหรับแอปพลิเคชันทั้งหมด ซึ่งหมายความว่าการโอเวอร์โหลดหรือการใช้งานที่ไม่ถูกต้องในส่วนหนึ่งของโปรแกรมอาจทำให้เกิดผลข้างเคียงในอีกส่วนได้ ยิ่งไปกว่านั้น เป็นการยากมากที่จะเข้าใจสาเหตุของข้อบกพร่องดังกล่าว ที่. อาจจำเป็นต้องใช้ WorkStealingTaskSchedulers แยกต่างหากในส่วนของโปรแกรมซึ่งการใช้ ThreadPool อาจรุนแรงและคาดเดาไม่ได้
  • QueuedTaskScheduler — อนุญาตให้คุณทำงานตามกฎคิวลำดับความสำคัญ
  • ThreadPerTaskScheduler — สร้างเธรดแยกต่างหากสำหรับแต่ละงานที่ดำเนินการ อาจมีประโยชน์สำหรับงานที่ใช้เวลานานในการดำเนินการให้เสร็จสิ้นโดยไม่อาจคาดเดาได้

มีรายละเอียดดี บทความ เกี่ยวกับ TaskSchedulers ในบล็อกของ Microsoft

เพื่อความสะดวกในการแก้ไขทุกสิ่งที่เกี่ยวข้องกับงาน Visual Studio มีหน้าต่างงาน ในหน้าต่างนี้ คุณสามารถดูสถานะปัจจุบันของงาน และข้ามไปยังบรรทัดโค้ดที่กำลังดำเนินการอยู่

.NET: เครื่องมือสำหรับการทำงานกับมัลติเธรดและอะซิงโครนัส ส่วนที่ 1

PLinq และคลาส Parallel

นอกจาก Tasks และทุกสิ่งที่กล่าวถึงแล้ว ยังมีเครื่องมือที่น่าสนใจอีกสองเครื่องมือใน .NET: PLinq (Linq2Parallel) และคลาส Parallel ครั้งแรกสัญญาว่าจะดำเนินการแบบขนานของการดำเนินการ Linq ทั้งหมดบนหลายเธรด จำนวนเธรดสามารถกำหนดค่าได้โดยใช้วิธีการขยาย WithDegreeOfParallelism น่าเสียดายที่ PLinq ส่วนใหญ่ในโหมดเริ่มต้นไม่มีข้อมูลเพียงพอเกี่ยวกับแหล่งข้อมูลภายในของคุณเพื่อเพิ่มความเร็วอย่างมีนัยสำคัญ ในทางกลับกัน ค่าใช้จ่ายในการลองต่ำมาก: คุณเพียงแค่ต้องเรียกใช้เมธอด AsParallel ก่อน ห่วงโซ่ของวิธี Linq และดำเนินการทดสอบประสิทธิภาพ ยิ่งไปกว่านั้น ยังสามารถส่งข้อมูลเพิ่มเติมไปยัง Plinq เกี่ยวกับลักษณะของแหล่งข้อมูลของคุณโดยใช้กลไกพาร์ติชั่นได้ คุณสามารถอ่านเพิ่มเติมได้ ที่นี่ и ที่นี่.

คลาสคงที่แบบขนานจัดเตรียมวิธีการวนซ้ำผ่านคอลเลกชัน Foreach แบบขนาน ดำเนินการ For loop และดำเนินการผู้รับมอบสิทธิ์หลายคนในการเรียกใช้แบบขนาน การดำเนินการของเธรดปัจจุบันจะหยุดจนกว่าการคำนวณจะเสร็จสิ้น คุณสามารถกำหนดค่าจำนวนเธรดได้โดยส่ง ParallelOptions เป็นอาร์กิวเมนต์สุดท้าย คุณยังสามารถระบุ TaskScheduler และ CancellationToken ได้โดยใช้ตัวเลือก

ผลการวิจัย

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

สรุป:

  • คุณจำเป็นต้องรู้เครื่องมือสำหรับการทำงานกับเธรด ไม่ซิงโครไนซ์ และขนาน เพื่อใช้ทรัพยากรของพีซีสมัยใหม่
  • .NET มีเครื่องมือต่างๆ มากมายสำหรับวัตถุประสงค์เหล่านี้
  • ไม่ใช่ทั้งหมดที่ปรากฏพร้อมกัน ดังนั้นคุณจึงสามารถค้นหาอันเก่าได้บ่อยครั้ง อย่างไรก็ตาม มีวิธีการแปลง API เก่าโดยไม่ต้องใช้ความพยายามมากนัก
  • การทำงานกับเธรดใน .NET แสดงโดยคลาส Thread และ ThreadPool
  • วิธีการ Thread.Abort, Thread.Interrupt และ Win32 API TerminateThread เป็นอันตรายและไม่แนะนำให้ใช้ ควรใช้กลไก CancellationToken แทน
  • โฟลว์เป็นทรัพยากรที่มีคุณค่าและอุปทานมีจำกัด สถานการณ์ที่เธรดกำลังยุ่งอยู่กับการรอเหตุการณ์ควรหลีกเลี่ยง ด้วยเหตุนี้ จึงสะดวกที่จะใช้คลาส TaskCompletionSource
  • เครื่องมือ .NET ที่ทรงพลังและล้ำสมัยที่สุดสำหรับการทำงานกับการทำงานแบบขนานและไม่ซิงโครไนซ์คืองาน
  • ตัวดำเนินการ c# async/await ใช้แนวคิดของการรอแบบไม่บล็อก
  • คุณสามารถควบคุมการกระจายงานข้ามเธรดได้โดยใช้คลาสที่ได้รับจาก TaskScheduler
  • โครงสร้าง ValueTask มีประโยชน์ในการเพิ่มประสิทธิภาพเส้นทางลัดและการรับส่งข้อมูลหน่วยความจำ
  • หน้าต่างงานและเธรดของ Visual Studio ให้ข้อมูลจำนวนมากที่เป็นประโยชน์สำหรับการดีบักโค้ดแบบมัลติเธรดหรือแบบอะซิงโครนัส
  • PLinq เป็นเครื่องมือที่ยอดเยี่ยม แต่อาจมีข้อมูลไม่เพียงพอเกี่ยวกับแหล่งข้อมูลของคุณ แต่สามารถแก้ไขได้โดยใช้กลไกการแบ่งพาร์ติชัน
  • จะยังคง ...

ที่มา: will.com

เพิ่มความคิดเห็น