แน่นอน ก่อนที่เราจะเริ่มพูดถึงการเขียนโปรแกรมเธรด 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 สามารถแบ่งออกอย่างไม่เป็นทางการออกเป็นสี่กลุ่มหลัก

  1. การจัดการเธรด: จัดการการสร้าง การยกเลิก และการรวมเธรด ฯลฯ
  2. Mutexes: เกี่ยวข้องกับการซิงโครไนซ์ เรียกว่า "mutex" ซึ่งเป็นคำย่อของการยกเว้นร่วมกัน ฟังก์ชัน Mutex มีไว้สำหรับสร้าง ทำลาย ล็อก และปลดล็อก mutexes
  3. ตัวแปรเงื่อนไข: ระบุการสื่อสารระหว่างเธรดที่แชร์ mutex ระบุฟังก์ชันในการสร้าง ทำลาย รอ และส่งสัญญาณตามค่าตัวแปรที่ระบุ
  4. การซิงโครไนซ์: จัดการการล็อกและอุปสรรคในการอ่าน/เขียน

ในบทความนี้ เราจะมาเรียนรู้วิธีสร้างและยุติเธรดโดยใช้ 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].