Bài giảng Đồng bộ hóa với Semaphore - TDA383/DIT390 Carlo A. Furia

Tìm hiểu chi tiết về đồng bộ hóa tiến trình và cơ chế semaphore trong hệ điều hành. Bài giảng cung cấp kiến thức nền tảng và ví dụ thực tế.

Trường đại học

Chalmers University of Technology – University of Gothenburg

Chuyên ngành

Lập trình đồng thời

Người đăng

Ẩn danh

Thể loại

Bài giảng

2016

70
0
0

Phí lưu trữ

30 Point

Tóm tắt

I. Tổng quan về Semaphore trong đồng bộ hóa

Semaphore là cơ chế đồng bộ hóa mạnh mẽ trong lập trình đồng thời, sử dụng bộ đếm để điều khiển truy cập vào tài nguyên chia sẻ. Khác với mutex chỉ cho phép một luồng truy cập tại thời điểm, semaphore cho phép tối đa N luồng cùng truy cập. Semaphore bao gồm hai hoạt động chính: down (hoặc wait) giảm bộ đếm và chặn luồng nếu bộ đếm bằng 0, up (hoặc signal) tăng bộ đếm và đánh thức luồng chờ. Cơ chế này giải quyết hiệu quả vấn đề truy cập đồng thời vào bộ đệm giới hạn, điều phối giữa nhà sản xuất và người tiêu dùng. Semaphore thường kết hợp với khóa (lock) để đảm bảo truy cập an toàn vào vùng nhớ chia sẻ.

1.1. Cấu trúc cơ bản của Semaphore

Semaphore bao gồm ba thành phần chính: bộ đếm (count), danh sách chờ (waiting list) và hai thao tác down/wait, up/signal. Bộ đếm xác định số lượng luồng tối đa được phép truy cập tài nguyên. Danh sách chờ lưu trữ các luồng bị chặn khi bộ đếm bằng 0. Thao tác down giảm bộ đếm; nếu kết quả âm, luồng hiện tại bị chặn và đưa vào danh sách chờ. Thao tác up tăng bộ đếm; nếu danh sách chờ không rỗng, luồng đầu tiên được đánh thức. Cơ chế này đảm bảo tính công bằng FIFO trong hầu hết triển khai.

1.2. Phân biệt Semaphore và Mutex

Mutex chỉ cho phép một luồng truy cập tài nguyên tại thời điểm, trong khi semaphore cho phép nhiều luồng (tùy thuộc bộ đếm). Mutex luôn liên kết với chủ sở hữu luồng, yêu cầu giải phóng bởi cùng luồng. Semaphore không có khái niệm chủ sở hữu, bất kỳ luồng nào cũng có thể giải phóng. Mutex thích hợp cho khóa đơn giản, semaphore phù hợp cho điều phối truy cập nhóm tài nguyên. Cả hai đều ngăn chặn race condition nhưng lựa chọn phụ thuộc vào yêu cầu cụ thể của bài toán.

II. Phân tích vấn đề đồng bộ hóa với Semaphore

Semaphore giải quyết ba vấn đề đồng bộ hóa chính: truy cập tài nguyên chia sẻ, điều phối giữa nhà sản xuất và người tiêu dùng, và đồng bộ hóa rào cản. Trong bài toán nhà sản xuất-người tiêu dùng, semaphore kiểm soát số lượng mục trong bộ đệm: nItems theo dõi số mục sẵn sàng, nFree quản lý slot trống. Việc sử dụng semaphore ngăn chặn tình trạng tràn bộ đệm hoặc truy cập vào bộ đệm rỗng. Tuy nhiên, triển khai sai lầm dẫn đến deadlock khi down/up không đúng thứ tự hoặc quên giải phóng semaphore.

2.1. Deadlock trong triển khai Semaphore

