การสร้างและรอให้เธรดทำงาน เธรด C - มันคืออะไร? ระบบตัวประมวลผลเดี่ยวและหลายตัว

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

หากคุณต้องการทำบางสิ่งที่ต้องใช้ CPU มาก คุณอาจต้องการดูที่โมดูล การประมวลผลหลายตัว, แทน เกลียว- เหตุผลก็คือ Python มี Global Interpreter Lock (GIL) ที่รันเธรดทั้งหมดภายในเธรดหลัก ด้วยเหตุนี้ เมื่อคุณต้องการรันการดำเนินการที่ใช้เธรดจำนวนมาก คุณจะสังเกตเห็นว่าทุกอย่างค่อนข้างช้า ดังนั้นเราจะเน้นไปที่เธรดที่ดีที่สุด: การดำเนินการ I/O

บทนำเล็กน้อย

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

นำเข้าเธรด def doubler(number): """ ฟังก์ชั่นที่สามารถใช้งานได้โดยเธรด """ print(threading.currentThread().getName() + "\n") print(number * 2) print() if __name__ == "__main__": สำหรับฉันอยู่ในช่วง (5): my_thread = threading.Thread(target=doubler, args=(i,)) my_thread.start()

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

การใช้มัลติเธรดทำให้คุณสามารถแก้ไขปัญหาต่างๆ ที่เกิดขึ้นเป็นประจำได้ เช่น การอัปโหลดวิดีโอหรือเนื้อหาอื่นๆ ไปที่ สื่อสังคมเช่น Youtube หรือ Facebook ในการพัฒนาช่อง Youtube ของคุณ คุณสามารถใช้ https://publbox.com/ru/youtube ซึ่งจะเข้ามาควบคุมดูแลช่องของคุณ Youtube เป็นแหล่งรายได้ที่ดีและยิ่งมีช่องมากเท่าไรก็ยิ่งดีเท่านั้น คุณไม่สามารถทำได้หากไม่มี Publbox

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

เธรด-1 0 เธรด-2 2 เธรด-3 4 เธรด-4 6 เธรด-5 8

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

นำเข้าการบันทึก นำเข้าเธรด def get_logger(): logger = logging.getLogger("threading_example") logger.setLevel(logging.DEBUG) fh = logging.FileHandler("threading.log") fmt = "%(asctime)s - %( threadName)s - %(levelname)s - %(ข้อความ)s" formatter = logging.Formatter(fmt) fh.setFormatter(formatter) logger.addHandler(fh) return logger def doubler(number, logger): """ A ฟังก์ชันที่สามารถใช้งานได้โดยเธรด """ logger.debug("doubler function executing") result = number * 2 logger.debug("doubler function ลงท้ายด้วย: ()".format(result)) if __name__ == " __main__": logger = get_logger() thread_names = ["Mike", "George", "Wanda", "Dingbat", "Nina"] สำหรับฉันในช่วง (5): my_thread = threading.Thread(target=doubler, name =thread_names[i], args=(i,logger)) my_thread.start()

การเปลี่ยนแปลงที่ใหญ่ที่สุดในโค้ดนี้คือการเพิ่ม ฟังก์ชัน get_logger- โค้ดส่วนนี้จะสร้างตัวบันทึกที่ได้รับการกำหนดค่าให้ทำการดีบัก การดำเนินการนี้จะบันทึกบันทึกลงในโฟลเดอร์การทำงานปัจจุบัน (หรืออีกนัยหนึ่งคือที่สคริปต์ทำงานอยู่) จากนั้นเราจะกำหนดรูปแบบของแต่ละบรรทัดที่จะบันทึก รูปแบบประกอบด้วยการประทับเวลา ชื่อสตรีม ระดับการบันทึก และข้อความที่บันทึกไว้ ในฟังก์ชัน doubler เราเปลี่ยนคำสั่งเอาต์พุตเป็นคำสั่ง การบันทึก.

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

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

นำเข้าการบันทึก นำเข้าคลาสเธรดการนำเข้า MyThread(threading.Thread): def __init__(self, number, logger): threading.Thread.__init__(self) self.number = number self.logger = logger def run(self): """ Run เธรด """ logger.debug ("การโทรสองเท่า") doubler (self.number, self.logger) def get_logger (): logger = logging.getLogger ("threading_example") logger.setLevel (logging.DEBUG) fh = การบันทึก .FileHandler("threading_class.log") fmt = "%(asctime)s - %(threadName)s - %(levelname)s - %(message)s" ฟอร์แมตเตอร์ = logging.ฟอร์แมตเตอร์(fmt) fh.setFormatter(ฟอร์แมตเตอร์) logger.addHandler(fh) return logger def doubler(number, logger): """ ฟังก์ชั่นที่สามารถใช้งานได้โดยเธรด """ logger.debug("doubler function executing") result = number * 2 logger.debug( "ฟังก์ชัน doubler ลงท้ายด้วย: ()".format(result)) if __name__ == "__main__": logger = get_logger() thread_names = ["Mike", "George", "Wanda", "Dingbat", "Nina" ] สำหรับฉันอยู่ในช่วง (5): thread = MyThread(i, logger) thread.setName(thread_names[i]) thread.start()

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

ล็อคและการซิงโครไนซ์

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

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

นำเข้าเธรดทั้งหมด = 0 def update_total(amount): """ อัปเดตผลรวมตามจำนวนที่กำหนด """ ยอดรวมทั่วโลก += จำนวน print (รวม) ถ้า __name__ == "__main__": for i in range(10): my_thread = threading.Thread(target=update_total, args=(5,)) my_thread.start()

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

