luận văn về nghiên cứu lập trình thread và ứng dụng
Trang 1LỜI CẢM ƠN
Lời đầu tiên tôi xin được gửi lời cảm ơn chân thành tới các thầy cô giáo Trường Đại Học Công Nghệ - Đại Học Quốc Gia Hà Nội, đặc biệt là các thầy cô trong khoa Công Nghệ Thông Tin, những người đã trực tiếp chỉ bảo tôi những kiến thức trong suốt bốn năm học vừa qua trên ghế giảng đường
Đặc biệt tôi xin được bày tỏ lòng kính trọng và biết ơn tới TS Nguyễn Hải Châu người đã trực tiếp hướng dẫn, giúp đỡ tôi hoàn thành khóa luận này
Xin được gửi lời chúc sức khỏe và hạnh phúc tới tất cả các thầy cô Xin chúc thầy cô đạt được nhiều thành tựu hơn nữa trong sự nghiệp đào tạo tri thức cho đất nước cũng như trong các công việc nghiên cứu khoa học
Trân trọng cảm ơn!
Hà Nội, Ngày 20 Tháng 05 năm 2010
Sinh viên thực hiện
Cấn Việt Dũng
Trang 2TÓM TẮT KHÓA LUẬN
Trong khóa luận này, tôi sẽ trình bày lý thuyết về Thread, lập trình posix thread
và cài đặt một bài toán sử dụng posix thread để thấy được hiệu quả của việc sử dụng chúng Phần đầu tiên,tôi giới thiêu lý thuyết về Thread, Multi thread và các vấn đề liên quan Tiếp theo, tôi trình bày về lập trình posix thread trên hệ điều hành linux, bao gồm cách tạo, ngắt thread và đồng bộ các thread trong chương trình Cuối cùng,
là cách cài đặt bài toán “tìm cặp điểm gần nhau nhất trong tập N điểm cho trước”( qui ước bài toán này là bài toán closest_pair) sử dụng posix thread trên ngôn ngữ C để thấy được sự hiệu quả của việc sử dụng multithread trong việc nâng cao hiệu năng của chương trình, ở đây cụ thể là tốc độ tính toán tăng lên rõ rệt
Trang 3MỤC LỤC
CHƯƠNG 1: GIỚI THIỆU VỀ THREAD VÀ MULTI THREAD 7
1.1 Tổng quan về thread 7
1.2 So sánh thread với tiến trình 7
1.3 Đa thread: những lợi thế 8
1.4 Tiến trình, thread nhân, thread người dùng, fiber 9
1.5 Vấn đề đưa ra của thread và fiber 10
1.5.1.Truy cập đồng thời và cấu trúc dữ liệu 10
1.5.2.Vào/ ra và bộ lập lịch 11
1.6 Các mô hình 12
1.6.1. Mô hình 1:1 (thread cấp nhân) 12
1.6.2. Mô hình N:1 (thread cấp người dùng) 12
1.6.3. Mô hình N:M (thread tích hợp) 12
1.7 Ngôn ngữ hỗ trợ 13
CHƯƠNG 2: POSIX THREAD PROGRAMMING 14
2.1 Tổng quan về Pthread 14
2.1.1. Khái niệm Pthread 14
2.1.2. Tại sao lại sử dụng Pthread? 14
2.1.3. Pthread API 16
2.1.4. Biên dịch chương trình Threaded 17
Trang 42.2 Quản lý Thread 18
2.2.1. Các thủ tục chính 18
2.2.2. Tạo Thread 18
2.2.3. Thiết lập các thuộc tính cho Thread 19
2.2.4. Hủy thread 19
2.2.5. Truyền tham số cho Thread 21
2.2.6. Nối và tách Thread 22
2.2.6.1 Những thủ tục chính 23
2.2.6.2 Nối Thread 23
2.2.6.3 Có thể nối được hay không? 23
2.2.6.4 Tách (detaching) 24
2.2.7. Quản lý stack 26
2.2.7.1 Những thủ tục 26
2.2.7.2 Ngăn ngừa những vấn đề với stack 26
2.3 Biến Mutex 26
2.3.1. Khái niệm mutex 26
2.3.2. Tạo ra và phá hủy mutex 27
2.3.2.1 Những thủ tục 27
2.3.2.2 Cách sử dụng 28
2.3.3. Khóa và mở khóa mutex 28
2.3.3.1 Các thủ tục 28
2.3.3.2 Cách sử dụng 28
2.4 Biến điều kiện 33
2.4.1. Khái niệm về biến điều kiện 33
Trang 52.4.2 Tạo ra và phá hủy 1 biến điều kiện 35
2.4.2.1 Các thủ tục 35
2.4.2.2 Cách sử dụng 35
2.4.3 Waiting và signaling trên biến điều kiện 35
2.4.3.1 Các thủ tục 36
2.4.3.2 Cách sử dụng 36
2.5 Dữ liệu riêng của Thread(Thread – specific data) 39
2.5.1. Khái niệm dữ liệu riêng của thread 39
2.5.2 Cấp phát dữ liệu riêng của thread 39
2.5.3 Truy cập vào dữ liệu riêng của thread 40
2.5.4.Xóa dữ liệu trong thread 42
CHƯƠNG 3: BÀI TOÁN CLOSEST_PAIR TRONG KHÔNG GIAN HAI CHIỀU SỬ DỤNG MULTITHREADING 43
3.1 Giới thiệu bài toán 43
3.2 Các thuật toán khác nhau để giải bài toán tìm khoảng cách ngắn nhất giữa các cặp điểm trong N điểm cho trước 43
Trang 6GIỚI THIỆU
Thread là một mô hình lập trình phổ biến cho phép nhiều thread đơn có thể chạy trên cùng một tiến trình, và các thread này có thể chia sẻ tài nguyên của tiến trình cũng như có thể tính toán độc lập Và ứng dụng hữu ích nhất của mô hình này
là khi nó được áp dụng cho một tiến trình đơn lẻ để cho phép tính toán song song trên một hệ thống đa xử lý Trong khóa luận này, tôi sẽ trình bày mô hình này trên chuẩn IEEE POSIX 1003.1c, được gọi là POSIX thread hay Pthread Lý do tôi chọn Pthread, là để nhận ra hiệu quả tiềm năng của chương trình, việc tạo ra một thread
sử dụng ít tài nguyên và chi phí của hệ điều hành hơn rất nhiều so với việc tạo ra một tiến trình
Nội dung chính của khóa luận bao gồm 3 chương, nội dung cụ thể như sau:
Chương I: Giới thiệu về thread và multi thread Chương này tập trung giới thiệu
về thread và multi thread, so sánh giữa thread với tiến trình và cùng với đó là những lợi thế khi sử dụng multi thread Cuối cùng là các mô hình thread và các ngôn ngữ
hỗ trợ
Chương II: Lập trình POSIX thread Chương này sẽ đề cập tới các vấn đề cơ bản
trong lập trình POSIX thread (Pthread) Các vấn đề được đề cập bao gồm việc quản
lý thread, tạo, hủy, tách và nối thread Các biến mutex, biến điều kiện và cách sử dụng Mỗi phần đều có những ví dụ minh họa
Chương III: Bài toán closest_pair Chương này tôi sẽ cài đặt bài toán closest_pair
hai chiều bằng các phương pháp thông thường, đệ qui và đệ qui sử dụng multi thread
để thấy được hiệu quả của việc sử dụng multi thread
Trang 7Trên một bộ xử lý đơn, multi thread thường xảy ra bởi sự phân chia thời gian ghép (như trong multitasking): bộ xử lý chuyển giữa những thread khác nhau Ngữ cảnh chuyển thường xảy ra một cách thường xuyên đủ để người dùng nhận thấy được nhiều thread hoặc nhiệm vụ đang chạy tại cùng một thời điểm Trên một bộ đa
xử lý hoặc hệ thống nhiều nhân, những thread hoặc nhiệm vụ sẽ chạy cùng lúc, với mỗi một bộ xử lý hoặc nhân chạy một thread hoặc nhiệm vụ riêng
Nhiều hệ điều hành hiện đại hỗ trợ trực tiếp sự phân chia thời gian hoặc đa thread với một bộ lập lịch tiến trình Nhân của hệ điều hành cho phép người lập trình tính toán thread bằng các giao diện lời gọi hệ thống Một vài thể hiện được gọi là thread nhân, trong khi một tiến trình nhẹ (lightweight process) là một kiểu xác định của thread nhân để chia sẻ trạng thái và thông tin
Chương trình có thể có thread không gian người dùng khi lập trình thread với thời gian, tín hiệu hoặc những phương thức khác để làm gián đoạn thực hiện riêng của họ để thực hiện một sắp xếp ad-hoc hoặc chia thời gian
1.2 So sánh thread với tiến trình
Thread khác với tiến trình trong hệ điều hành đa nhiệm truyền thống ở các điểm sau:
- Các tiến trình thường được độc lập, còn các thread thì tồn tại như là các tập con của một tiến trình
- Tiến trình có trạng thái thông tin đáng kể, trong khi nhiều thread trong một tiến trình chia sẽ trạng thái tiến trình cũng như bộ nhớ và các tài nguyên khác
Trang 81.3 Đa thread: những lợi thế
Multi thread như là một mô hình lập trình phổ biến và cho phép thực hiện nhiều thread tồn tại trong một tiến trình đơn Những thread này chia sẻ tài nguyên của tiến trình nhưng cũng có thể tính toán độc lập Mô hình lập trình thread cung cấp cho người phát triển những sự hữu ích của việc tính toán đồng thời Tuy nhiên, có lẽ ứng dụng thú vị nhât cho công nghệ này là khi nó được áp dụng cho một tiến trình đơn lẻ để cho phép tính toán song song trên một hệ thống đa xử lý Lợi thế này của lập trình multi thread cho phép nó tính toán nhanh hơn trên hệ thống máy tính có nhiều CPU, CPU với nhiều nhân hoặc qua một cụm máy – bởi vì những thread của chương trình cho vay chính bản thân nó để thực hiện sự đồng thời Trong trường hợp này, người lập trình cần phải cẩn thận để tránh lỗi chạy điều kiện (race condition), và những đối xử không thuộc trực giác Để các dữ liệu được thao tác chính xác, những thread sẽ quy thời gian để xử lý dữ liệu theo thứ tự đúng Thread
có thể yêu cầu hoạt động độc quyền (thường được thực hiện bằng cách sử dụng semaphore) để ngăn chặn dữ liệu không bị đồng thời sửa đổi hoặc đọc trong khi quá trình đang bị sửa đổi
Một lợi thế khác của multi thread, kể cả đối với hệ thống đơn CPU, là có khả năng cho một ứng dụng vẫn đáp ứng được nhu cầu đầu vào Trong một chương trình thread đơn, nếu khối thread tính toán chính trên một nhiệm vụ lớn, toàn bộ ứng dụng
có thể xuất hiện để đóng băng Bằng cách di chuyển nhiệm vụ lớn này tới một thread worker để chạy đồng thời với thread tính toán chính, nó có thể cho các ứng dụng đáp ứng đầu vào của người dùng trong khi vẫn thực hiện nhiệm vụ tính toán
Hệ điều hành sắp xếp các thread theo một trong hai cách sau:
Trang 9- Chế độ lập lịch ưu tiên(preemptive) thường được coi là phương pháp tốt hơn,
vì nó cho phép hệ điều hành xác định khi nào một chuyển trạng thái xảy ra Sự bất lợi của phương pháp này là hệ điều hành có thể làm cho bối cảnh chuyển đổi tại một thời gian không thích hợp, gây đảo ngược ưu tiên hoặc các hiệu ứng tiêu cực nào khác mà có thể tránh bằng cooperative multithreading
- Cooperative multithreading, mặt khác,dựa vào bản thân các thread để từ bỏ kiểm soát sau khi tới một điểm dừng Điều này có thể tạo ra vấn đề nếu một thread đang đợi cho tới khi một tài nguyên trở nên sẵn sàng
Phần cứng máy tính truyền thống không có nhiều hỗ trợ cho multi thread, vì chuyển đổi giữa các thread nói chung đã nhanh hơn chuyển đổi giữa các tiến trình
Bộ xử lý trong hệ thống nhúng, có nhiều yêu cầu cao hơn đói với xử lý thời gian thực, có thể hỗ trợ multi thread bằng cách giảm thời gian chuyển đổi giữa các thread, có lẽ bằng cách cấp phát một tập tin đăng ký chuyên biệt dành riêng cho mỗi thread thay vì lưu trữ hoặc khôi phục lại một tập tin đăng ký phổ biến Vào cuối nhữn năm 90, ý tưởng thực hiện các tính toán đồng thời đã được biết đến như là multi thread đồng thời Tính năng này đã được giới thiệu trong bộ vi xử lý Pentium
4 của Intel, có tên là Hyper Threading
1.4 Tiến trình, thread nhân, thread người dùng, fiber
Một tiến trình là đơn vị nặng nhất của lập lịch nhân Tiến trình sở hữu tài nguyên được cấp phát bởi hệ điều hành Tài nguyên bao gồm bộ nhớ, xử lý tập tin, socket, thiết bị xử lý, và window Tiến trình không chia sẻ không gian địa chỉ hoặc tài nguyên tập tin ngoại trừ thông qua phương pháp rõ ràng như thừa kế tập tin xử lý hoặc các phân đoạn chia sẻ bộ nhớ, hoặc lập bản đồ cùng một tập tin tron một cách
đã được chia sẻ Tiến trình thường là đa nhiệm phòng ngừa (preemptively multitasked)
Một thread nhân là thành phần nhẹ nhất của bộ nhân lập lịch Ít nhất một thread nhân tồn tại trong một tiến trình Nếu nhiều thread nhân tồn trại trong một tiến trình, sau đó chúng chia sẻ bộ nhớ và tài nguyên tập tin Thread nhân là đa nhiệm phòng ngừa (preemptively multitasked) nếu quá trình lập lịch của hệ điều hành là “phòng ngừa” Thread nhân không sử hữu tài nguyên cho riêng nó ngoại trừ
Trang 10một ngăn xếp, một bản sao đăng ký bao gồm bộ đếm chương trình, và lưu trữ thread địa phương (thread-local) Hạt nhân có thể chỉ định một thread cho mỗi lõi (core) trong một hệ thống (bởi vì mỗi core tự chia nó thành nhiều core logic nếu nó hỗ trợ multi thread, hoặc chỉ hỗ trợ một core logic với mỗi core vật lý nếu không hỗ trợ multi thread) và có thể đổi chỗ thread để được chặn Tuy nhiên, thread nhân mất nhiều thời gian hơn thread người dùng khi đổi chỗ
Thread đôi khi được thể hiện trong thư viện không gian người dùng (userspace), do đó được gọi là user thread Kernel không phải để ý tới chúng, chúng được quản lý và lập lịch bởi userspace User thread như được thực hiện bởi máy ảo nên cũng được gọi là green thread User thread thường tạo ra và quản lý nhanh chóng, nhưng không thể tận dụng lợi thế của multi thread hay đa xử lý và bị chặn nếu tất cả các thread nhân liên quan bị chặn ngay cả khi có một số user thread đã sẵn sàng để chạy
Fibers là thành phần thậm chí còn nhẹ hơn cả đơn vị của bộ lập lịch dự kiến hợp tác: một fiber đang chạy phải rõ ràng “năng suất” để cho phép những fiber khác chạy, mà làm cho sự thể hiện của chúng dễ dàng hơn nhiều so với thread nhân hay thread người dùng Một fiber có thể được lập lịch để chạy trong bất kỳ thread nào trong cùng một tiến trình Điều này cho phép những ứng dụng đạt được những hiệu suất cải tiến bằng cách quản lý sự lập lịch, thay vì dựa vào bộ lập lịch của nhân (mà
có thể không được điều chỉnh cho ứng dụng) Môi trường lập trình song song như
OpenMP thường thể hiện nhiệm vụ của chúng thông qua fiber
1.5 Vấn đề đưa ra của thread và fiber
1.5.1 Truy cập đồng thời và cấu trúc dữ liệu
Những thread trong cùng một tiến trình chia sẻ cùng không gian địa chỉ Điều này cho phép đồng thời chạy các đoạn mã thành từng cặp để trao đổi dữ liệu một cách thuận tiện và chặt chẽ mà không cần chi phí hoạt động hoặc sự phức tạo của giao tiếp liên tiến trình (inter-process communication - IPC) Tuy nhiên, khi chia sẽ giữa các thread, kể cả những cấu trúc dữ liệu đơn giản cũng trở nên rủi ro nếu chúng yêu cầu nhiều hơn một cấu trúc CPU để cập nhật: hai thread có thể cố gắng cập nhật
Trang 11cấu trúc dữ liệu cùng lúc và tìm thấy những sự thay đổi không mong muốn Lỗi xảy
ra bởi những rủi ro có thể rất khó để tái sản xuất và cô lập
Để ngăn ngừa điều này, những API threading cung cấp những API để đồng bộ hóa như mutexes để khóa cấu trúc dữ liệu chống lại quyền truy cập đồng thời Trên
hệ thống một bộ vi xử lý, một thread đang chạy trong một mutex đã bị khóa phải tạm dừng và gây ra một ngữ cảnh chuyển Trên hệ thống đa xử lý, thread có thể thay bởi việc dò ý kiến các mutex trong một spinlock
1.5.2 Vào/ ra và bộ lập lịch
Sự thể hiện của thread người dùng hoặc fiber thường hoàn toàn bằng userspace Như là một kết quả, ngữ cảnh chuyển giữa thread người dùng và fiber trong cùng một tiến trình cực kỳ hiệu quả bởi vì nó không yêu cầu bất kỳ sự tương tác nào với nhân trong tất cả : một ngữ cảnh chuyển có thể được thực hiện tại “địa phương” do đó tiết kiệm các CPU đăng ký sử dụng bởi thread hoặc fiber thực thi người dùng và sau đó tải những đăng ký bắt buộc bởi thread người dùng hoặc fiber được thực thi Kể từ khi sự lập lịch xảy ra ở không gian người dùng, chính sách lập lịch sẽ dễ dàng phù hợp với yêu cầu của khối lượng công việc của chương trình hơn Tuy nhiên, cách sử dụng các khối lời gọi hệ thống trong thread người dùng hoặc fiber có thể khó giải quyết Nếu một thread người dùng hoặc một fiber thực hiện một lời gọi hệ thống, những thread người dùng hoặc fiber khác trong tiến trình
sẽ không thể chạy cho tới khi có lời gọi trả về của hệ thống Một ví dụ điển hình của vấn đề này khi thực hiện đầu vào/ đầu ra : hầu hết các chương trình được viết để thực hiện việc đồng bộ đầu vào/đầu ra Khi một toán tử đầu vào/đầu ra được khởi tạo, một lời gọi hệ thống sẽ được thực hiện và không trả về cho tới khi toán tử đầu vào/đầu ra được hoàn thành Trong giai đoạn can thiệp, toàn bộ tiến trình sẽ bị chặn bởi nhân và không thể chạy mà bỏ mặc các thread và fiber khác trong cùng tiến trình
Một giải pháp phổ biến cho vấn đề này là cung cấp một API I/O để thể hiện giao diện đồng bộ bằng cách sử dụng đầu vào/ đầu ra không bị chặn bên trong, và lập lịch cho những thread người dùng hoặc fiber khác trong khi toán tử đầu vào/ đầu
ra đang được xử lý Những giải pháp tương tự có thể được cung cấp cho những lời
Trang 12và lỗi truy cập đồng thời
1.6 Các mô hình
1.6.1 Mô hình 1:1 (thread cấp nhân)
Thread được tạo ra bởi người dùng tương ứng 1:1 với các thực thể có thể lập lịch trong nhân Đây là việc thực hiện đơn giản nhất có thể trong thread Trên Linux, thư viện ngôn ngữ C thông thường thực hiện phương pháp này (bằng các thư viện POSIX Pthread NPTL hoặc các LinuxThread cũ hơn) Phương pháp tương tự cũng được sử dụng bởi Solaris, NetBSD và FreeBSD
1.6.2 Mô hình N:1 (thread cấp người dùng)
Một mô hình N:1 ngụ rằng tất cả các thread cấp ứng dụng đều vạch ra một đơn thực thể được lập lịch cấp nhân Nhân không có tri thức về thread ứng dụng Với phương pháp này, ngữ cảnh chuyển có thể được hoàn thành rất nhanh và ngoài ra,
nó có thể được thực hiện ngay cả trên những nhân đơn giản mà không hỗ trợ thread Một trong những nhược điểm hiển nhiên là nó không thể tăng tốc phần cần trên bộ
xử lý multi thread hay nhiều bộ xử lý máy tính: không bao giờ có nhiều hơn một thread được lập lịch cùng thời điểm Nó được sử dụng bởi GNU Portable Threads
1.6.3 Mô hình N:M (thread tích hợp)
Mô hình N:M chỉ ra với N thread ứng dụng tương ứng với M thực thể nhân Đây là kết hợp giữa cấp nhân(1:1) và cấp người dùng(N:1) Nói chung, hệ thống thread N:M thể hiện phức tạp hơn thread nhân hoặc thread người dùng, bởi vì đòi hỏi mã của cả thread nhân và thread người dùng Trong thể hiện N:M, thư viện
Trang 13Trong chương tiếp theo sẽ đi sâu vào lập trình thread trong hệ thống Unix với POSIX Thread
Trang 14CHƯƠNG 2: POSIX THREAD PROGRAMMING
2.1 Tổng quan về Pthread
2.1.1 Khái niệm Pthread
Trong lịch sử, các nhà phát triển phần cứng đã triển khai thực hiện việc sử độc quyển sở hữu về các phiên bản của thread Các thực hiện này khác biệt đáng kể so với các thực hiện khác làm cho các nhà lập trình khó khăn trong việc phát triển các ứng dụng thread di động Để tận dụng đầy đủ các khả năng được cung cấp bởi thread, một giao diện lập trình chuẩn hóa đã được yêu cầu:
Với hệ thống UNIX, giao diện này được xác định bởi chuẩn IEEE
2.1.2 Tại sao lại sử dụng Pthread?
Động lực chính đế sử dụng Pthread là để nhận ra hiệu quả tiềm năng của một chương trình Khi so sánh với việc tạo ra và quản lý một process, một thread được tạo ra với ít chi phí hoạt động của hệ điều hành hơn Quản lý thread cũng yêu cầu ít tài nguyên hệ thống hơn là quản lý process Ví dụ: bảng sau so sánh kết quả thời gian giữa thử tục fork() và thủ tục pthreads_create() Thời gian được tính toán với
việc tạo ra 50000 process/thread, được thực hiện với tiện ích time, tính theo đơn vị
là giây và không có đánh dấu tối ưu hóa
Trang 15Tất cả các thread trong một process đều chia sẻ cùng một không gian địa chỉ Những giao tiếp inter-thread hiệu quả hơn và trong nhiều trường hợp, dễ sử dụng hơn giao tiếp inter-process Những ứng dụng threaded cung cấp hiệu suất tiềm năng
và lợi thế hơn các ứng dụng không sử dụng thread trong một số trường hợp như :
- Sự chồng CPU làm việc với đầu vào/đầu ra : ví dụ, một chương chình có thể có nhiều phần mà chúng thực hiện một tính toán đầu vào/ đầu ra dài Trong khi một thread đợi cho một hệ thống đầu vào/ đầu ra được gọi để hoàn thành, CPU làm việc chuyên sâu có thể được thực hiện bởi thread khác
- Lập lịch ưu tiên/thời gian thực: những nhiệm vụ mà quan trọng hơn có thể được lập lịch để thay thế hoặc ngắt các nhiệm vụ có độ ưu tiên ít hơn
- Quản lý sự kiện không đồng bộ: những nhiệm vụ mà phục vụ các sự kiện với tần số
vô định hoặc kéo dài có thể được xen kẽ Ví dụ, một web server có thể đồng thời chuyển dữ liệu từ các yêu cầu trước và quản lý sự xuất hiện của các yêu cầu mới Động cơ chính để thúc đẩy việc cân nhắc sử dụng Pthread trên một kiến trúc SMP ( symmetric multyprocessing ) để đạt được hiệu suất tối ưu
Trang 162.1.3 Pthread API
Những Ptrhead API thông thường được định nghĩa trong chuẩn ANSI/IEEE POSIX 1003.1 – 1995 Chuẩn POSIX tiếp tục được phát triển và trải qua các phiên bản, bao gồm các đặc điểm kỹ thuật Pthreads Phiên bản mới nhất được gọi là IEEE Std 1003.1, 2004 Các thủ tục con trong đó bao gồm các API Pthreads có thể được chính thức nhóm lại thành bốn nhóm chính:
Quản lý thread: các thủ tục làm việc trực tiếp với thread: tạo thread, tách, nối
… Chúng cũng bao gồm các hàm để thiết lập hoặc truy vấn thuộc tính thread (như nối, lập lịch…)
Mutexes: các thủ tục xử lý việc đồng bộ hóa, được gọi là một “mutex”, viết tắt của “mutual exclusion” Các hàm mutex cung cấp các hàm tạo, phá hủy, khóa
và mở khóa mutexes Các hàm này được bổ sung thêm các hàm thuộc tính mutex để thiết lập hoặc sửa đổi các thuộc tính liên kết với mutexes
Biến điều kiện: các thủ tục để đánh địa chỉ liên kết giữa các thread để chia sẻ một mutex Tùy thuộc và các lập trình viên và những điều kiện cụ thể Nhóm này bao gồm các hàm để khởi tạo, phá hủy, đợi và signal dựa vào giá trị cụ thể của biến Các hàm để thiết lập/truy vấn thuộc tính biến điều kiện cũng được bao gồm
Đồng bộ hóa: các thủ tục quả lý đọc/ghi các khóa và các rào cản
Quy ước đặt tên: tất cả các định danh trong thư viện đều được bắt đầu với pthread_ Một số ví dụ:
Trang 17Khái niệm về các đối tượng không rõ ràng trong thiết kế của API Việc gọi cơ bản để tạo ra hoặc sửa đổi các đối tượng không rõ ràng - các đối tượng không rõ ràng có thể được sửa đổi bởi lệnh gọi các hàm thuộc tính để xử lý các thuộc tính không rõ ràng Pthread API chứa khoảng 100 thủ tục con Hướng dẫn này sẽ tập trung vào một tập hợp con của các thủ tục này, cụ thể, là những thủ tục mà có nhiều khả năng có thể có hữu ích ngay lập tức với những người mới lập trình về Phtread Đối với tính di động, file pthread.h được include trong mỗi file mã nguồn sử dụng thư viện Pthreads Chuẩn POSIX hiện hành chỉ được định nghĩa trong ngôn ngữ C
2.1.4 Biên dịch chương trình Threaded
Một vài ví dụ về lệnh biên dịch sử dụng cho code Pthread được liệt kê ở bảng sau:
Trang 18 Thread: một định danh duy nhất cho một thread mới và được trả về bởi thủ tục con
Trang 19 Attr: một đối tượng thuộc tính có thể được sử dụng để thiết lập các thuộc tính cho thread Bạn có thể xác định một đối tượng thuộc tính thread, hoặc để NULL với giá trị mặc định
Start_routine: thủ tục C để thread sẽ thực thi một lần khi nó được tạo ra
Arg: một tham số đơn để có thể được truyền cho start_routine Nó phải được truyền bởi tham chiếu như là con trỏ kiểu void
Mỗi một lần được tao ra, thread có thể tạo ra các thread khác Không có hệ thống cập bậc hoặc phụ thuộc giữa các thread
2.2.3 Thiết lập các thuộc tính cho Thread
Mặc định, một thread được tạo ra với một thuộc tính cố định Một vài trong số những thuộc tính có thể được thay đổi bởi người lập trình thông qua đối tượng thuộc tính thread Pthread_attr_init và pthread_attr_destroy được sử dụng để khởi tạo hoặc phá hủy đối tượng thuộc tính thread Những thủ tục khác được sử dụng sau đó để truy vấn hoặc thiết lập những thuộc tính xác định trong đối tượng thuộc tính thread
2.2.4 Hủy thread
Có một vài cách mà trong đó Pthread bị hủy:
Những thread mà trả về từ thủ tục bắt đầu của nó ( thủ tục chính với thread ban đầu)
Trang 20 Những thread thực hiện lời gọi tới hàm pthread_exit
Những thread bị hủy từ những thủ tục khác bằng hàm pthread_cancel
Tiến trình cuối bị ngắt trong khi thực hiện lời gọi tới hàm khác hoặc thoát khỏi thủ tục con
Hàm pthread_exit được sử dụng để thoát khỏi một thread Thông thường, thủ tục pthread_exit() được gọi sau khi 1 thread đã hoàn thành công việc của nó và không còn yêu cầu nào Nếu hàm main() kết thúc trước khi những thread được tạo ra
và thoát với hàm pthread_exit(), những thread khác vẫn tiếp tục được thực hiện Ngoài ra, chúng cũng sẽ tự động bị ngắt khi hàm main() kết thúc Người lập trình có thể tùy chọn chỉ định một trạng thái (status) ngắt, được lưu như một con trỏ kiểu void cho bất kỳ thread nào để có thể tham gia vào lời gọi thread Tóm lại: thủ tục pthead_exit() không phải để đóng file, bất kỳ file nào được mở bên trong thread, các file này sẽ vẫn mở sau khi thread bị ngắt
Ví dụ về tạo thread và ngắt thread: ví dụ này tạo ra 5 thread với thủ tục pthread_create() Mỗi thread sẽ in ra thông điệp “Hello world” và sau đó được ngắt bới lời gọi hàm pthread_exit():
Trang 21int 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);
2.2.5 Truyền tham số cho Thread
Thủ tục pthread_create() cho phép người lập trình có thể truyền một đối số cho
thủ tục khởi tạo thread Trong trường hợp có nhiều tham số cần truyền, sự giới hạn
này rất dễ dàng được vượt qua bằng cách tạo ra 1 cấu trúc struct bao gồm tất cả các
tham số, sau đó truyền 1 tham số con trỏ của struct này vào thủ tục
pthread_create() Tất cả các tham số đều phải được truyền bằng tham chiếu và kiểu
(void *) Ví dụ về cách truyền tham số:
Trang 22struct thread_data thread_data_array[NUM_THREADS];
void *PrintHello(void *threadarg)
Trang 23Nối (joining) là 1 cách để thực hiện việc đồng bộ hóa giữa các thread Ví dụ:
Thủ tục con pthread_join() khóa lời gọi thread cho tới khi threadid xác định bị ngắt Người lập trình cũng có thể nhận được trạng thái ngắt trả về của thread mục tiêu nếu nó được xác định từ lời gọi pthread_exit() Một thread đang được nối có thể gặp lời gọi pthead_join() Nó là lỗi logic khi cố thử nhiều phép nối trên cùng một thread Hai phương thức đồng bộ hóa khác là mutexes và biến điều kiện sẽ được thảo luận ở phần sau
2.2.6.3 Có thể nối được hay không?
Khi một thread được tạo ra, một trong những thuộc tính của nó định nghĩa khi nào thì có thể nối được hoặc tách được Chỉ thread mà được tạo ra với thuộc tính là
có nối được thì mới có thể được nối Nếu một thread được tạo ra với thuộc tính là tách, nó không bao giờ được nối Bản nháp cuối cùng của chuẩn POSIX xác định rằng các thread nên được tạo ra với thuộc tính có thể nối được, để rõ ràng khi tạo ra một thread có thể nối hoặc tách, tham số attr trong thủ tục pthread_create() được sử dụng Quá trình có 4 bước là:
Trang 24 Khai báo một biến thuộc tính của pthread với kiểu dữ liệu là pthread_attr_t
Khởi tạo biến thuộc tính với hàm pthread_attr_init()
Thiết lập thuộc tính tách được với hàm pthread_attr_setdetachstate()
Khi đã hoàn thành, giải phóng tài nguyên được sử dụng bởi các thuộc tính với hàm pthread_attr_destroy()
Trang 25printf("Main: creating thread %ld\n", t);
rc = pthread_create(&thread[t], &attr, BusyWork, (void *)t);
Trang 262.2.7.2 Ngăn ngừa những vấn đề với stack
Chuẩn POSIX không quy định kích thước stack của một thread Điều này phụ thuộc và thực hiện khác nhau Vượt giới hạn mặc định của stack rất dễ làm, với một kết quả bình thường: chương trình chấm dứt hoặc dữ liệu bị hỏng Một chương trình
an toàn và di động không phụ thuộc vào giới hạn mặc định của stack, nhưng thay vào đó, rõ ràng stack bố trí đủ cho mỗi thread bằng cách sử dụng thủ tục
pthread_attr_setstackaddr có thể được sử dụng bởi những ứng dụng trong một môi trường mà ở đó stack cho mỗi thread phải đặt ở một vài vùng cụ thể của bộ nhớ
2.3 Biến Mutex
2.3.1 Khái niệm mutex
Mutex là viết tắt của “mutual exclusion” Biến mutex là một trong những phương tiện chính để thực hiện việc đồng bộ thread và cho việc bảo vệ việc chia sẻ
dữ liệu khi xảy ra nhiều lời viết Một biến mutex hoạt động như một “khóa” bảo vệ quyền truy cập vào một nguồn dữ liệu được chia sẻ Khái niệm cơ bản của một mutex được sử dụng trong Pthread là chỉ có một thread có thể khóa (hoặc sở hữu) một biến mutex tại bất kỳ thời điểm nào Vì vậy, ngay cả khi nếu một vài thread cố khóa một mutex thì chỉ một thread sẽ thành công Không có thread khác có thể sở hữu mutex đó cho tới khi thread sở hữu mở khóa mutex đó Những thread phải thay phiên nhau tuy cập dữ liệu được bảo vệ Mutex có thể được sử dụng để ngăn chặn
dịch ngân hàng được chỉ ra dưới đây:
Trang 27Trong ví dụ trên, một mutex nên được sử dụng để khóa “balance” trong khi một thread đang sử dụng nguồn dữ liệu được chia sẻ này Rất thường xuyên các hành động được thực hiện bởi một thread sở hữu một mutex là cập nhật các biến toàn cục Đây là một cách an toàn để đảm bảo rằng khi một vài thread cập nhật cùng biến này, giá trị cuối cùng là giống như những gì nó sẽ được nếu chỉ có một thread thực hiện cập nhật Các biến đang được cập nhật thuộc về “critical section”
Một trình tự thông thường trong việc sử dụng một mutex là như sau:
[1] Tạo ra và khởi tạo một biến mutex
[2] Một vài thread thử khóa mutex
[3] Chỉ một thread thành công và thread đó sỡ hữu mutex
[4] Thread sở hữu thực hiện một vài tập các hành động
[5] Thread sở hữu mở khóa mutex
[6] Một thread khác giành được mutex và lặp lại quá trình trên
[7] Cuối cùng mutex bị phá hủy
Khi bảo vệ dữ liệu được chia sẻ, đó là trách nhiệm của người lập trình để đảm bảo mọi thread có nhu cầu sử dụng một mutex Ví dụ, nếu bốn thread đang được cập nhật cùng dữ liệu, nhưng chỉ có một sử dụng mutex, dữ liệu vẫn có thể bị hỏng
2.3.2 Tạo ra và phá hủy mutex
2.3.2.1 Những thủ tục
Pthread_mutex_init(mutex, attr)
Pthread_mutex_destroy(mutex)
Trang 28 Protocol: Chỉ định giao thức được sử dụng để ngăn chặn sự đảo lộn ưu tiên cho một mutex
Prioceiling: chỉ định trần ưu tiên của một mutex
Process-shared: chỉ định quá trình chia sẻ một mutex
Chú ý rằng không phải tất cả các thực hiện có thể cung cấp ba thuộc tính mutex tùy chọn Các thủ tục pthead_mutexattr_init() và pthread_mutexattr_destroy() được dùng để tạo ra và phá hủy đối tượng thuộc tính mutex tương ứng Pthead_mutexattr_destroy() nên được dùng để giải phóng đối tượng mutex khi nó không còn cần thiết
2.3.3 Khóa và mở khóa mutex
Trang 29 Mutex này đã được mở khóa
Mutex được sở hữu bởi thread khác
Không có gì gọi là “ma thuật” về mutexes… trong thực tế, chúng na ná như một “thỏa thuận của quý ông (gentlement’s agreement)” giữa các thread tham gia Đây là thiết lập của người viết chương trình để đảm bảo rằng những thread cần thiết làm cho các mutex khóa và mở khóa một cách chính xác Kịch bản sau đây thể hiện một lỗi logic:
Ví dụ sau đây minh họa cho cách sử dụng một biến mutex trong một chương trình thread Dữ liệu chính luôn sẵn sàng cho tất cả các thread thông qua một biến struct toàn cục Mỗi thread làm việc trên một phần dữ liệu khác nhau Thread chính đợi cho đến khi tất cả các thread khác hoàn thành xong việc tính toán của chúng, và sau đó in kết quả tổng:
Trang 30to allow the function "dotprod" to access its input data and
place its output into the structure
The function dotprod is activated when the thread is created
All input to this routine is obtained from a structure
of type DOTDATA and all output from this function is written into
this structure The benefit of this approach is apparent for the
multi-threaded program: when a thread is created we pass a single
argument to the activated function - typically this argument
is a thread number All the other information required by the
function is accessed from the globally accessible structure