Deadlock xảy ra khi hai luồng chờ nhau giải phóng semaphore, tạo vòng lặp vô tận. Ví dụ, luồng A giữ semaphore A và chờ semaphore B, trong khi luồng B giữ semaphore B và chờ semaphore A. Triển khai semaphore không đúng thứ tự down/up cũng gây deadlock. Giải pháp bao gồm sử dụng khóa (lock) để bảo vệ vùng truy cập, đảm bảo thứ tự giải phóng semaphore, và triển khai timeouts cho các thao tác chờ.

2.2. Starvation và hiệu suất

Starvation xảy ra khi một số luồng không bao giờ được cấp quyền truy cập do ưu tiên không công bằng. Trong triển khai semaphore, luồng mới có thể liên tục được ưu tiên so với luồng chờ lâu. Starvation ảnh hưởng đến hiệu suất hệ thống bằng cách giảm thông lượng tổng thể. Giải pháp bao gồm sử dụng cơ chế FIFO cho danh sách chờ, giới hạn thời gian chờ tối đa, hoặc điều chỉnh bộ đếm semaphore để cân bằng tải giữa các luồng.

III. Giải pháp đồng bộ hóa với Semaphore nâng cao

Các giải pháp nâng cao bao gồm semaphore đếm (counting semaphore) cho truy cập nhóm tài nguyên, semaphore nhị phân (binary semaphore) tương đương mutex, và kết hợp semaphore với khóa (lock) để bảo vệ vùng nhớ chia sẻ. Trong bài toán rào cản tái sử dụng, semaphore quản lý số luồng đã hoàn thành giai đoạn. Giải pháp phải đảm bảo tính đúng đắn: deadlock-free, starvation-free, và hiệu quả. Triển khai phải tuân thủ nguyên tắc bất biến (invariants) để xác nhận hành vi đúng của hệ thống.

3.1. Triển khai bộ đệm chia sẻ giới hạn

Bộ đệm chia sẻ giới hạn sử dụng hai semaphore: nItems theo dõi số mục sẵn sàng, nFree quản lý slot trống. Nhà sản xuất gọi put(): chờ nFree > 0, thêm mục, tăng nItems. Người tiêu dùng gọi get(): chờ nItems > 0, lấy mục, tăng nFree. Khóa (lock) bảo vệ truy cập vào bộ đệm. Giải pháp ngăn chặn race condition và đảm bảo tính nhất quán dữ liệu. Triển khai phải xử lý ngoại lệ và đảm bảo semaphore luôn được giải phóng.

3.2. Rào cản tái sử dụng Reusable Barriers

Rào cản tái sử dụng yêu cầu tất cả luồng chờ tại điểm rào cản trước khi tiếp tục. Sử dụng semaphore open (bộ đếm 0) và biến nDone đếm số luồng đã đến. Khi luồng cuối cùng đến, tăng open (đánh thức tất cả luồng). Sau khi tất cả luồng vượt rào cản, đặt lại open về 0. Giải pháp phải ngăn chặn tình trạng mở rào cản nhiều lần gây lỗi. Cơ chế đồng bộ hóa phải đảm bảo tính đúng đắn ngay cả khi luồng rời khỏi rào cản sớm.

IV. Kết luận và ứng dụng thực tế của Semaphore

Semaphore là công cụ đồng bộ hóa linh hoạt giải quyết nhiều vấn đề trong lập trình đồng thời. Khác với mutex, semaphore cho phép điều phối truy cập nhóm tài nguyên, phù hợp cho các hệ thống sản xuất-tiêu dùng, rào cản, và điều phối luồng. Triển khai phải tuân thủ nguyên tắc bất biến và xử lý các tình huống cạnh tranh như deadlock, starvation. Semaphore kết hợp khóa tạo ra giải pháp mạnh mẽ cho truy cập an toàn vào vùng nhớ chia sẻ. Hiểu rõ cơ chế down/up và thứ tự thao tác là chìa khóa thành công.

4.1. Lựa chọn giữa Semaphore và Mutex