นำเข้าเธรดทั้งหมด = 0 lock = threading.Lock() def update_total(amount): """ อัปเดตผลรวมตามจำนวนที่กำหนด """ ผลรวมทั่วโลก lock.acquire() ลอง: รวม += จำนวนในที่สุด: lock.release( ) พิมพ์ (รวม) ถ้า __name__ == "__main__": สำหรับฉันอยู่ในช่วง (10): my_thread = threading.Thread(target=update_total, args=(5,)) my_thread.start()

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

นำเข้าเธรดทั้งหมด = 0 lock = threading.Lock() def update_total(amount): """ อัปเดตผลรวมตามจำนวนที่กำหนด """ ผลรวมทั่วโลกพร้อมการล็อค: รวม += จำนวน พิมพ์ (รวม) ถ้า __name__ == "__main__ ": สำหรับฉันอยู่ในช่วง (10): my_thread = threading.Thread(target=update_total, args=(5,)) my_thread.start()

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

นำเข้าเธรดทั้งหมด = 0 lock = threading.Lock() def do_something(): lock.acquire() ลอง: print("Lock Acquire in the do_something function") ในที่สุด: lock.release() print("Lock release in the do_something function") ส่งคืน "ทำบางสิ่งเสร็จแล้ว" def do_something_else(): lock.acquire() ลอง: print("ล็อคที่ได้รับในฟังก์ชัน do_something_else") ในที่สุด: lock.release() print("ล็อคที่ปล่อยออกมาในฟังก์ชัน do_something_else") ส่งคืน "ทำอย่างอื่นเสร็จแล้ว" ถ้า __name__ == "__main__": result_one = do_something() result_two = do_something_else()

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

นำเข้าเธรดทั้งหมด = 0 lock = threading.RLock() def do_something(): พร้อมล็อค: print("ล็อคที่ได้รับในฟังก์ชัน do_something") print("ล็อคที่ปล่อยออกมาในฟังก์ชัน do_something") ส่งคืน "เสร็จสิ้นการทำบางสิ่งบางอย่าง" def do_something_else (): ด้วยการล็อค: print("ล็อคที่ได้รับในฟังก์ชัน do_something_else") print("ล็อคที่ปล่อยออกมาในฟังก์ชัน do_something_else") ส่งคืน "ทำอย่างอื่นเสร็จแล้ว" def main(): ด้วยการล็อค: result_one = do_something() result_two = do_something_else () พิมพ์ (result_one) พิมพ์ (result_two) ถ้า __name__ == "__main__": main()

เมื่อคุณรันโค้ดนี้ คุณจะเห็นว่าโค้ดค้าง เหตุผลก็คือเราเพียงแค่บอกโมดูล เกลียวแขวนล็อค ดังนั้นเมื่อเราเรียกใช้ฟังก์ชันแรกจะเห็นว่าตัวล็อคถูกแขวนและปิดกั้นไว้แล้ว สิ่งนี้จะคงอยู่จนกว่าการล็อคจะถูกลบออก ซึ่งจะไม่เกิดขึ้น เนื่องจากไม่ได้ระบุไว้ในโค้ด การตัดสินใจที่ดีในกรณีนี้ ให้ใช้การล็อคการเข้าใหม่ โมดูล เกลียวให้อันหนึ่งเป็นฟังก์ชัน อาร์ล็อค- เพียงเปลี่ยนสายล็อค= เกลียว.ล็อค() เมื่อล็อค = เกลียว RLock() และลองรันโค้ดอีกครั้ง ตอนนี้เขาควรจะได้รับมัน หากคุณต้องการลองใช้โค้ดด้านบนแต่เพิ่มเธรดเข้าไป เราสามารถแทนที่ได้ เรียกบน หลักด้วยวิธีต่อไปนี้:

ถ้า __name__ == "__main__": สำหรับฉันอยู่ในช่วง (10): my_thread = threading.Thread(target=main) my_thread.start()

สิ่งนี้จะเรียกใช้ฟังก์ชันหลักในแต่ละเธรด ซึ่งจะเรียกใช้ฟังก์ชันอีกสองฟังก์ชันตามลำดับ ในที่สุดคุณจะได้รับการจ่ายเงินที่ค่อนข้างมาก

ตัวจับเวลา

โมดูล เกลียวรวมคลาสหนึ่งที่สะดวกมากที่เรียกว่า ตัวจับเวลาซึ่งคุณสามารถใช้เพื่อกระตุ้นการดำเนินการหลังจากช่วงระยะเวลาหนึ่งได้ คลาสนี้เริ่มเธรดของตัวเองและเริ่มทำงานจากเธรดเดียวกัน เริ่มต้นวิธีการ() เช่นเดียวกับสตรีมปกติ คุณยังสามารถหยุดตัวจับเวลาได้โดยใช้วิธีการยกเลิก โปรดทราบว่าคุณสามารถยกเลิกตัวจับเวลาก่อนที่จะเริ่มได้ ครั้งหนึ่งฉันมีกรณีที่ฉันต้องสร้างการสื่อสารกับกระบวนการย่อยที่ฉันเริ่มต้นไว้ แต่ฉันต้องการการนับถอยหลัง แม้ว่าจะมีตัวเลขอยู่ก็ตาม ในรูปแบบต่างๆวิธีแก้ปัญหาเฉพาะนี้ วิธีแก้ปัญหาที่ฉันชอบคือใช้มาตลอด คลาสจับเวลาโมดูลเกลียว สำหรับตัวอย่างนี้ เราจะดูที่การใช้คำสั่ง ping บนลินุกซ์ คำสั่งปิงจะทำงานจนกว่าคุณจะฆ่ามัน ดังนั้นคลาส Timer จึงมีประโยชน์อย่างยิ่งในโลก Linux นี่คือตัวอย่าง:

