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
32 trang |
Chia sẻ: candy98 | Lượt xem: 646 | Lượt tải: 0
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.