แน่นอน ก่อนที่เราจะเริ่มพูดถึงการเขียนโปรแกรมเธรด POSIX เรามารีเฟรชแนวคิดพื้นฐานและข้อควรพิจารณาในการออกแบบในการเขียนโปรแกรมหน่วยความจำแบบแบ่งใช้กันก่อน ดังนั้น บทความนี้จึงเหมาะสำหรับผู้ที่ยังใหม่ต่อการเขียนโปรแกรมแบบขนานด้วยเธรด POSIX หรือบางครั้งเรียกว่า Pthreads
กระบวนการเทียบกับเธรด
กระบวนการ คือโปรแกรมใดๆ ที่กำลังดำเนินการซึ่งช่วยให้คุณสามารถดำเนินการตามความเหมาะสมที่ระบุไว้ในโปรแกรมได้ ในขณะที่ เธรด เป็นเส้นทางของการดำเนินการภายในกระบวนการซึ่งถือเป็นกระบวนการน้ำหนักเบาและใช้ทรัพยากรของกระบวนการ ดังนั้น กระบวนการสามารถมีได้หลายเธรด ซึ่งทั้งหมดดำเนินการพร้อมกัน อย่างไรก็ตาม ข้อแตกต่างหลักคือเธรดภายในกระบวนการเดียวกันจะทำงานในพื้นที่หน่วยความจำที่ใช้ร่วมกัน ในขณะที่กระบวนการทำงานในพื้นที่หน่วยความจำแยกกัน ดังนั้นในสถาปัตยกรรมมัลติโปรเซสเซอร์หน่วยความจำแบบแบ่งใช้ เธรดสามารถใช้เพื่อดำเนินการขนานได้ เธรดจำนวนมากแบ่งปันข้อมูล เช่น ข้อมูล รหัส ทรัพยากรระบบปฏิบัติการ เช่น ไฟล์ ฯลฯ คล้ายกับกระบวนการ เธรดมีตัวนับโปรแกรม สแต็ก และชุดรีจิสเตอร์ของตัวเอง เนื่องจากแต่ละเธรดมีทรัพยากรที่เป็นอิสระสำหรับการประมวลผลกระบวนการ กระบวนการต่างๆ จึงสามารถดำเนินการแบบขนานได้โดยการเพิ่มจำนวนเธรด
เรามาเริ่มต้นการเขียนโปรแกรม POSIX กันดีกว่า…..
เธรด POSIX หรือ Pthreads
เธรด POSIX หรือที่เรียกกันทั่วไปว่า Pthreads ระบุ Application Programming Interface (API) สำหรับการเขียนโปรแกรมแบบมัลติเธรดที่ใช้ UNIX ต่างจาก C หรือ Java ตรงที่ Pthreads ไม่ใช่ภาษาการเขียนโปรแกรม แต่เป็นไลบรารีที่สามารถเชื่อมโยงกับโปรแกรม C ได้ ยกเว้น Pthreads ยังมีข้อกำหนดอื่นๆ สำหรับการเขียนโปรแกรมแบบมัลติเธรด เช่น Java threads, Windows threads, Solaris threads เป็นต้น อย่างไรก็ตาม ในบทความนี้ เรามาดูที่ Pthreads กัน และเมื่อเราเสร็จแล้ว การเรียนรู้วิธีการก็ไม่ใช่เรื่องยากอีกต่อไป โปรแกรมด้วย thread API อื่นที่ผมกล่าวถึงก่อนหน้านี้
ทำไมต้อง Pthreads?
- น้ำหนักเบา — โดยทั่วไปแล้ว ค่าใช้จ่ายในการสร้างและจัดการกระบวนการจะสูงกว่าต้นทุนของเธรด ดังนั้น จึงสามารถสร้างเธรดได้โดยมีค่าใช้จ่ายระบบปฏิบัติการน้อยกว่า นอกจากนี้ เธรดยังต้องการทรัพยากรระบบน้อยกว่ากระบวนการอีกด้วย
- การสื่อสารที่มีประสิทธิภาพ/การแลกเปลี่ยนข้อมูล — เมื่อเปรียบเทียบกับการใช้ไลบรารี MPI (Message Passing Interface) สำหรับการสื่อสารบนโหนด การใช้ Pthreads อาจกลายเป็นประโยชน์อย่างมากจากการบรรลุประสิทธิภาพที่ดีขึ้น ไลบรารี MPI มักจะใช้การสื่อสารงานบนโหนดผ่านหน่วยความจำที่ใช้ร่วมกัน ซึ่งเกี่ยวข้องกับการดำเนินการคัดลอกหน่วยความจำอย่างน้อยหนึ่งครั้ง (กระบวนการต่อกระบวนการ) ในขณะที่สำหรับ Pthreads ไม่จำเป็นต้องมีการคัดลอกหน่วยความจำระดับกลาง เนื่องจากเธรดใช้พื้นที่ที่อยู่เดียวกันภายในกระบวนการเดียว ดังนั้นจึงไม่มีการถ่ายโอนข้อมูลและมีประสิทธิภาพเท่ากับการส่งพอยน์เตอร์
Pthreads API
Pthreads API สามารถแบ่งออกอย่างไม่เป็นทางการออกเป็นสี่กลุ่มหลัก
- การจัดการเธรด: จัดการการสร้าง การยกเลิก และการรวมเธรด ฯลฯ
- Mutexes: เกี่ยวข้องกับการซิงโครไนซ์ เรียกว่า "mutex" ซึ่งเป็นคำย่อของการยกเว้นร่วมกัน ฟังก์ชัน Mutex มีไว้สำหรับสร้าง ทำลาย ล็อก และปลดล็อก mutexes
- ตัวแปรเงื่อนไข: ระบุการสื่อสารระหว่างเธรดที่แชร์ mutex ระบุฟังก์ชันในการสร้าง ทำลาย รอ และส่งสัญญาณตามค่าตัวแปรที่ระบุ
- การซิงโครไนซ์: จัดการการล็อกและอุปสรรคในการอ่าน/เขียน
ในบทความนี้ เราจะมาเรียนรู้วิธีสร้างและยุติเธรดโดยใช้ Pthreads API
การดำเนินการ
โปรแกรม Pthreads ได้รับการคอมไพล์คล้ายกับโปรแกรม C ทั่วไป และนอกจากนี้ เราจำเป็นต้องลิงก์ไลบรารี Pthreads ด้วย หากคุณใช้คอมไพเลอร์ GNU C คำสั่งคอมไพเลอร์สำหรับโปรแกรม Pthreads จะเป็นดังนี้
gcc -g -Wall -o pth_name pth_name.c -lpthread
การจัดการเธรด
การสร้างและยุติเธรด
เริ่มแรก โปรแกรม main() ของคุณประกอบด้วยเธรดเริ่มต้นเพียงเธรดเดียว และเธรดอื่นๆ ทั้งหมดจะต้องถูกสร้างขึ้นอย่างชัดเจนโดยโปรแกรมเมอร์ ดังนั้น ในการสร้างเธรดใหม่ เราสามารถใช้ฟังก์ชัน pthread_create ใน Pthreads API ไวยากรณ์สำหรับ pthread_create คือ
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
ฟังก์ชัน pthread_create มีสี่อาร์กิวเมนต์:
- เธรด: ตัวระบุที่ไม่ซ้ำใครสำหรับเธรดใหม่ที่ส่งคืนโดยรูทีนย่อย
- attr: ออบเจ็กต์แอตทริบิวต์ทึบแสงที่อาจใช้ในการตั้งค่าแอตทริบิวต์ของเธรด คุณสามารถระบุวัตถุแอตทริบิวต์ของเธรดหรือ NULL สำหรับค่าเริ่มต้นได้
- start_routine: รูทีน C ที่เธรดจะดำเนินการเมื่อถูกสร้างขึ้น
- หาเรื่อง: อาร์กิวเมนต์เดียวที่อาจส่งผ่านไปยัง start_routine มันจะต้องถูกส่งผ่านโดยการอ้างอิงเป็นตัวชี้ประเภทโมฆะ อาจใช้ NULL ได้หากไม่มีการส่งผ่านอาร์กิวเมนต์
อย่างไรก็ตาม มีหลายวิธีในการยุติเธรด โดยปกติ เธรดจะสิ้นสุดเมื่องานเสร็จสิ้น หรือหากทำการเรียกไปยังรูทีนย่อย pthreads_exit นอกจากนี้ เธรดจะถูกยกเลิกโดยเธรดอื่นผ่านรูทีน pthread_cancel ยกเว้นกรณีเหล่านี้ กระบวนการทั้งหมดสามารถยุติได้โดยทำการเรียกไปที่ exec() หรือ exit() อย่างไรก็ตาม ปัญหาจะเกิดขึ้นหาก main() เสร็จสิ้นก่อนที่เธรดจะถูกสร้างขึ้น และถ้าคุณไม่เรียกใช้ pthread_exit() อย่างชัดเจน ในสถานการณ์สมมตินี้ มันจะยุติเธรดทั้งหมดที่สร้างขึ้นเมื่อ main() เสร็จสิ้นและไม่มีอยู่อีกต่อไป แต่ถ้าคุณเรียก pthread_exit() อย่างชัดเจน main() จะถูกบล็อกและคงอยู่หรือสนับสนุนเธรดที่สร้างขึ้นจนกว่าจะเสร็จสิ้น
ตัวอย่าง: การสร้างและการสิ้นสุด Pthread
ตอนนี้ เรามาเจาะลึกโค้ดตัวอย่างง่ายๆ ที่สร้าง 5 เธรดด้วยรูทีนย่อย pthread_create() และสิ้นสุดด้วยการเรียกรูทีน pthread_exit()
#include <pthread.h> #include <stdio.h> #define NUM_THREADS 5 void *PrintHello (void *threadid) { long tid; tid = (long)threadid; printf("Hello World! It's me, thread #%ld!\n", tid); pthread_exit(NULL); } int main (int argc, char *argv[]) { pthread_t threads[NUM_THREADS]; int rc; long t; for(t=0; t<NUM_THREADS; t++){ printf("In main: creating thread %ld\n", t); rc = pthread_create(&threads[t], NULL, PrintHello, (void *)t); if (rc){ printf("ERROR; return code from pthread_create() is %d\n", rc); exit(-1); } } /* Last thing that main() should do */ pthread_exit(NULL); }
ตอนนี้เรามาดูซอร์สโค้ดในตัวอย่างกันดีกว่า เช่นเดียวกับโปรแกรม C อื่นๆ โปรแกรมนี้รวมไฟล์ส่วนหัวที่คุ้นเคย เช่น stdio.h อย่างไรก็ตาม ในที่นี้ เรายังจำเป็นต้องรวม pthread.h ซึ่งเป็นไฟล์ส่วนหัวของ Pthread ซึ่งประกาศฟังก์ชัน ค่าคงที่ และประเภทต่างๆ ของ Pthreads ถัดไป คุณต้องกำหนดตัวแปรโกลบอลสำหรับจำนวนเธรด และในที่นี้จะกำหนดให้เป็น 5 จากนั้นเราได้ใช้ฟังก์ชันทั้งสอง pthread_create() และ pthread_exit() ที่เรากล่าวถึงก่อนหน้านี้ ขั้นแรก เราได้จัดสรรพื้นที่เก็บข้อมูลสำหรับหนึ่งอ็อบเจ็กต์ pthread_t สำหรับแต่ละเธรด อาร์กิวเมนต์แรกของ pthread_create() เป็นตัวชี้ไปยังอ็อบเจ็กต์ pthread_t ที่เหมาะสม ออบเจ็กต์ pthread_t เหล่านี้เป็นตัวอย่างของออบเจ็กต์ทึบ ดังนั้นข้อมูลจริงที่เก็บไว้จึงเป็นข้อมูลเฉพาะระบบ และโค้ดผู้ใช้ไม่สามารถเข้าถึงสมาชิกของข้อมูลได้โดยตรง อย่างไรก็ตาม มาตรฐาน Pthreads รับประกันว่าอ็อบเจ็กต์ pthread_t เก็บข้อมูลได้เพียงพอที่จะระบุเธรดที่เกี่ยวข้องได้โดยไม่ซ้ำกัน เราจะไม่ใช้อาร์กิวเมนต์ที่สอง ดังนั้นเราจึงส่งอาร์กิวเมนต์ NULL ในการเรียกใช้ฟังก์ชันของเรา อาร์กิวเมนต์ที่สามคือฟังก์ชันที่เธรดจะทำงาน และอาร์กิวเมนต์สุดท้ายคือตัวชี้ไปยังอาร์กิวเมนต์ที่ควรส่งผ่านไปยังรูทีนการเริ่มฟังก์ชัน ดังนั้นผลลัพธ์สุดท้ายจะมีลักษณะดังนี้ด้านล่าง
In main: creating thread 0 In main: creating thread 1 Hello World! It's me, thread #0! In main: creating thread 2 Hello World! It's me, thread #1! Hello World! It's me, thread #2! In main: creating thread 3 In main: creating thread 4 Hello World! It's me, thread #3! Hello World! It's me, thread #4!
บทสรุป
ในบทความนี้ ฉันอธิบายเพียงส่วนสำคัญเพียงส่วนเดียวใน Pthreads API ซึ่งก็คือการจัดการเธรด ที่นั่น ฉันแสดงวิธีสร้างและยุติเธรดโดยใช้ตัวอย่างสวัสดีชาวโลกง่ายๆ อย่างไรก็ตาม ยังมีอะไรอีกมากมายให้เล่นด้วย Pthreads API และสำหรับการเริ่มต้น ฉันขอแนะนำให้ลองใช้ตัวอย่างนี้และดูว่าการทำงานพร้อมกันเกิดขึ้นอย่างไร
อ้างอิง
[1]”ความรู้เบื้องต้นเกี่ยวกับการประมวลผลแบบขนาน”, Computing.llnl.gov, 2020 [ออนไลน์] มีจำหน่าย: https://computing.llnl.gov/tutorials/parallel_comp/. [เข้าถึง: 02- ก.ย.- 2020].