นำเข้ากระบวนการย่อยจากเธรดการนำเข้า Timer kill = กระบวนการ lambda: process.kill() cmd = ["ping", "www.google.com"] ping = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) my_timer = Timer(5, kill, ) ลอง: my_timer.start() stdout, stderr = ping.communicate() ในที่สุด: my_timer.cancel() print (str(stdout))

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

ส่วนประกอบด้ายอื่นๆ

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

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

การสื่อสารด้วยเธรด

มีหลายกรณีที่คุณต้องสร้างเธรดที่เกี่ยวข้องกัน ดังที่ได้กล่าวไปแล้วคุณสามารถใช้ เหตุการณ์เพื่อจุดประสงค์นี้. แต่วิธีที่สะดวกกว่าคือการใช้งาน คิว- ในตัวอย่างของเรา เราใช้ทั้งสองวิธี! มาดูกันว่ามันจะมีลักษณะอย่างไร:

นำเข้าเธรดจากคิว นำเข้าคิว def creator(data, q): """ สร้างข้อมูลที่จะใช้และรอให้ผู้บริโภคเสร็จสิ้นการประมวลผล """ print("การสร้างข้อมูลและวางลงในคิว") สำหรับรายการในข้อมูล : evt = threading.Event() q.put((item, evt)) print("Waiting for data to be doubled") evt.wait() def my_consumer(q): """ ใช้ข้อมูลบางส่วนและดำเนินการกับข้อมูลนั้น ในกรณีนี้ สิ่งที่ทำได้คือเพิ่มอินพุต """ เป็นสองเท่าในขณะที่ True: data, evt = q.get() print("data found to beprocess: ()".format(data))processed = data * 2 print (ประมวลผลแล้ว) evt.set() q.task_done() ถ้า __name__ == "__main__": q = Queue() data = thread_one = threading.Thread(target=creator, args=(data, q)) thread_two = threading เธรด (เป้าหมาย = my_consumer, args = (q,)) thread_one.start () thread_two.start () q.join ()

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

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

มาสรุปกัน

เราได้ครอบคลุมเนื้อหาค่อนข้างมาก กล่าวคือ:

  1. พื้นฐานการทำงานกับโมดูลเธรด
  2. ล็อคทำงานอย่างไร?
  3. Event คืออะไร และนำไปใช้ได้อย่างไร
  4. วิธีการใช้งานตัวจับเวลา
  5. การสื่อสารภายในเธรดโดยใช้คิว/เหตุการณ์

ตอนนี้คุณรู้วิธีใช้เธรดแล้วและมีประโยชน์อย่างไร ฉันหวังว่าคุณจะพบว่ามีประโยชน์บางอย่างในโค้ดของคุณเอง!


คำอธิบายโดยละเอียด

Protothreads เป็นเธรดประเภทหนึ่งที่มีน้ำหนักเบาและไม่มีการวางซ้อนซึ่งออกแบบมาสำหรับระบบที่มีหน่วยความจำต่ำ เช่น ระบบไมโครคอนโทรลเลอร์แบบฝังหรือโหนดเซ็นเซอร์แบบเครือข่าย

Protothreads จัดเตรียมการเรียกใช้โค้ดเชิงเส้นสำหรับระบบที่ขับเคลื่อนด้วยเหตุการณ์ที่ใช้งานใน C Protothreads สามารถใช้โดยมีหรือไม่มี RTOS ได้

Protothreads จัดเตรียมบริบทที่ล็อคการรอคอยไว้ด้านบนของระบบที่ขับเคลื่อนด้วยเหตุการณ์ โดยไม่มีโอเวอร์เฮดของสแต็กเดียว วัตถุประสงค์ของเธรดคือการใช้การเรียกใช้โค้ดตามลำดับโดยไม่ต้องใช้เครื่องสถานะที่ซับซ้อนหรือมัลติเธรด Protothreads จัดให้มีการบล็อกแบบมีเงื่อนไขสำหรับการเรียกใช้โค้ดภายในเนื้อความของฟังก์ชัน C

ข้อดีของโปรโตเธรดคือบรรลุกลไกเหตุการณ์ล้วนๆ ซึ่งสามารถบล็อกการเรียกใช้โค้ดฟังก์ชันเชิงเส้นได้ตามเงื่อนไขที่ต้องการ ในระบบที่อิงตามเหตุการณ์ล้วนๆ การบล็อกจะต้องดำเนินการด้วยตนเองโดยแบ่งฟังก์ชันออกเป็น 2 ส่วน - ส่วนหนึ่งสำหรับโค้ดก่อนการเรียกการบล็อก และส่วนที่สองสำหรับโค้ดหลังจากการเรียกการบล็อก ซึ่งทำให้ยากต่อการใช้โครงสร้างการควบคุม เช่น คำสั่ง if() แบบมีเงื่อนไข และ while() ลูป

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

หมายเหตุ:เนื่องจากโปรโตเธรดไม่ได้เก็บบริบทไว้ในสแต็กระหว่างการบล็อกการโทร ตัวแปรโลคัลจะไม่ถูกรักษาไว้เมื่อโปรโตเธรดถูกบล็อก- ซึ่งหมายความว่าควรใช้ตัวแปรท้องถิ่นด้วยความระมัดระวัง - หากคุณมีข้อสงสัยใดๆ อย่าใช้ตัวแปรท้องถิ่นในโปรโตเธรด!