Lựa chọn phụ thuộc vào yêu cầu bài toán: mutex phù hợp cho khóa đơn giản (1 luồng truy cập), semaphore cho điều phối nhóm luồng (N luồng truy cập). Mutex có chi phí thấp hơn do không cần quản lý danh sách chờ. Semaphore cung cấp linh hoạt cao hơn nhưng yêu cầu quản lý phức tạp hơn. Trong hệ thống đa luồng, semaphore giúp ngăn chặn race condition trong truy cập bộ đệm giới hạn. Cả hai công cụ đều không thể thay thế nhau hoàn toàn.

4.2. Thực hành triển khai an toàn

Triển khai an toàn yêu cầu tuân thủ thứ tự down/up, sử dụng khóa để bảo vệ vùng truy cập, và xử lý ngoại lệ. Luôn khởi tạo semaphore với bộ đếm hợp lý (0 cho điều phối, N cho truy cập nhóm). Kiểm tra tính đúng đắn bằng cách xác nhận bất biến: bộ đệm không bao giờ tràn hoặc rỗng khi truy cập. Sử dụng công cụ phân tích tĩnh hoặc kiểm thử đa luồng để phát hiện lỗi đồng bộ hóa tiềm ẩn. Thực hành triển khai phải tuân thủ tiêu chuẩn an toàn lập trình.

Tóm tắt và mô tả trên trang này được tạo với sự hỗ trợ của AI. Nếu bạn thấy nội dung không chính xác hoặc có vấn đề, vui lòng Báo lỗi nội dung.

03/06/2026

Trích đoạn nội dung tài liệu

Synchronization problems with semaphores Lecture 4 of TDA383/DIT390 (Concurrent Programming) Carlo A. Furia Chalmers University of Technology – University of Gothenburg SP3 2016/2017 Today’s menu Dining philosophers Producer-consumer Barriers Readers-writers 1 / 46 A gallery of synchronization problems In today’s class, we go through several classical synchronization problems and solve them using threads and semaphores. If you want to learn about many other synchronization problems and their solutions, check out “The little book of semaphores” by A. Downey available at http://greenteapress.

We will use pseudo-code, which simplifies the details of Java syntax and libraries but which can be turned into fully functioning code by adding boilerplate. On the course website you can download fully working implementations of some of the problems. In particular, we occasionally annotate classes with invariants using the pseudo-code keyword invariant; invariant is not a valid Java keyword—that is why we highlight it in a different color—but we will use it to help make more explicit the behavior of classes. 2 / 46 Dining philosophers The dining philosophers The dining philosophers is a classic synchronization problem introduced by Dijkstra.

