Bài giảng Nguyên lý Hệ điều hành - Chương 4: Luồng - Phạm Quang Dũng

4.1. Tạo luồng  Mỗi luồng trong 1 tiến trình được xác định bởi 1 thread ID.  Trong C/C++, để dùng thread ID, sử dụng kiểu pthread_t  Chương trình có thể truyền tham số cho luồng mới và lấy dữ liệu từ luồng qua giá trị trả về.  Sử dụng hàm pthread_create

pdf32 trang | Chia sẻ: candy98 | Lượt xem: 646 | Lượt tải: 0download
Bạn đang xem trước 20 trang tài liệu Bài giảng Nguyên lý Hệ điều hành - Chương 4: Luồng - Phạm Quang Dũng, để xem tài liệu hoàn chỉnh bạn click vào nút DOWNLOAD ở trên
Chương 4. Luồng Phạm Quang Dũng 24.1. Tạo luồng  Mỗi luồng trong 1 tiến trình được xác định bởi 1 thread ID.  Trong C/C++, để dùng thread ID, sử dụng kiểu pthread_t  Chương trình có thể truyền tham số cho luồng mới và lấy dữ liệu từ luồng qua giá trị trả về.  Sử dụng hàm pthread_create 3Các tham số của hàm pthread_create 1. Một con trỏ tới biến kiểu pthread_t chứa ID của luồng mới. 2. Một con trỏ tới đối tượng thread attribute. Đối tượng này điều khiển chi tiết việc tiến trình tương tác với phần còn lại của chương trình. Nếu truyền thread attribute là NULL, luồng mới sẽ có các thuộc tính mặc định. 3. Một con trỏ tới hàm thread, là hàm bình thường có kiểu: void* (*) (void*) 4. Một giá trị thread argument có kiểu void*. Bạn truyền bất kể cái gì cũng đơn giản là tham số cho hàm thread khi luồng bắt đầu thực hiện. 4 Lời gọi pthread_create trở về ngay lập tức,  luồng ban đầu thực hiện tiếp các lệnh sau lời gọi  trong khi đó, luồng mới bắt đầu thực hiện hàm thread.   Linux lập lịch cả 2 luồng theo cách không đồng bộ  Chương trình của bạn phải không phụ thuộc vào thứ tự thực hiện của các luồng. 5Ví dụ  Listing 4.1 (thread-create.c): tạo 1 luồng in các ký tự ‘x’; sau khi gọi pthread_create, luồng chính in các ký tự ‘o’.  kết quả chạy chương trình?  Lý do: Linux luân phiên lập lịch 2 luồng 64.1.1. Truyền dữ liệu cho luồng  thread argument có thể được dùng để truyền dữ liệu cho các luồng.  Vì kiểu của argument là void* nên bạn không thể truyền nhiều dữ liệu trực tiếp thông qua nó.  Giải pháp?  Dùng argument để truyền 1 con trỏ tới một cấu trúc hoặc mảng dữ liệu.  Listing 4.2: thread-create2.c 7 kết quả chạy chương trình?  lý do?  vì không có gì đảm bảo cho luồng chính (main) kết thúc sau các luồng con.  Giải pháp? 84.1.2. Joining Threads  Là cơ chế đảm bảo đợi sự kết thúc của 1 luồng  Hàm pthread_join, có 2 tham số:  Thread ID  Con trỏ tới biến void* để nhận giá trị trả về của luồng kết thúc. Nếu không quan tâm đến giá trị này thì truyền cho nó kiểu NULL.  Listing 4.3: 94.1.3. Giá trị trả về của luồng  Nếu đối số thứ 2 của pthread_join khác NULL, giá trị trả về của luồng sẽ được đặt ở vị trí trỏ bởi đối số.  Giá trị đó có kiểu void*, nếu muốn kiểu khác cần ép kiểu sau khi gọi pthread_join  Listing 4.4 (primes.c): tính số nguyên tố thứ n trong một luồng mới. Luồng đó trả về số nguyên tố mong muốn trong giá trị trả về. Trong khi đó luồng chính có thể rỗi để thực hiện việc khác. 10 4.1.4. Nói thêm về Thread ID  Đôi khi ta cần xác định luồng nào đang thực hiện  Hàm pthread_self trả về thread ID của luồng mà trong đó hàm được gọi.  Vd ứng dụng: tránh lỗi một luồng gọi pthread_join để join chính nó bằng cách dùng mã sau: if(!pthread_equal(pthread_self(),other_thread) pthread_join(other_thread,NULL); 11 4.1.5. Các thuộc tính của luồng  Nhắc lại hàm pthread_create chấp nhận một đối số là con trỏ tới một đối tượng thuộc tính luồng.  Nếu truyền NULL, các thuộc tính luồng mặc định được cấu hình cho luồng mới.  Tuy nhiên, bạn có thể tạo và tùy chỉnh một đối tượng thuộc tính luồng để xác định các giá trị thuộc tính khác 12 Các bước tạo và tùy chỉnh 1. Tạo một đối tượng pthread_attr_t 2. Gọi pthread_attr_init, truyền một con trỏ cho đối tượng này  khởi tạo các thuộc tính có giá trị mặc định. 3. Thay đổi đối tượng thuộc tính theo giá trị mong muốn 4. Truyền một con trỏ tới đối tượng thuộc tính khi gọi pthread_create 5. Gọi pthread_attr_destroy để giải phóng đối tượng thuộc tính, nếu cần có thể khởi tạo lại nó với pthread_attr_init. 13  Trong hầu hết các tác vụ lập trình ứng dụng Linux, ta chỉ quan tâm đến 1 thuộc tính là detach state.  Một luồng có thể được tạo là joinable thread (mặc định) hoặc là detached thread.  joinable thread: không được dọn tự động bởi GNU/Linux khi nó chấm dứt. Trạng thái thoát của nó sẽ treo đâu đó trong hệ thống đến khi một luồng khác gọi pthread_join để lấy giá trị trả về của nó.  detached thread: được dọn tự động khi nó chấm dứt. Luồng khác không thể đợi sự chấm dứt của nó bằng pthread_join hoặc lấy giá trị trả về. Listing 4.5 14 4.2. Thread Cancelation  Một luồng chấm dứt khi:  Trở về từ hàm thead của nó, hoặc  Gọi pthread_exit  Một luồng có thể yêu cầu một luồng khác chấm dứt:  Gọi pthread_cancel, truyền thread ID của luồng cần hủy bỏ.  Luồng bị hủy bỏ sau đó có thể được join để giải phóng các tài nguyên, trừ các detached thread.  Giá trị trả về của luồng bị hủy bỏ là PTHREAD_CANCELED  Cần kiểm soát sự cần thiết và thời điểm hủy bỏ luồng. 15 Các trạng thái có thể của luồng liên quan đến hủy bỏ luồng  asynchronously cancelable: luồng có thể bị hủy bỏ tại bất kỳ thời điểm thực hiện nào.  synchronously cancelable: (mặc định) luồng có thể bị hủy bỏ nhưng không phải tại bất kỳ thời điểm nào. Yêu cầu hủy được xếp hàng đợi, và luồng bị hủy khi nó đến điểm xác định (cancelation point).  uncancelable: yêu cầu hủy luồng âm thầm bị bỏ qua 16 4.2.1. Luồng đồng bộ và không đồng bộ  Để thiết lập luồng là asynchronously cancelable, sử dụng pthread_setcanceltype: pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL)  T.số thứ 2 khác Null sẽ là con trỏ tới biến sẽ nhận kiểu hủy bỏ trước đó của luồng.  Cancelation point được tạo ntn? được đặt ở đâu?  Trực tiếp: gọi hàm pthread_testcancel, nên định kỳ gọi trong các tính toán dài trong một hàm thread, tại các điểm ‘an toàn’ – luồng bị hủy mà không bị mất tài nguyên hoặc không tạo các hiệu ứng xấu.  Gián tiếp: một số hàm, được liệt kê trong pthead_cancel man page. 17 4.2.2. Uncancelable Critical Sections  pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL)  pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL)  Cho phép bạn thực hiện các critical section (đoạn găng) – đoạn code hoặc phải được thực hiện toàn bộ, hoặc không chút nào.  Vd: viết chương trình ngành ngân hàng thực hiện chuyển tiền từ tài khoản này đến tài khoản khác: cộng giá trị vào bản quyết toán của tài khoản nhận và trừ giá trị tương ứng của tài khoản gửi. Phải đặt 2 hoạt động trên vào đoạn găng để tránh sai sót do luồng bị hủy giữa chừng.  Listing 4.6: critical-section.c 18 4.3. Đồng bộ hóa và Đoạn găng  Lập trình với luồng đòi hỏi phải rất khéo léo vì hầu hết các chương trình đa luồng là chương trình đồng thời.  Không có cách nào biết hệ thống sẽ lập lịch luồng nào để chạy. Một luồng có thể chạy rất lâu, hoặc hệ thống sẽ chuyển giữa các luồng rất nhanh.  Trên các hệ thống đa bộ xử lý, thậm chí hệ thống có thể lập lịch nhiều luồng chạy đúng nghĩa đồng thời.  Gỡ lỗi chương trình đa luồng rất khó vì không thể khiến hệ thống lập lịch các luồng theo cách ổn định. 19 4.3.1. Race Conditions  Là trạng thái các luồng tranh nhau thay đổi cùng một cấu trúc dữ liệu.  Giả sử chương trình có một job queue, được biểu diễn bởi một danh sách liên kết.  Sau khi mỗi luồng kết thúc thực hiện, nó kiểm tra queue. Nếu queue không rỗng, luồng loại bỏ job đầu danh sách, thiết lập job_queue cho job kế tiếp.  Listing 4.10: job-queue1.c 20  Giả sử 2 luồng kết thúc một job gần như cùng thời điểm, nhưng chỉ còn lại 1 job trong queue.  Luồng 1 kiểm tra job_queue có rỗng không; thấy còn và chứa con trỏ tới next_job.  Đúng lúc này, Linux ngắt luồng 1 và lập lịch luồng 2.  Luồng 2 cũng kiểm tra job_queue và cũng gán con trỏ tới next_job.   2 luồng thực hiện cùng một job. 21  Để loại bỏ các trạng thái tranh đua, bạn cần một cách làm cho các hoạt động là nguyên tử (atomic).  Một hoạt động nguyên tử là hoạt động không thể chia nhỏ và không thể bị ngắt.  Chỉ cho phép 1 luồng được truy nhập job queue tại một thời điểm. 22 4.3.2. Mutexes  MUTual EXclusion locks – sự hỗ trợ của HĐH để thực thi việc loại bỏ trạng thái đua tranh.  Là một khóa đặc biệt cho phép chỉ một luồng khóa tại một thời điểm, khi đó các luồng khác bị chặn lại (blocked).  Để khởi tạo 1 mutex với thuộc tính mặc định: pthread_mutex_t mutex; pthread_mutex_init(&mutex, NULL);  Hoặc: pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; 23  Luồng cố gắng khóa (giành lấy) mutex bằng cách gọi pthread_mutex_lock:  nếu mutex không bị khóa thì luồng đó giành được  trái lại, luồng đó bị chặn sự thực hiện (blocked)  Có thể có nhiều luồng bị chặn, khi mutex được mở khóa, chỉ 1 luồng được mở chặn và giành được mutex, các luồng khác vẫn bị chặn.  Mở khóa mutex: pthread_mutex_unlock  Listing 4.11: job-queue2.c 24 Hàm thêm job vào queue void enqueue_job(struct job* new_job) { pthread_mutex_lock(&job_queue_mutex); new_job->next = job_queue; job_queue = new_job; pthread_mutex_unlock(&job_queue_mutex); } 25 4.3.3. Nonblocking Mutex Tests  Dùng pthread_mutex_trylock thay vì pthread_mutex_lock  Nếu không giành được mutex thì luồng gọi không bị chặn mà nó lập tức trở về với mã lỗi EBUSY. 26 4.3.4. Semaphores for Threads  Khi luồng thực hiện quá nhanh, job queue sẽ bị rỗng, luồng sẽ thoát.  Sau đó, nếu có thêm job vào queue, sẽ không còn luồng nào để xử lý nó.  Cần một cơ chế chặn các luồng khi queue rỗng cho đến khi có các job mới.  Giải pháp? semaphore (cờ báo)  Mỗi semaphore có 1 giá trị đếm nguyên, không âm.  Một semaphore hỗ trợ 2 hoạt động cơ bản 27 2 hoạt động cơ bản của semaphore  wait: giảm giá trị của semaphore đi 1. Nếu giá trị đã = 0, hoạt động chặn lại đến khi giá trị trở thành dương (do hành động của luồng khác). Khi giá trị trở thành dương, nó bị giảm 1 và hoạt động wait trở về.  post: tăng giá trị của semaphore lên 1. Nếu semaphore trước đó = 0 và các luồng khác bị chặn trong hoạt động wait trên semaphore đó thì một trong số các luồng được mở chặn và hoạt động wait của nó hoàn thành (khiến giá trị =0). 28 Thư viện, kiểu, hàm với semaphore   sem_t  sem_init  sem_destroy  sem_wait, sem_trywait  sem_post  sem_getvalue  Listing 4.12: job-queue3.c 29 4.3.5. Biến trạng thái  Biến trạng thái là cơ chế thứ ba giúp đồng bộ hóa các tiến trình thực hiện dưới điều kiện phức tạp hơn.  Giả sử bạn đang viết một hàm thread thực hiện lặp vô hạn, mỗi bước lặp thực hiện việc gì đó. Tuy nhiên, việc lặp cần được điều khiển bởi một cờ  khi cờ được thiết lập, việc lặp tiếp tục  khi cờ không thiết lập, việc lặp tạm ngừng  Listing 4.13 (spin-condvar.c): thực thi biến trạng thái đơn giản. 30 4.4. GNU/Linux Thread Implemetation  Sự thực thi các luồng POSIX trên GNU/Linux khác với trên nhiều HĐH dạng UNIX khác:  các luồng được thực thi như các tiến trình  khi gọi pthread_create để tạo luồng mới, Linux tạo một tiến trình mới để chạy luồng đó.  tuy nhiên, tiến trình này không giống như tiến trình tạo bởi fork: thay vì nhận bản copy, nó chia sẻ cùng không gian địa chỉ và các tài nguyên.  Listing 4.15 (thread-pid.c): chương trình tạo 1 luồng, cả luồng ban đầu và luồng mới gọi hàm getpid, in process ID tương ứng và quay vòng vô hạn. 31 4.5. Processes Vs. Threads  Tất cả các luồng trong một chương trình phải chạy cùng excutable. Một tiến trình con có thể chạy một excutable khác bằng cách gọi hàm exec.  Một luồng sai sót có thể gây hại cho các luồng khác trong tiến trình vì chúng chia sẻ cùng không gian bộ nhớ ảo và các tài nguyên khác. Một tiến trình sai sót không thể vì mỗi tiến trình có bản copy không gian bộ nhớ của chương trình.  Bộ nhớ copy cho tiến trình mới dễ gây quá tải hệ thống hơn khi tạo luồng mới. Tuy nhiên, việc copy chỉ xảy ra khi bộ nhớ bị thay đổi. 32 Processes Vs. Threads (tiếp)  Các luồng nên được dùng khi chương trình cần độ song song cao. Các tiến trình nên được dùng khi chương trình cần độ song song ít hơn.  Việc chia sẻ dữ liệu giữa các luồng là ít quan trọng vì chúng chia sẻ cùng bộ nhớ (tuy nhiên cần rất cẩn thận để tránh trạng thái tranh đua). Việc chia sẻ dữ liệu giữa các tiến trình cần có cơ chế giao tiếp liên tiến trình (IPC, chương 5), có thể nặng nề hơn nhưng giảm sự ảnh hưởng bởi các lỗi do đồng thời.