คุณสมบัติหลัก:

  • ไม่มีโค้ดที่เชื่อมโยงกับแอสเซมเบลอร์ - ไลบรารี protothread เขียนด้วย pure C
  • ไม่ใช้ฟังก์ชันที่มีแนวโน้มเกิดข้อผิดพลาด เช่น longjmp()
  • การใช้หน่วยความจำ RAM ต่ำมาก - เพียง 2 ไบต์ต่อโปรโตเธรด
  • สามารถใช้ได้ทั้งระบบปฏิบัติการ (ที่มีมัลติเธรด) และไม่ใช้
  • ให้การรอบล็อกโดยไม่ต้องยุ่งเกี่ยวกับมัลติเธรดหรือสแต็ก

ตัวอย่างแอปพลิเคชันที่คุณสามารถใช้ได้:

  • ระบบที่มีข้อจำกัดด้านหน่วยความจำ
  • สแต็กโปรโตคอลที่ขับเคลื่อนด้วยเหตุการณ์
  • ระบบฝังตัวขนาดเล็กมาก
  • โหนดเครือข่ายสำหรับเซ็นเซอร์

Protostreams API ประกอบด้วยการดำเนินการพื้นฐาน 4 รายการ สิ่งเหล่านี้คือการเริ่มต้น PT_INIT() การดำเนินการ PT_BEGIN() การบล็อกตามเงื่อนไข PT_WAIT_UNTIL() และทางออก PT_END() นอกจากนี้ เพื่อความสะดวก ยังมีฟังก์ชันเพิ่มเติมอีก 2 ฟังก์ชัน: การบล็อกโดยเงื่อนไขย้อนกลับ PT_WAIT_WHILE() และการบล็อกบนโปรโตเธรด PT_WAIT_THREAD()

ดูสิ่งนี้ด้วย:เอกสาร Protothread API

ผู้เขียน

ไลบรารี Protothread เขียนโดย Adam Dunkels ด้วยการสนับสนุนจากโอลิเวอร์ ชมิดต์ .

โปรโตเธรด

Protothreads เป็นเธรดที่มีน้ำหนักเบามาก และไม่มีสแต็กที่ให้บริบทการล็อคที่ด้านบนของระบบที่ขับเคลื่อนด้วยเหตุการณ์ โดยไม่มีโอเวอร์เฮดของสแต็กของแต่ละเธรด วัตถุประสงค์ของโปรโตเธรดคือการใช้โฟลว์การดำเนินการตามลำดับโดยไม่ต้องใช้เครื่องสถานะที่ซับซ้อนหรือมัลติเธรดเต็มรูปแบบ Protothreads จัดให้มีการบล็อกแบบมีเงื่อนไขสำหรับการเรียกใช้โค้ดภายในเนื้อความของฟังก์ชัน C

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

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

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

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

ตัวแปรท้องถิ่น

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

การกำหนดเวลา (ตัวกำหนดเวลางาน) ของโปรโตเธรด

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

การนำไปปฏิบัติ

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

ความต่อเนื่องในท้องถิ่นสามารถนำไปใช้ได้หลายวิธี:

  1. โดยใช้โค้ดแอสเซมเบลอร์เฉพาะสถาปัตยกรรม
  2. ใช้โครงสร้าง C มาตรฐานหรือ
  3. การใช้ส่วนขยายคอมไพเลอร์

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

การใช้งาน C มาตรฐานต้องการเพียง 2 ไบต์ต่อโปรโตสตรีมเพื่อจัดเก็บสถานะ และใช้คำสั่ง C switch() ในลักษณะที่ไม่ชัดเจน อย่างไรก็ตาม การใช้งานนี้ทำให้เกิดข้อจำกัดเล็กน้อยเกี่ยวกับโค้ดที่ใช้โปรโตเธรด - ตัวโค้ดเองไม่สามารถใช้คำสั่ง switch() ได้

คอมไพเลอร์บางตัวมีส่วนขยาย C ที่สามารถใช้เพื่อใช้งานโปรโตเธรด GCC รองรับตัวชี้ป้ายกำกับที่สามารถใช้เพื่อจุดประสงค์นี้ ด้วยการใช้งานนี้ protothreads จะต้องมี RAM 4 ไบต์ต่อ protothread

มาโคร

ตัวอย่าง:ดีเอชซีพีซี.ซี.

ตัวอย่าง:ดีเอชซีพีซี.ซี.

ดูสิ่งนี้ด้วย: PT_SPAWN() ตัวอย่าง:ดีเอชซีพีซี.ซี.

ดูคำจำกัดความในไฟล์

เสื้อยืดและโปรแกรมคอมพิวเตอร์มีอะไรเหมือนกัน? ทั้งสองประกอบด้วยหลายกระทู้! ในขณะที่ด้ายในเสื้อยืดยึดผ้าไว้ด้วยกัน ด้าย C (แปลว่า “ด้าย” หรือ “ด้าย”) ระบบปฏิบัติการรวมโปรแกรมทั้งหมดเพื่อดำเนินการตามลำดับหรือแบบขนานพร้อมกัน แต่ละเธรดในโปรแกรมระบุกระบวนการที่ทำงานเมื่อระบบ (ระบบ Thread C) ร้องขอ สิ่งนี้จะปรับการทำงานของอุปกรณ์ที่ซับซ้อนเช่น คอมพิวเตอร์ส่วนบุคคลและมีผลดีต่อความเร็วและประสิทธิภาพของมัน

คำนิยาม

ในด้านวิทยาการคอมพิวเตอร์ C, Thread หรือ Thread of Execution เป็นลำดับคำสั่งที่เล็กที่สุดที่ถูกควบคุมโดยตัวกำหนดตารางเวลาอิสระ ซึ่งโดยปกติจะเป็นส่วนหนึ่งของระบบปฏิบัติการ

