โมดูล เกลียวเปิดตัวครั้งแรกใน 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 รอจนกว่าเธรดจะเสร็จสิ้นการประมวลผล เธรดแรกจะเสร็จสิ้นเมื่อไม่มีอะไรต้องส่งผ่านไปยังคิวอีกต่อไป
มาสรุปกัน
เราได้ครอบคลุมเนื้อหาค่อนข้างมาก กล่าวคือ:
- พื้นฐานการทำงานกับโมดูลเธรด
- ล็อคทำงานอย่างไร?
- Event คืออะไร และนำไปใช้ได้อย่างไร
- วิธีการใช้งานตัวจับเวลา
- การสื่อสารภายในเธรดโดยใช้คิว/เหตุการณ์
ตอนนี้คุณรู้วิธีใช้เธรดแล้วและมีประโยชน์อย่างไร ฉันหวังว่าคุณจะพบว่ามีประโยชน์บางอย่างในโค้ดของคุณเอง!
คำอธิบายโดยละเอียด
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 ซึ่งสร้างขึ้นบนโปรโตเธรด
ความต่อเนื่องในท้องถิ่นสามารถนำไปใช้ได้หลายวิธี:
- โดยใช้โค้ดแอสเซมเบลอร์เฉพาะสถาปัตยกรรม
- ใช้โครงสร้าง C มาตรฐานหรือ
- การใช้ส่วนขยายคอมไพเลอร์
วิธีแรกทำงานโดยการบันทึกและกู้คืนสถานะโปรเซสเซอร์ ไม่รวมตัวชี้สแต็ก และต้องใช้ขนาด 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 ตัวอย่าง.
การสร้างและรอเธรด
ลองดูตัวอย่างง่ายๆ
#รวม
ในตัวอย่างนี้ ภายในเธรดหลักที่ ฟังก์ชั่นหลักเธรดใหม่จะถูกสร้างขึ้น ภายในซึ่งมีการเรียกใช้ฟังก์ชัน 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) ที่นี่ รหัสเต็ม
#รวม
ทำหลายครั้ง โปรดทราบว่าลำดับที่เธรดดำเนินการไม่ได้ถูกกำหนดไว้ ด้วยการรันโปรแกรม คุณจะได้รับลำดับการดำเนินการที่แตกต่างกันในแต่ละครั้ง