It illustrates the problem of deadlocks using a colorful metaphor (by Hoare). • Five philosophers are sitting around a dinner table, with a fork in between each pair of adjacent philosophers • Each philosopher alternates between thinking (non-critical section) and eating (critical section) • In order to eat, a philosopher needs to pick up the two forks that lie at the philopher’s left and right sides • Since the forks are shared, there is a synchronization problem between philosophers (threads) 3 / 46 Dining philosophers: the problem interface Table { // philosopher k picks up forks void getForks(int k); // philosopher k releases forks void putForks(int k); } Dining philosophers problem: implement Table such that: • forks are held exclusively by one philosopher at a time • each philosopher only accesses adjacent forks Properties that a good solution should have: • support an arbitrary number of philosophers • deadlock freedom • starvation freedom • reasonable efficiency: eating in parallel still possible 4 / 46 The philosophers Each philosopher continuously alternate between thinking and eating; the table must guarantee proper synchronization when eating. Table table; // table shared by all philosophers philosopherk while (true) { think(); // think table.getForks(k); // wait for forks eat(); // eat table.putForks(k); // release forks } 5 / 46 Left and right For convenience, we introduce a consistent numbering scheme for forks and philosophers, in a way that it is easy to refer to the left or right fork of each philosopher. 0 // in classes implementing Table: // fork to the left of philosopher k 1 0 public int left(int k) { 1 4 return k; } 2 4 // fork to the right of philosopher k public int right(int k) { 3 // N is the number of philosophers 2 3 return (k + 1) % N; } 6 / 46 Dining philosophers with locks and semaphores We use semaphores to implement mutual exclusion when philosophers access the forks.

In fact, we only need locks.lock(); } 3 if all philosophers hold 2 3 left fork: deadlock! 8 / 46 Dining philosophers solution 1: breaking the symmetry Having one philosopher pick up forks in a different order than the others is sufficient to break the symmetry, and thus to avoid deadlock. public class AsymmetricTable implements Table { Lock[] forks = new Lock[N]; 0 public void getForks(int k) { if (k == N) { // right before left forks[right(k)].lock(); } } 3 // putForks as in DeadTable 2 3 9 / 46 Breaking symmetry to avoid deadlock Breaking the symmetry is a general strategy to avoid deadlock when acquiring multiple shared resources: • assign a total order between the shared resources R0 < R1 < · · · < RM • a thread can try to obtain resource Ri , with i > j, only after it has successfully obtained resource Rj Recall the Coffman conditions in a previous lecture: circular wait is one of the most common conditions for a deadlock to occur. 10 / 46 Dining philosophers solution 2: bounding resources Limiting the number of philosophers active at the table to M < N ensures that there are enough resources for everyone at the table, thus avoiding deadlock. public class SeatingTable implements Table { Lock[] forks = new Lock[N]; Semaphore seats = new Semaphore(M); // # available seats public void getForks(int k) { public void putForks(int k) { // get a seat // put down left fork seats.up(); } } 11 / 46 Starvation-free philosophers The two solutions to the dining philosophers problem also guarantee freedom from starvation, under the assumption that locks/semaphores (and scheduling) are fair.

In the asymmetric solution (AsymmetricTable): • if a philosopher P waits for a fork k , P gets the fork as soon as P’s neighbor holding fork k releases it • P’s neighbor eventually releases fork k because there are no deadlocks In the bounded-resource solution (SeatingTable): • at most M philosophers are active at the table • the other N - M philosophers are waiting on seats.down() • the first of the M philosophers that finishes eating releases a seat • the philosopher P that has been waiting on seats.down proceeds • similarly to the asymmetric solution, P also eventually gets the forks 12 / 46 Producer-consumer Producer-consumer: overview Producers and consumer exchange items through a shared buffer: • producers asynchronously produce items and store them in the buffer • consumers asynchronously consume items after taking them out of the buffer producer buffer consumer 13 / 46 Producer-consumer: the problem interface Buffer<T> { // add item to buffer; block if full void put(T item); // remove item from buffer; block if empty T get(); // number of items in buffer int count(); } Producer-consumer problem: implement Buffer such that: • producers and consumers access the buffer in mutual exclusion • consumers block when the buffer is empty • producers block when the buffer is full (bounded buffer variant) 14 / 46 Producer-consumer: desired properties Producer-consumer problem: implement Buffer such that: • producers and consumer access the buffer in mutual exclusion • consumers block when the buffer is empty • producers block when the buffer is full (bounded buffer variant) Other properties that a good solution should have: • support an arbitrary number of producers and consumers • deadlock freedom • starvation freedom 15 / 46 Producers and consumers Producers and consumers continuously and asynchronously access the buffer, which must guarantee proper synchronization. Buffer<Item> buffer; producern consumerm while (true) { while (true) { // create a new item Item item = buffer.get(); Item item = produce(); // do something with ‘item’ buffer.put(item); consume(item); } } 16 / 46 Unbounded shared buffer public class UnboundedBuffer<T> implements Buffer<T> { Lock lock = new Lock(); // for exclusive access to buffer Semaphore nItems = new Semaphore(0); // number of items in buffer Collection storage = .; // any collection (list, set, .lock(); // lock // wait until nItems > 0 // store item nItems.lock(); // lock nItems.up(); // update nItems // retrieve item lock.unlock(); // release T item = storage.unlock(); // release return item; public int count() { } return nItems.lock(); // lock // store item storage.add(item); // update nItems nItems.lock(); // lock // store item signal to consumers waiting in get storage.add(item); that they can proceed // update nItems nItems.up(); Can we execute up after unlock? lock.lock(); // lock // store item signal to consumers waiting in get storage.add(item); that they can proceed // update nItems nItems.up(); Can we execute up after unlock? lock.unlock(); T item = storage.down(); // wait until nItems > 0 lock.lock(); // lock T item = storage.remove(); // retrieve item lock.unlock(); // release return item; } What happens if another thread gets the lock just after the current threads has decremented the semaphore nItems? • if the other thread is a producer, it does not matter: as soon as get resumes execution, there will be one element in storage to remove • if the other thread is a consumer, it must have synchronized with the current thread on nItems.down(); Can we execute down after lock? lock.lock(); T item = storage.down(); Can we execute down after lock? lock.lock(); T item = storage.unlock(); return item; } Executing down after lock: • if the buffer is empty when locking, there is a deadlock! 21 / 46 Bounded shared buffer public class BoundedBuffer<T> implements Buffer<T> { Lock lock = new Lock(); // for exclusive access to buffer Semaphore nItems = new Semaphore(0); // # items in buffer Semaphore nFree = new Semaphore(N); // # free slots in buffer Collection storage = .; // any collection (list, set, .count(); } public void put(T item) { public T get() { // wait until nFree > 0 // wait until nItems > 0 nFree.lock(); // lock lock.lock(); // lock // store item // retrieve item storage.add(item); T item = storage.up(); // update nItems nFree.up(); // update nFree lock.unlock(); // release lock.unlock(); // release } return item; } 22 / 46 Bounded shared buffer public class BoundedBuffer<T> implements Buffer<T> { size of buffer Lock lock = new Lock(); // for exclusive access to buffer Semaphore nItems = new Semaphore(0); // # items in buffer Semaphore nFree = new Semaphore(N); // # free slots in buffer Collection storage = .; // any collection (list, set, .count(); } public void put(T item) { public T get() { // wait until nFree > 0 // wait until nItems > 0 nFree.lock(); // lock lock.lock(); // lock // store item // retrieve item storage.add(item); T item = storage.up(); // update nItems nFree.up(); // update nFree lock.unlock(); // release lock.unlock(); // release } return item; } 22 / 46 Bounded shared buffer public class BoundedBuffer<T> implements Buffer<T> { Lock lock = new Lock(); // for exclusive access to buffer Semaphore nItems = new Semaphore(0); // # items in buffer Semaphore nFree = new Semaphore(N); // # free slots in buffer Collection storage = .; // any collection (list, set, .count(); } public void put(T item) { public T get() { // wait until nFree > 0 // wait until nItems > 0 nFree.down(); may deadlock nItems.down(); may deadlock if swapped lock.lock(); // lock if swapped lock.lock(); // lock // store item // retrieve item storage.add(item); T item = storage.up(); // update nItems nFree.up(); // update nFree lock.unlock(); // release lock.unlock(); // release } return item; OK to swap } OK to swap 22 / 46 Waiting on multiple conditions? The operations offered by semaphores do not support waiting on multiple conditions (not empty and not full in our case) using one semaphore: // wait until there is space in the buffer while (!(nItems.count() < N)) {}; // the buffer may be full again when locking! lock.lock(); // lock // store item storage.up(); // update nItems lock.unlock(); // release 23 / 46 Barriers Barriers (also called rendezvous) A barrier is a form of synchronization where there is a point (the barrier) in a program’s execution that all threads in a group have to reach before any of them is allowed to continue 24 / 46 Barriers (also called rendezvous) A barrier is a form of synchronization where there is a point (the barrier) in a program’s execution that all threads in a group have to reach before any of them is allowed to continue A solution to the barrier synchronization problem for 2 threads using binary semaphores.down(); // wait t // code after barrier // code after barrier 24 / 46 Barriers (also called rendezvous) A barrier is a form of synchronization where there is a point (the barrier) in a program’s execution that all threads in a group have to reach before any of them is allowed to continue A solution to the barrier synchronization problem for 2 threads using binary semaphores.

Nội dung được bảo vệ bản quyền — Tải xuống đầy đủ