โดยทั่วไปเธรดจะได้รับลำดับความสำคัญเฉพาะ ซึ่งหมายความว่าบางเธรดมีลำดับความสำคัญเหนือเธรดอื่นๆ เมื่อโปรเซสเซอร์เสร็จสิ้นการประมวลผลหนึ่งเธรดแล้ว ก็สามารถเริ่มต้นเธรดถัดไปที่รออยู่ในคิวได้ โดยปกติแล้ว การรอจะไม่เกินสองสามมิลลิวินาที โปรแกรมคอมพิวเตอร์การใช้ "มัลติเธรด" สามารถดำเนินการหลายเธรดพร้อมกันได้ ระบบปฏิบัติการสมัยใหม่ส่วนใหญ่รองรับ C Thread ในระดับระบบ ซึ่งหมายความว่าเมื่อโปรแกรมหนึ่งพยายามเข้าครอบครองทรัพยากร CPU ทั้งหมด ระบบจะบังคับให้สลับไปยังโปรแกรมอื่นและบังคับให้โปรแกรมสนับสนุน CPU แบ่งปันทรัพยากรอย่างเท่าเทียมกัน

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

เธรด C มัลติเธรดบน Windows

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

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

ตัวอย่างของเครื่องมืองาน C Thread

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

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

มัลติทาสกิ้ง

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

ย้อนหลังทางประวัติศาสตร์

ระบบปฏิบัติการในยุคแรกๆ สามารถรันหลายโปรแกรมพร้อมกันได้ แต่ไม่รองรับการทำงานหลายอย่างพร้อมกันอย่างสมบูรณ์ โปรแกรมหนึ่งอาจใช้ทรัพยากร CPU ทั้งหมดในขณะที่ดำเนินการบางอย่าง งานระบบปฏิบัติการขั้นพื้นฐาน เช่น การคัดลอกไฟล์ ทำให้ผู้ใช้ไม่สามารถทำงานอื่นได้ (เช่น การเปิดหรือปิดหน้าต่าง)

ระบบปฏิบัติการสมัยใหม่มีการรองรับการทำงานหลายอย่างพร้อมกันอย่างเต็มรูปแบบ โดยโซลูชันซอฟต์แวร์หลายตัวสามารถทำงานพร้อมกันได้โดยไม่รบกวนฟังก์ชันการทำงานของกันและกัน

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

ระบบตัวประมวลผลเดี่ยวและหลายตัว

การใช้งานเทคโนโลยีเธรดและโปรเซสเซอร์แตกต่างกันไปขึ้นอยู่กับระบบปฏิบัติการ แต่บ่อยครั้งที่เธรดเป็นส่วนประกอบของกระบวนการ สามารถมีหลายเธรดพร้อมกันในกระบวนการเดียว โดยดำเนินการและแบ่งปันทรัพยากร โดยเฉพาะอย่างยิ่งเธรดกระบวนการ C Thread แบ่งปันรหัสปฏิบัติการและค่าตัวแปรในเวลาใดก็ตาม

ระบบที่มีโปรเซสเซอร์ตัวเดียวจะใช้มัลติเธรดตามเวลา: ซีพียู(CPU) สลับระหว่างเธรดต่างๆ ซอฟต์แวร์- ในโปรเซสเซอร์หลายตัว เช่นเดียวกับระบบมัลติคอร์ เธรดจำนวนหนึ่งจะถูกดำเนินการพร้อมกัน โดยแต่ละโปรเซสเซอร์หรือคอร์จะดำเนินการเธรดแยกกันพร้อม ๆ กัน

ประเภทของลำธาร

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

หัวข้อและกระบวนการ: ความแตกต่าง

เธรดแตกต่างจากกระบวนการระบบปฏิบัติการมัลติทาสกิ้งแบบคลาสสิกในลักษณะดังต่อไปนี้:

    โดยทั่วไปกระบวนการจะเป็นอิสระ ในขณะที่เธรดมีอยู่เป็นเซตย่อยของกระบวนการ

    กระบวนการมีข้อมูลมากกว่าเธรด

    กระบวนการมีช่องที่อยู่เฉพาะ

    กระบวนการโต้ตอบผ่านกลไกการสื่อสารของระบบเท่านั้น

    การสลับบริบทระหว่างเธรดในกระบวนการเกิดขึ้นเร็วกว่าการสลับบริบทระหว่างกระบวนการ

การวางแผนป้องกันและร่วมมือกัน

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

วิวัฒนาการของเทคโนโลยี

จนกระทั่งต้นทศวรรษ 2000 มากที่สุด คอมพิวเตอร์ตั้งโต๊ะมีโปรเซสเซอร์แบบคอร์เดียวเพียงตัวเดียวซึ่งไม่รองรับเธรดฮาร์ดแวร์ ในปี 2545 Intel ได้เปิดตัวการสนับสนุนมัลติเธรดพร้อมกันบนโปรเซสเซอร์ Pentium 4 ซึ่งเรียกว่า Hyper-Threading ในปี 2548 โปรเซสเซอร์แบบดูอัลคอร์และดูอัลคอร์ โปรเซสเซอร์เอเอ็มดีแอธลอน 64 X2.

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

โมเดล

ให้เราแสดงรายการโมเดลการใช้งานหลักๆ

1:1 (เธรดระดับเคอร์เนล) - เธรดที่สร้างโดยผู้ใช้ในเคอร์เนลเป็นการใช้งานเธรดที่ง่ายที่สุดที่เป็นไปได้ OS/2 และ Win32 ใช้แนวทางนี้โดยกำเนิด ในขณะที่ Linux Thread join ใช้แนวทางนี้ผ่าน NPTL หรือ LinuxThreads ที่เก่ากว่า วิธีการนี้ยังใช้โดย Solaris, NetBSD, FreeBSD, macOS และ iOS

