การร้อยไหมโดยนัย
วิธีหนึ่งในการแก้ไขปัญหาและสนับสนุนการออกแบบแอปพลิเคชันแบบมัลติเธรดได้ดีขึ้นคือการถ่ายโอนการสร้างและการจัดการเธรดจากนักพัฒนาแอปพลิเคชันไปยังคอมไพเลอร์และไลบรารีรันไทม์ สิ่งนี้เรียกว่าการทำเกลียวโดยนัยเป็นเทรนด์ที่ได้รับความนิยมในปัจจุบัน
เธรดโดยนัย ส่วนใหญ่เป็นการใช้ไลบรารี่หรือการสนับสนุนภาษาอื่น ๆ เพื่อซ่อนการจัดการเธรด ไลบรารีเธรดโดยนัยที่พบบ่อยที่สุดคือ OpenMP ในบริบทของ C.
OpenMP เป็นชุดของคำสั่งคอมไพเลอร์เช่นเดียวกับ API สำหรับโปรแกรมที่เขียนในภาษา C, C++ หรือ FORTRAN ที่ให้การสนับสนุนการเขียนโปรแกรมแบบขนานในสภาพแวดล้อมหน่วยความจำที่ใช้ร่วมกัน OpenMP ระบุขอบเขตคู่ขนานเป็นบล็อกของโค้ดที่อาจทำงานแบบขนาน นักพัฒนาแอปพลิเคชันแทรกคำสั่งคอมไพเลอร์ลงในโค้ดของตนในพื้นที่คู่ขนาน และคำสั่งเหล่านี้จะสั่งให้ไลบรารีรันไทม์ของ OpenMP ดำเนินการในภูมิภาคแบบขนาน โปรแกรม C ต่อไปนี้แสดงคำสั่งคอมไพเลอร์เหนือขอบเขตคู่ขนานที่มีคำสั่ง printf():
ตัวอย่าง
#include <omp.h> #include <stdio.h> int main(int argc, char *argv[]){ /* sequential code */ #pragma omp parallel{ printf("I am a parallel region."); } /* sequential code */ return 0; }
ผลลัพธ์
I am a parallel region.
เมื่อ OpenMP พบกับคำสั่ง
#pragma omp parallel
มันสร้างเธรดจำนวนมากซึ่งเป็นแกนประมวลผลในระบบ ดังนั้นสำหรับระบบดูอัลคอร์จะมีการสร้างสองเธรดสำหรับระบบควอดคอร์สี่เธรดถูกสร้างขึ้น และอื่นๆ จากนั้นเธรดทั้งหมดจะดำเนินการพื้นที่คู่ขนานพร้อมกัน เมื่อแต่ละเธรดออกจากขอบเขตคู่ขนาน เธรดจะสิ้นสุดลง OpenMP ให้คำสั่งเพิ่มเติมหลายประการสำหรับการรันขอบเขตโค้ดแบบขนาน รวมถึงการวนซ้ำแบบขนาน
นอกเหนือจากการจัดเตรียมคำสั่งสำหรับการทำให้ขนานกัน OpenMP ยังช่วยให้นักพัฒนาสามารถเลือกระหว่างหลายระดับของการขนาน เช่น สามารถกำหนดจำนวนเธรดได้ด้วยตนเอง นอกจากนี้ยังช่วยให้นักพัฒนาสามารถระบุได้ว่าข้อมูลจะถูกแบ่งปันระหว่างเธรดหรือเป็นข้อมูลส่วนตัวของเธรด OpenMP มีอยู่ในคอมไพเลอร์โอเพนซอร์สและคอมไพเลอร์เชิงพาณิชย์หลายตัวสำหรับระบบ Linux, Windows และ Mac OS X
แกรนด์เซ็นทรัล Dispatch (GCD)
Grand Central Dispatch (GCD) ซึ่งเป็นเทคโนโลยีสำหรับระบบปฏิบัติการ Mac OS X และ iOS ของ Apple เป็นการผสมผสานระหว่างส่วนขยายกับภาษา C, API และไลบรารีรันไทม์ที่ช่วยให้นักพัฒนาแอปพลิเคชันระบุส่วนของโค้ดที่จะรันได้ ขนาน. เช่นเดียวกับ OpenMP GCD ยังจัดการรายละเอียดส่วนใหญ่ของเธรดด้วย ระบุส่วนขยายของภาษา C และ C++ ที่เรียกว่าบล็อก บล็อกเป็นเพียงหน่วยของงานที่มีในตัวเอง มันถูกระบุโดยคาเร็ต ˆ ที่ด้านหน้าของวงเล็บปีกกา { } ตัวอย่างง่ายๆ ของบล็อกแสดงอยู่ด้านล่าง −
{ ˆprintf("This is a block"); }
มันกำหนดเวลาบล็อกสำหรับการดำเนินการรันไทม์โดยวางไว้บนคิวการจัดส่ง เมื่อ GCD ลบบล็อกออกจากคิว GCD จะกำหนดบล็อกให้กับเธรดที่มีอยู่จากพูลเธรดที่จัดการ ระบุคิวการจัดส่งสองประเภท:แบบอนุกรมและแบบพร้อมกัน บล็อกที่วางอยู่บนคิวอนุกรมจะถูกลบออกในลำดับ FIFO เมื่อบล็อกถูกลบออกจากคิวแล้ว จะต้องดำเนินการให้เสร็จสิ้นก่อนที่จะลบบล็อกอื่น แต่ละกระบวนการมีคิวซีเรียลของตัวเอง (เรียกว่าคิวหลัก) นักพัฒนาสามารถสร้างคิวซีเรียลเพิ่มเติมที่เป็นแบบโลคัลสำหรับกระบวนการเฉพาะได้ คิวอนุกรมมีประโยชน์สำหรับการรับรองการดำเนินการตามลำดับของงานต่างๆ บล็อกที่วางอยู่บนคิวพร้อมกันจะถูกลบออกในลำดับ FIFO แต่หลายบล็อกอาจถูกลบในคราวเดียว ดังนั้นจึงอนุญาตให้หลายบล็อกดำเนินการพร้อมกัน มีคิวการจัดส่งพร้อมกันทั่วทั้งระบบสามคิว และมีการแยกแยะตามลำดับความสำคัญ:ต่ำ ค่าเริ่มต้น และสูง ลำดับความสำคัญแสดงถึงการประเมินความสำคัญของกลุ่ม พูดง่ายๆ ก็คือ บล็อกที่มีลำดับความสำคัญสูงกว่าควรอยู่ในคิวการจัดส่งที่มีลำดับความสำคัญสูง ส่วนรหัสต่อไปนี้แสดงให้เห็นถึงการรับคิวพร้อมกันที่มีลำดับความสำคัญดีฟอลต์และส่งบล็อกไปยังคิวโดยใช้ฟังก์ชันการจัดส่ง async():
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch async(queue, ˆ{ printf("This is a block."); });
ภายในเธรดพูลของ GCD ประกอบด้วยเธรด POSIX GCD จัดการพูลอย่างแข็งขัน ทำให้จำนวนเธรดเพิ่มขึ้นและลดขนาดตามความต้องการของแอปพลิเคชันและความสามารถของระบบ
กระทู้เป็นวัตถุ
ในภาษาทางเลือก ภาษาเชิงวัตถุโบราณให้การสนับสนุนมัลติเธรดอย่างชัดแจ้งโดยมีเธรดเป็นอ็อบเจ็กต์ ในรูปแบบภาษาเหล่านี้ พื้นที่คลาสที่เขียนเพื่อขยายคลาสเธรดหรือใช้อินเทอร์เฟซที่เกี่ยวข้อง ลักษณะนี้คล้ายกับแนวทาง Pthread เนื่องจากโค้ดนี้เขียนด้วยการจัดการเธรดที่ชัดเจน อย่างไรก็ตาม การห่อหุ้มข้อมูลภายในคลาสและตัวเลือกการซิงโครไนซ์พิเศษจะปรับเปลี่ยนงาน
จาวาเธรด
Java จัดเตรียมหมวดหมู่เธรดและอินเทอร์เฟซ Runnable ที่สามารถใช้ได้ แต่ละคนต้องใช้เทคนิค public void run() ที่กำหนดวัตถุประสงค์ในการเข้าของเธรด เมื่ออินสแตนซ์ของวัตถุได้รับการจัดสรรแล้ว เธรดจะเริ่มต้นโดยเรียกใช้เทคนิค start() บนนั้น เช่นเดียวกับ Pthreads การเริ่มต้นเธรดเป็นแบบอะซิงโครนัส ซึ่งการจัดเรียงชั่วคราวของการดำเนินการนั้นไม่ได้กำหนดไว้
กระทู้หลาม
Python ยังมีกลไกสองอย่างเพิ่มเติมสำหรับการทำมัลติเธรด วิธีหนึ่งเปรียบได้กับสไตล์ Pthread ไม่ว่าชื่อฟังก์ชันจะถูกส่งไปยังเมธอดของไลบรารี thread.start_new_thread() ที่ใด วิธีการนี้เป็นแนวทางอย่างมากและไม่มีความยืดหยุ่นในการเข้าร่วมหรือยุติเธรดเมื่อเริ่มต้น เทคนิคที่ยืดหยุ่นเพิ่มเติมคือการใช้โมดูลเธรดเพื่อร่างคลาสที่ขยายเธรด เกลียว. เกือบจะเหมือนกับวิธีการของ Java หมวดหมู่ควรมีเมธอด run() ที่ให้จุดประสงค์ในการป้อนเธรด เมื่อวัตถุสร้างอินสแตนซ์จากหมวดหมู่นี้แล้ว ก็สามารถเริ่มและเข้าร่วมได้ในภายหลัง
การทำงานพร้อมกันในฐานะการออกแบบภาษา
ภาษาโปรแกรมที่ใหม่กว่าได้หลีกเลี่ยงสภาวะการแข่งขันโดยสร้างสมมติฐานของการดำเนินการพร้อมกันโดยตรงในรูปแบบภาษาเอง ตัวอย่างเช่น Go ผสมผสานเทคนิคการทำเธรดโดยนัยเล็กน้อย (goroutines) เข้ากับแชนเนล ซึ่งเป็นรูปแบบการสื่อสารที่ส่งข้อความผ่านข้อความที่กำหนดไว้อย่างดี Rust ใช้วิธีการทำเกลียวที่แน่นอนเช่นเดียวกับ Pthreads อย่างไรก็ตาม Rust มีการป้องกันหน่วยความจำที่แข็งแกร่งอย่างมากซึ่งไม่จำเป็นต้องทำงานเพิ่มเติมโดยวิศวกรซอฟต์แวร์
กรูทีน
ภาษา Go มีกลไกเล็กน้อยสำหรับเธรดโดยปริยาย:วางคีย์เวิร์ดก่อนการโทร เธรดใหม่ถูกส่งผ่านการเชื่อมโยงไปยังช่องทางการส่งข้อความ จากนั้นเธรดส่วนใหญ่เรียก Success :=<-messages ที่ทำการสแกนสัญญาณรบกวนในช่อง เมื่อผู้ใช้ป้อนการเดาที่ถูกต้องของเจ็ดชุดข้อความผู้ตรวจสอบแป้นพิมพ์จะเขียนไปยังช่องเพื่อให้ชุดข้อความส่วนใหญ่ก้าวหน้า
Channels และ goroutines เป็นองค์ประกอบหลักของภาษา Go ซึ่งได้รับการออกแบบภายใต้ความเชื่อที่ว่าเกือบทุกโปรแกรมจะเป็นแบบมัลติเธรด ทางเลือกรูปแบบนี้ทำให้โมเดลเหตุการณ์คล่องตัวขึ้น โดยอนุญาตให้ภาษาอัปเดตความรับผิดชอบในการจัดการเธรดและการเขียนโปรแกรม
การเกิดสนิมพร้อมกัน
อีกภาษาหนึ่งคือ Rust ที่สร้างขึ้นในช่วงไม่กี่ปีที่ผ่านมา โดยมีการทำงานพร้อมกันเป็นคุณลักษณะการออกแบบส่วนกลาง ตัวอย่างต่อไปนี้แสดงให้เห็นถึงการใช้ thread::spawn() เพื่อสร้างเธรดใหม่ ซึ่งสามารถเข้าร่วมได้ในภายหลังโดยเรียกใช้ join() บนเธรดนั้น อาร์กิวเมนต์ของ thread::spawn() เริ่มต้นที่ || เรียกว่าการปิด ซึ่งถือได้ว่าเป็นฟังก์ชันที่ไม่ระบุชื่อ นั่นคือเธรดย่อยที่นี่จะพิมพ์ค่าของ a.
ตัวอย่าง
use std::thread; fn main() { /* Initialize a mutable variable a to 7 */ let mut a = 7; /* Spawn a new thread */ let child_thread = thread::spawn(move || { /* Make the thread sleep for one second, then print a */ a -= 1; println!("a = {}", a) }); /* Change a in the main thread and print it */ a += 1; println!("a = {}", a); /* Join the thread and print a again */ child_thread.join(); }
อย่างไรก็ตาม มีจุดที่ละเอียดอ่อนในโค้ดนี้ซึ่งเป็นศูนย์กลางของการออกแบบของ Rust ภายในเธรดใหม่ (เรียกใช้โค้ดในส่วนปิด) ตัวแปรจะแตกต่างจาก a ในส่วนอื่น ๆ ของโค้ดนี้ มันบังคับใช้รูปแบบหน่วยความจำที่เข้มงวดมาก (เรียกว่า "ความเป็นเจ้าของ") ซึ่งป้องกันไม่ให้หลายเธรดเข้าถึงหน่วยความจำเดียวกัน ในตัวอย่างนี้ คีย์เวิร์ด move ระบุว่าเธรดที่วางไข่จะได้รับสำเนาของ a แยกต่างหากสำหรับการใช้งานของตัวเอง โดยไม่คำนึงถึงการจัดกำหนดการของสองเธรด เธรดหลักและเธรดย่อยไม่สามารถรบกวนการแก้ไข a ของกันและกัน เนื่องจากเป็นสำเนาที่แตกต่างกัน ทั้งสองเธรดไม่สามารถแชร์การเข้าถึงหน่วยความจำเดียวกันได้