Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> การเขียนโปรแกรม C

เธรดโดยปริยายและเธรดตามภาษา


การร้อยไหมโดยนัย

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

เธรดโดยนัย ส่วนใหญ่เป็นการใช้ไลบรารี่หรือการสนับสนุนภาษาอื่น ๆ เพื่อซ่อนการจัดการเธรด ไลบรารีเธรดโดยนัยที่พบบ่อยที่สุดคือ 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 ของกันและกัน เนื่องจากเป็นสำเนาที่แตกต่างกัน ทั้งสองเธรดไม่สามารถแชร์การเข้าถึงหน่วยความจำเดียวกันได้