N: 1 (เธรดผู้ใช้) - โมเดลนี้ต้องการให้เธรดระดับแอปพลิเคชันทั้งหมดแมปกับออบเจ็กต์ที่กำหนดเวลาไว้ระดับเคอร์เนลเดียว ด้วยแนวทางนี้ การสลับบริบทสามารถทำได้อย่างรวดเร็ว และยิ่งไปกว่านั้น ยังสามารถนำไปใช้กับเคอร์เนลที่ไม่รองรับเธรดได้อีกด้วย อย่างไรก็ตามข้อเสียเปรียบหลักประการหนึ่งคือไม่ได้รับประโยชน์ การเร่งความเร็วด้วยฮาร์ดแวร์บนโปรเซสเซอร์แบบมัลติเธรดหรือคอมพิวเตอร์ ตัวอย่างเช่น หากหนึ่งในเธรดจำเป็นต้องดำเนินการเมื่อมีการร้องขอ I/O กระบวนการทั้งหมดจะถูกบล็อกและเธรดจะไม่สามารถใช้ได้ ใน GNU Portable C ข้อยกเว้นของเธรดจะถูกใช้เป็นเธรดระดับผู้ใช้

M:N (การใช้งานแบบไฮบริด) - โมเดลจะจับคู่เธรดแอปพลิเคชันจำนวนหนึ่งกับเซลล์เคอร์เนลจำนวน N จำนวนหนึ่งหรือ "ตัวประมวลผลเสมือน" นี่คือการประนีประนอมระหว่างเธรดระดับเคอร์เนล ("1:1") และระดับผู้ใช้ ("N:1") ระบบสตรีมมิ่ง M:N มีความซับซ้อนมากขึ้น โดยจำเป็นต้องเปลี่ยนแปลงทั้งเคอร์เนลและรหัสผู้ใช้ ในการใช้งาน M:N ไลบรารีการประมวลผลเธรดมีหน้าที่รับผิดชอบในการกำหนดเวลาเธรดบนเอนทิตีที่กำหนดเวลาได้ที่มีอยู่ สิ่งนี้ทำให้บริบทเหมาะสมที่สุดเนื่องจากจะหลีกเลี่ยงการเรียกของระบบ อย่างไรก็ตาม สิ่งนี้จะเพิ่มความซับซ้อนและความเป็นไปได้ของการกลับรายการ เช่นเดียวกับการตั้งเวลาที่ต่ำกว่าปกติโดยไม่มีการประสานงานที่กว้างขวาง (และมีราคาแพง) ระหว่างตัวกำหนดเวลาสภาพแวดล้อมของผู้ใช้และตัวกำหนดเวลาเคอร์เนล

ตัวอย่างของการใช้งานแบบไฮบริดคือการเปิดใช้งานตัวกำหนดเวลาที่ใช้โดยการใช้งานไลบรารี POSIX ดั้งเดิมของ NetBSD (สำหรับโมเดล M:N ซึ่งตรงข้ามกับโมเดลการใช้งานเคอร์เนล 1:1 หรือโมเดลพื้นที่ผู้ใช้)

กระบวนการน้ำหนักเบาที่ใช้โดยระบบปฏิบัติการ Solaris เวอร์ชันเก่า (ชุดเครื่องมือ Std Thread C)

รองรับภาษาการเขียนโปรแกรม

ระบบที่เป็นทางการหลายระบบรองรับการทำงานของเธรด การใช้งาน C และ C++ ใช้เทคโนโลยีนี้และให้การเข้าถึง API ดั้งเดิมสำหรับระบบปฏิบัติการ ภาษาโปรแกรมบางภาษามีมากกว่านั้น ระดับสูงภาษาต่างๆ เช่น Java, Python และ .NET Framework เปิดเผยเธรดให้กับนักพัฒนา ในขณะเดียวกันก็แยกความแตกต่างเฉพาะในการใช้งานรันไทม์ของเธรดออกไป ส่วนขยายภาษาอื่นๆ ยังพยายามที่จะสรุปแนวคิดเรื่องการทำงานพร้อมกันและเธรดจากนักพัฒนา บางภาษาได้รับการออกแบบมาเพื่อการทำงานแบบขนานตามลำดับโดยใช้ GPU

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

การใช้งานโปรแกรมอื่นๆ เช่น Tcl ใช้ส่วนขยาย Thread sleep C ซึ่งจะช่วยหลีกเลี่ยงขีดจำกัดสูงสุดของ GIL โดยใช้โมเดลที่เนื้อหาและโค้ดต้อง "แชร์" อย่างชัดเจนระหว่างเธรด

ภาษาการเขียนโปรแกรมแอปพลิเคชันที่ขับเคลื่อนด้วยเหตุการณ์ เช่น Verilog และส่วนขยาย Thread sleep C มีโมเดลเธรดที่แตกต่างกันซึ่งรองรับจำนวนเธรดสูงสุดสำหรับการจำลองฮาร์ดแวร์

มัลติเธรดที่ใช้งานได้จริง

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

แท็ก: pthreads, pthread_create, pthread_join, EINVAL, ESRCH, EDEADLK, EDEADLOCK, EAGAIN, EPERM, PTHREAD_THREADS_MAX, การส่งผ่านอาร์กิวเมนต์ไปยังเธรด, การส่งคืนอาร์กิวเมนต์จากเธรด, ข้อผิดพลาด pthread_create, ข้อผิดพลาด pthread_join, กำลังรอเธรด, การเข้าร่วมเธรด, ตัวระบุเธรด, pthreads ตัวอย่าง.

การสร้างและรอเธรด

ลองดูตัวอย่างง่ายๆ

#รวม #รวม #รวม #รวม #define ERROR_CREATE_THREAD -11 #define ERROR_JOIN_THREAD -12 #define SUCCESS 0 void* helloWorld(void *args) ( printf("Hello from thread!\n"); return SUCCESS; ) int main() ( pthread_t thread; int status; int status_addr; status = pthread_create(&thread, NULL, helloWorld, NULL); if (สถานะ != 0) ( printf("ข้อผิดพลาดหลัก: ไม่สามารถสร้างเธรด, สถานะ = %d\n", สถานะ); exit(ERROR_CREATE_THREAD ) printf("สวัสดีจาก main!\n"); status = pthread_join(thread, (void**)&status_addr); if (status != SUCCESS) ( printf("main error: can"t join thread, status = %d\n", status); exit(ERROR_JOIN_THREAD); ) printf("joined with address %d\n", status_addr); _getch(); return 0; )

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

เธรดใหม่ถูกสร้างขึ้นโดยใช้ฟังก์ชัน pthread_create

Int pthread_create(*ptherad_t, const pthread_attr_t *attr, void* (*start_routine)(void*), void *arg);

ฟังก์ชันรับตัวชี้ไปยังเธรดเป็นอาร์กิวเมนต์ ซึ่งเป็นตัวแปรประเภท pthread_t ซึ่งหากดำเนินการเสร็จเรียบร้อยแล้ว ฟังก์ชันจะบันทึกรหัสเธรด pthread_attr_t - คุณลักษณะของเธรด หากใช้แอตทริบิวต์เริ่มต้น จะสามารถส่งค่า NULL ได้ start_routin คือฟังก์ชันที่จะดำเนินการในเธรดใหม่ หาเรื่องคืออาร์กิวเมนต์ที่จะถูกส่งไปยังฟังก์ชัน

เธรดสามารถทำสิ่งต่าง ๆ มากมายและรับข้อโต้แย้งที่แตกต่างกันมากมาย เมื่อต้องการทำเช่นนี้ ฟังก์ชันที่จะเปิดตัวในเธรดใหม่จะใช้อาร์กิวเมนต์ประเภท void* ด้วยเหตุนี้ คุณจึงสามารถรวมอาร์กิวเมนต์ที่ส่งผ่านทั้งหมดไว้ในโครงสร้างได้ คุณยังสามารถส่งกลับค่าผ่านอาร์กิวเมนต์ที่ส่งผ่านได้

หากสำเร็จฟังก์ชันจะคืนค่า 0 หากเกิดข้อผิดพลาดค่าต่อไปนี้อาจส่งคืนได้

  • อีกครั้ง– ระบบไม่มีทรัพยากรในการสร้างเธรดใหม่ หรือระบบไม่สามารถสร้างเธรดเพิ่มเติมได้เนื่องจากจำนวนเธรดเกินค่า PTHREAD_THREADS_MAX (เช่น บนเครื่องใดเครื่องหนึ่งที่ใช้ทดสอบ หมายเลขวิเศษนี้คือ 2019 )
  • ไอน์วาล– แอตทริบิวต์สตรีมไม่ถูกต้อง (ส่งผ่านเป็นอาร์กิวเมนต์ attr)
  • อีเปิร์ม– เธรดการโทรไม่มีสิทธิ์ที่เหมาะสมในการตั้งค่า พารามิเตอร์ที่จำเป็นหรือนโยบายกำหนดการ

มาดูโปรแกรมกันดีกว่า

#define ERROR_CREATE_THREAD -11 #define ERROR_JOIN_THREAD -12 #define SUCCESS 0

ที่นี่เราระบุชุดค่าที่จำเป็นในการจัดการข้อผิดพลาดที่อาจเกิดขึ้น

Void* helloWorld(void *args) ( printf("Hello from thread!\n"); return SUCCESS; )

นี่คือฟังก์ชันที่จะทำงานในเธรดที่แยกจากกัน มันจะไม่ได้รับการโต้แย้งใด ๆ ตามมาตรฐาน ถือว่าการออกจากฟังก์ชันอย่างชัดเจนเรียกใช้ฟังก์ชัน pthread_exit และค่าที่ส่งคืนจะถูกส่งไปเมื่อเรียกใช้ฟังก์ชัน pthread_join เป็นสถานะ

สถานะ = pthread_create(&thread, NULL, helloWorld, NULL); if (status != 0) ( printf("main error: can"t create thread, status = %d\n", status); exit(ERROR_CREATE_THREAD); )

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

สถานะ = pthread_join(thread, (void**)&status_addr); if (status != SUCCESS) ( printf("main error: can"t join thread, status = %d\n", status); exit(ERROR_JOIN_THREAD); )

ทำให้เธรดหลักรอให้เธรดลูกดำเนินการเสร็จสิ้น การทำงาน

Int pthread_join (เธรด pthread_t เป็นโมฆะ **value_ptr);

ชะลอการดำเนินการของการเรียกเธรด (ฟังก์ชันนี้) จนกว่าจะมีการดำเนินการ ด้าย- เมื่อ pthread_join สำเร็จ จะส่งคืนค่า 0 หากเธรดส่งคืนค่าอย่างชัดเจน (ซึ่งเป็นค่า SUCCESS เดียวกันจากฟังก์ชันของเรา) เธรดนั้นจะถูกวางไว้ในตัวแปร value_ptr ข้อผิดพลาดที่เป็นไปได้ส่งคืนโดย pthread_join

  • ไอน์วาล– เธรดชี้ไปที่เธรดที่ไม่รวม
  • อีสอาร์ช– ไม่มีเธรดที่มีตัวระบุเดียวกันจัดเก็บโดยตัวแปรเธรด
  • EDEADL– ตรวจพบการหยุดชะงัก (การบล็อกร่วมกัน) หรือการเรียกเธรดเองถูกระบุเป็นเธรดที่ผสาน

ตัวอย่างการสร้างเธรดโดยส่งอาร์กิวเมนต์ไปให้เธรดเหล่านั้น

สมมติว่าเราต้องการส่งข้อมูลไปยังสตรีมและส่งคืนบางสิ่งกลับมา สมมติว่าเราส่งสตริงไปยังสตรีมและส่งกลับความยาวของสตริงนี้จากสตรีม

เนื่องจากฟังก์ชันสามารถรับได้เฉพาะพอยน์เตอร์ประเภท void เท่านั้น อาร์กิวเมนต์ทั้งหมดจึงควรถูกรวมไว้ในโครงสร้าง มากำหนดโครงสร้างประเภทใหม่กัน:

Typedef struct someArgs_tag ( int id; const char *msg; int out; ) someArgs_t;

ในที่นี้ id คือตัวระบุเธรด (โดยทั่วไป ไม่จำเป็นในตัวอย่างของเรา) ฟิลด์ที่สองคือสตริง และฟิลด์ที่สามคือความยาวของสตริงที่เราจะส่งคืน

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

Void* helloWorld(void *args) ( someArgs_t *arg = (someArgs_t*) args; int len; if (arg->msg == NULL) ( return BAD_MESSAGE; ) len = strlen(arg->msg); printf(" %s\n", arg->msg); arg->out = len; return SUCCESS; )

หากทุกอย่างเป็นไปด้วยดี เราจะส่งคืนค่า SUCCESS เป็นสถานะ และหากมีข้อผิดพลาดเกิดขึ้น (ในกรณีของเรา หากส่งสตริงเป็นศูนย์) เราจะออกโดยมีสถานะ BAD_MESSAGE

ในตัวอย่างนี้ เราจะสร้าง 4 เธรด สำหรับ 4 เธรด คุณจะต้องมีอาร์เรย์ประเภท pthread_t ที่มีความยาว 4 อาร์เรย์ของอาร์กิวเมนต์ที่ส่งผ่าน และ 4 สตริง ซึ่งเราจะส่งผ่าน

เธรด Pthread_t; สถานะ int; ฉัน; int status_addr; args_t บางส่วน; const char *messages = ( "แรก", NULL, "ข้อความที่สาม", "ข้อความที่สี่" );

ก่อนอื่นเรากรอกค่าของอาร์กิวเมนต์

สำหรับ (i = 0; i< NUM_THREADS; i++) { args[i].id = i; args[i].msg = messages[i]; }

สำหรับ (i = 0; i< NUM_THREADS; i++) { status = pthread_create(&threads[i], NULL, helloWorld, (void*) &args[i]); if (status != 0) { printf("main error: can"t create thread, status = %d\n", status); exit(ERROR_CREATE_THREAD); } }

จากนั้นเราก็รอให้เสร็จสิ้น

สำหรับ (i = 0; i< NUM_THREADS; i++) { status = pthread_join(threads[i], (void**)&status_addr); if (status != SUCCESS) { printf("main error: can"t join thread, status = %d\n", status); exit(ERROR_JOIN_THREAD); } printf("joined with address %d\n", status_addr); }

สุดท้ายนี้ เรายังส่งออกอาร์กิวเมนต์ซึ่งขณะนี้เก็บค่าที่ส่งคืนแล้ว โปรดสังเกตว่าหนึ่งในอาร์กิวเมนต์คือ "ไม่ดี" (สตริงเป็น NULL) ที่นี่ รหัสเต็ม

#รวม #รวม #รวม #รวม #รวม #define ERROR_CREATE_THREAD -11 #define ERROR_JOIN_THREAD -12 #define BAD_MESSAGE -13 #define SUCCESS 0 typedef struct someArgs_tag ( int id; const char *msg; int out; ) someArgs_t; void* helloWorld(void *args) ( someArgs_t *arg = (someArgs_t*) args; int len; if (arg->msg == NULL) ( return BAD_MESSAGE; ) len = strlen(arg->msg); printf(" %s\n", arg->msg); arg->out = len; return SUCCESS; ) #define NUM_THREADS 4 int main() ( pthread_t threads; int status; int i; int status_addr; someArgs_t args; const char * ข้อความ = ("แรก", NULL, "ข้อความที่สาม", "ข้อความที่สี่" ); สำหรับ (i = 0; i< NUM_THREADS; i++) { args[i].id = i; args[i].msg = messages[i]; } for (i = 0; i < NUM_THREADS; i++) { status = pthread_create(&threads[i], NULL, helloWorld, (void*) &args[i]); if (status != 0) { printf("main error: can"t create thread, status = %d\n", status); exit(ERROR_CREATE_THREAD); } } printf("Main Message\n"); for (i = 0; i < NUM_THREADS; i++) { status = pthread_join(threads[i], (void**)&status_addr); if (status != SUCCESS) { printf("main error: can"t join thread, status = %d\n", status); exit(ERROR_JOIN_THREAD); } printf("joined with address %d\n", status_addr); } for (i = 0; i < NUM_THREADS; i++) { printf("thread %d arg.out = %d\n", i, args[i].out); } _getch(); return 0; }

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



กำลังโหลด...
สูงสุด