Semaphore là một khái niệm lập trình thường được sử dụng để giải quyết các vấn đề đa luồng. Câu hỏi của tôi với cộng đồng:
Một semaphore là gì và làm thế nào để bạn sử dụng nó?
Semaphore là một khái niệm lập trình thường được sử dụng để giải quyết các vấn đề đa luồng. Câu hỏi của tôi với cộng đồng:
Một semaphore là gì và làm thế nào để bạn sử dụng nó?
Câu trả lời:
Hãy nghĩ về semaphores như bouncers tại một hộp đêm. Có một số người chuyên dụng được phép vào câu lạc bộ cùng một lúc. Nếu câu lạc bộ đầy, không ai được vào, nhưng ngay khi một người rời khỏi, một người khác có thể vào.
Đó đơn giản là một cách để giới hạn số lượng người tiêu dùng cho một tài nguyên cụ thể. Ví dụ: để giới hạn số lượng cuộc gọi đồng thời vào cơ sở dữ liệu trong một ứng dụng.
Đây là một ví dụ rất sư phạm trong C # :-)
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace TheNightclub
{
public class Program
{
public static Semaphore Bouncer { get; set; }
public static void Main(string[] args)
{
// Create the semaphore with 3 slots, where 3 are available.
Bouncer = new Semaphore(3, 3);
// Open the nightclub.
OpenNightclub();
}
public static void OpenNightclub()
{
for (int i = 1; i <= 50; i++)
{
// Let each guest enter on an own thread.
Thread thread = new Thread(new ParameterizedThreadStart(Guest));
thread.Start(i);
}
}
public static void Guest(object args)
{
// Wait to enter the nightclub (a semaphore to be released).
Console.WriteLine("Guest {0} is waiting to entering nightclub.", args);
Bouncer.WaitOne();
// Do some dancing.
Console.WriteLine("Guest {0} is doing some dancing.", args);
Thread.Sleep(500);
// Let one guest out (release one semaphore).
Console.WriteLine("Guest {0} is leaving the nightclub.", args);
Bouncer.Release(1);
}
}
}
Bài viết Mutexes và Semaphores Được làm sáng tỏ bởi Michael Barr là một giới thiệu ngắn tuyệt vời về những gì làm cho mutexes và semaphores khác nhau, và khi nào chúng không nên và không nên sử dụng. Tôi đã trích một số đoạn quan trọng ở đây.
Điểm mấu chốt là các mutexes nên được sử dụng để bảo vệ các tài nguyên được chia sẻ, trong khi semaphores nên được sử dụng để báo hiệu. Nói chung, bạn không nên sử dụng semaphores để bảo vệ các tài nguyên được chia sẻ, cũng như các mutexes để báo hiệu. Ví dụ, có một số vấn đề với sự tương tự của bouncer về việc sử dụng semaphores để bảo vệ các tài nguyên được chia sẻ - bạn có thể sử dụng chúng theo cách đó, nhưng nó có thể gây khó khăn cho việc chẩn đoán lỗi.
Trong khi mutexes và semaphores có một số điểm tương đồng trong việc thực hiện, chúng nên luôn được sử dụng khác nhau.
Câu trả lời phổ biến nhất (nhưng dù sao không chính xác) cho câu hỏi được đặt ra ở đầu là các trường hợp biến đổi và ngữ nghĩa rất giống nhau, với sự khác biệt đáng kể duy nhất là các ngữ nghĩa có thể đếm cao hơn một. Gần như tất cả các kỹ sư dường như hiểu đúng rằng mutex là một cờ nhị phân được sử dụng để bảo vệ tài nguyên được chia sẻ bằng cách đảm bảo loại trừ lẫn nhau bên trong các phần quan trọng của mã. Nhưng khi được yêu cầu mở rộng về cách sử dụng "semaphore", hầu hết các kỹ sư khác nhau chỉ khác nhau về mức độ tin cậy của họ, bày tỏ một số ý kiến của sách giáo khoa rằng chúng được sử dụng để bảo vệ một số tài nguyên tương đương.
...
Tại thời điểm này, một sự tương tự thú vị được thực hiện bằng cách sử dụng ý tưởng về chìa khóa phòng tắm như bảo vệ tài nguyên chung - phòng tắm. Nếu một cửa hàng có một phòng tắm duy nhất, thì một chìa khóa duy nhất sẽ đủ để bảo vệ tài nguyên đó và ngăn không cho nhiều người sử dụng nó cùng một lúc.
Nếu có nhiều phòng tắm, người ta có thể cố gắng khóa chúng giống nhau và tạo ra nhiều chìa khóa - điều này tương tự như một semaphore bị sử dụng sai. Khi bạn có chìa khóa, bạn thực sự không biết phòng tắm nào có sẵn, và nếu bạn đi theo con đường này, có lẽ bạn sẽ kết thúc bằng cách sử dụng mutexes để cung cấp thông tin đó và đảm bảo rằng bạn không sử dụng phòng tắm đã bị chiếm dụng .
Một semaphore là công cụ sai lầm để bảo vệ một số tài nguyên về cơ bản giống nhau, nhưng đây là cách nhiều người nghĩ về nó và sử dụng nó. Sự tương tự của bouncer rất khác biệt - không có nhiều loại tài nguyên giống nhau, thay vào đó có một tài nguyên có thể chấp nhận nhiều người dùng đồng thời. Tôi cho rằng một semaphore có thể được sử dụng trong những tình huống như vậy, nhưng hiếm khi có những tình huống trong thế giới thực mà sự tương tự thực sự nắm giữ - thường thì có một số tài nguyên cùng loại, nhưng vẫn là các tài nguyên riêng lẻ, như phòng tắm, không thể sử dụng cách này.
...
Việc sử dụng đúng một semaphore là để báo hiệu từ nhiệm vụ này sang nhiệm vụ khác. Một mutex có nghĩa là được thực hiện và phát hành, luôn theo thứ tự đó, bởi mỗi tác vụ sử dụng tài nguyên được chia sẻ mà nó bảo vệ. Ngược lại, các tác vụ sử dụng semaphores báo hiệu hoặc chờ đợi không phải cả hai. Ví dụ: Nhiệm vụ 1 có thể chứa mã để đăng (tức là tín hiệu hoặc tăng) một semaphore cụ thể khi nhấn nút "power" và Nhiệm vụ 2, đánh thức màn hình, hiển thị trên cùng một semaphore đó. Trong kịch bản này, một nhiệm vụ là nhà sản xuất tín hiệu sự kiện; người tiêu dùng khác.
...
Ở đây, một điểm quan trọng được đưa ra là các mutex can thiệp vào các hệ điều hành thời gian thực theo chiều hướng xấu, gây ra sự đảo ngược ưu tiên trong đó một nhiệm vụ ít quan trọng hơn có thể được thực thi trước một nhiệm vụ quan trọng hơn vì chia sẻ tài nguyên. Nói tóm lại, điều này xảy ra khi một nhiệm vụ ưu tiên thấp hơn sử dụng một mutex để lấy tài nguyên, A, sau đó cố gắng lấy B, nhưng bị tạm dừng vì B không có sẵn. Trong khi chờ đợi, một nhiệm vụ ưu tiên cao hơn xuất hiện và cần A, nhưng nó đã bị ràng buộc và bởi một quá trình thậm chí không chạy vì nó đang chờ B. Có nhiều cách để giải quyết vấn đề này, nhưng nó thường được sửa bằng cách thay đổi mutex và trình quản lý tác vụ. Mutex phức tạp hơn nhiều trong những trường hợp này so với semaphore nhị phân,
...
Nguyên nhân của sự nhầm lẫn hiện đại lan rộng giữa mutexes và semaphores là lịch sử, vì nó bắt nguồn từ phát minh năm 1974 của Semaphore (viết hoa "S", trong bài viết này) của Djikstra. Trước ngày đó, không có cơ chế báo hiệu và đồng bộ hóa nhiệm vụ an toàn ngắt nào được các nhà khoa học máy tính biết đến có thể mở rộng một cách hiệu quả để sử dụng cho hơn hai nhiệm vụ. Semaphore mang tính cách mạng, an toàn và có thể mở rộng của Dijkstra đã được áp dụng trong cả tín hiệu và bảo vệ phần quan trọng. Và do đó, sự nhầm lẫn bắt đầu.
Tuy nhiên, sau đó nó trở nên rõ ràng đối với các nhà phát triển hệ điều hành, sau khi xuất hiện RTOS ưu tiên dựa trên ưu tiên (ví dụ: VRTX, ca. 1980), xuất bản các bài báo học thuật thiết lập RMA và các vấn đề gây ra bởi sự đảo ngược ưu tiên và một bài báo về ưu tiên Các giao thức thừa kế vào năm 1990, 3 điều rõ ràng là các trường hợp đột biến không chỉ là các ngữ nghĩa với một bộ đếm nhị phân.
Mutex: chia sẻ tài nguyên
Semaphore: báo hiệu
Không sử dụng cái này cho cái kia mà không xem xét cẩn thận các tác dụng phụ.
Mutex: quyền truy cập thành viên độc quyền vào tài nguyên
Semaphore: n-thành viên truy cập vào một tài nguyên
Đó là, một mutex có thể được sử dụng để đồng bộ hóa truy cập vào một bộ đếm, tệp, cơ sở dữ liệu, v.v.
Một sempahore có thể làm điều tương tự nhưng hỗ trợ một số lượng người gọi đồng thời cố định. Ví dụ: tôi có thể gói các cuộc gọi cơ sở dữ liệu của mình trong một semaphore (3) để ứng dụng đa luồng của tôi sẽ truy cập cơ sở dữ liệu với tối đa 3 kết nối đồng thời. Tất cả các nỗ lực sẽ chặn cho đến khi một trong ba vị trí mở ra. Họ làm những việc như làm điều tiết ngây thơ thực sự, thực sự dễ dàng.
Hãy xem xét, một chiếc taxi có thể chứa tổng cộng 3 người ( phía sau ) +2 ( phía trước ) bao gồm cả người lái xe. Vì vậy, một semaphore
chỉ cho phép 5 người trong một chiếc xe tại một thời điểm. Và mutex
chỉ cho phép 1 người trên một chỗ ngồi duy nhất của xe.
Do đó, Mutex
là cho phép truy cập độc quyền cho một tài nguyên ( như luồng hệ điều hành ) trong khi a Semaphore
là cho phép truy cập n số tài nguyên tại một thời điểm.
@Craig:
Một semaphore là một cách để khóa một tài nguyên để nó được đảm bảo rằng trong khi một đoạn mã được thực thi, chỉ có đoạn mã này có quyền truy cập vào tài nguyên đó. Điều này giữ cho hai luồng không đồng thời tích lũy một tài nguyên, có thể gây ra vấn đề.
Điều này không bị hạn chế chỉ một chủ đề. Một semaphore có thể được cấu hình để cho phép một số luồng cố định truy cập vào một tài nguyên.
Semaphore cũng có thể được sử dụng như một ... semaphore. Ví dụ: nếu bạn có nhiều quy trình xử lý dữ liệu cho hàng đợi và chỉ có một tác vụ tiêu thụ dữ liệu từ hàng đợi. Nếu bạn không muốn nhiệm vụ tiêu thụ của mình liên tục thăm dò hàng đợi cho dữ liệu có sẵn, bạn có thể sử dụng semaphore.
Ở đây semaphore không được sử dụng như một cơ chế loại trừ, mà là một cơ chế báo hiệu. Nhiệm vụ tiêu thụ đang chờ đợi trên semaphore Nhiệm vụ sản xuất đang được đăng trên semaphore.
Bằng cách này, tác vụ tiêu thụ đang chạy khi và chỉ khi có dữ liệu được xử lý
Có hai khái niệm cần thiết để xây dựng các chương trình đồng thời - đồng bộ hóa và loại trừ lẫn nhau. Chúng ta sẽ thấy hai loại khóa này (semaphores nói chung là một loại cơ chế khóa) giúp chúng ta đạt được sự đồng bộ hóa và loại trừ lẫn nhau.
Một semaphore là một cấu trúc lập trình giúp chúng ta đạt được sự đồng thời, bằng cách thực hiện cả đồng bộ hóa và loại trừ lẫn nhau. Semaphores có hai loại, Nhị phân và Đếm.
Một semaphore có hai phần: bộ đếm và danh sách các nhiệm vụ đang chờ để truy cập vào một tài nguyên cụ thể. Một semaphore thực hiện hai thao tác: chờ (P) [điều này giống như lấy được khóa] và giải phóng (V) [tương tự như phát hành khóa] - đây là hai thao tác duy nhất mà người ta có thể thực hiện trên semaphore. Trong một semaphore nhị phân, bộ đếm logic nằm trong khoảng từ 0 đến 1. Bạn có thể nghĩ nó giống như một khóa có hai giá trị: mở / đóng. Một semaphore đếm có nhiều giá trị cho đếm.
Điều quan trọng cần hiểu là bộ đếm semaphore theo dõi số lượng nhiệm vụ không phải chặn, tức là chúng có thể đạt được tiến bộ. Nhiệm vụ chặn và tự thêm vào danh sách của semaphore khi bộ đếm bằng không. Do đó, một tác vụ sẽ được thêm vào danh sách trong thường trình P () nếu nó không thể tiến triển và "giải phóng" bằng cách sử dụng thường trình V ().
Bây giờ, khá rõ ràng để xem làm thế nào các ngữ nghĩa nhị phân có thể được sử dụng để giải quyết đồng bộ hóa và loại trừ lẫn nhau - về cơ bản chúng là các khóa.
Ví dụ. Đồng bộ hóa:
thread A{
semaphore &s; //locks/semaphores are passed by reference! think about why this is so.
A(semaphore &s): s(s){} //constructor
foo(){
...
s.P();
;// some block of code B2
...
}
//thread B{
semaphore &s;
B(semaphore &s): s(s){} //constructor
foo(){
...
...
// some block of code B1
s.V();
..
}
main(){
semaphore s(0); // we start the semaphore at 0 (closed)
A a(s);
B b(s);
}
Trong ví dụ trên, B2 chỉ có thể thực thi sau khi B1 thực hiện xong. Giả sử luồng A xuất hiện trước - đến sem.P () và chờ, vì bộ đếm là 0 (đóng). Chủ đề B xuất hiện, hoàn thành B1 và sau đó giải phóng chủ đề A - sau đó hoàn thành B2. Vì vậy, chúng tôi đạt được đồng bộ hóa.
Bây giờ chúng ta hãy xem xét loại trừ lẫn nhau với một semaphore nhị phân:
thread mutual_ex{
semaphore &s;
mutual_ex(semaphore &s): s(s){} //constructor
foo(){
...
s.P();
//critical section
s.V();
...
...
s.P();
//critical section
s.V();
...
}
main(){
semaphore s(1);
mutual_ex m1(s);
mutual_ex m2(s);
}
Loại trừ lẫn nhau cũng khá đơn giản - m1 và m2 không thể vào phần quan trọng cùng một lúc. Vì vậy, mỗi luồng đang sử dụng cùng một semaphore để cung cấp loại trừ lẫn nhau cho hai phần quan trọng của nó. Bây giờ, có thể có đồng thời lớn hơn? Phụ thuộc vào các phần quan trọng. (Hãy suy nghĩ về cách người khác có thể sử dụng semaphores để đạt được loại trừ lẫn nhau .. gợi ý gợi ý: tôi có nhất thiết chỉ cần sử dụng một semaphore không?)
Đếm semaphore: Một semaphore có nhiều hơn một giá trị. Chúng ta hãy xem điều này có nghĩa là gì - một khóa có nhiều hơn một giá trị ?? Vì vậy, mở, đóng, và ... hmm. Sử dụng khóa đa giai đoạn trong loại trừ hoặc đồng bộ hóa lẫn nhau là gì?
Chúng ta hãy dễ dàng hơn trong hai:
Đồng bộ hóa bằng cách sử dụng một semaphore đếm: Giả sử bạn có 3 tác vụ - # 1 và 2 bạn muốn thực hiện sau 3. Bạn sẽ thiết kế đồng bộ hóa như thế nào?
thread t1{
...
s.P();
//block of code B1
thread t2{
...
s.P();
//block of code B2
thread t3{
...
//block of code B3
s.V();
s.V();
}
Vì vậy, nếu semaphore của bạn bắt đầu đóng, bạn đảm bảo rằng khối t1 và t2, được thêm vào danh sách của semaphore. Sau đó, tất cả các t3 quan trọng, kết thúc việc kinh doanh của nó và giải phóng t1 và t2. Họ được giải phóng theo thứ tự nào? Phụ thuộc vào việc thực hiện danh sách của semaphore. Có thể là FIFO, có thể dựa trên một số ưu tiên cụ thể, v.v. (Lưu ý: suy nghĩ về cách bạn sắp xếp P và V của mình nếu bạn muốn t1 và t2 được thực thi theo một thứ tự cụ thể nào đó và nếu bạn không biết về việc triển khai semaphore)
(Tìm hiểu: Điều gì xảy ra nếu số lượng V lớn hơn số lượng P?)
Loại trừ lẫn nhau bằng cách sử dụng các semaphores: Tôi muốn bạn xây dựng mã giả của riêng mình cho việc này (làm cho bạn hiểu mọi thứ tốt hơn!) - nhưng khái niệm cơ bản là: một semaphore đếm = N cho phép N nhiệm vụ vào phần quan trọng một cách tự do . Điều này có nghĩa là bạn có N nhiệm vụ (hoặc chủ đề, nếu bạn muốn) vào phần quan trọng, nhưng nhiệm vụ N + 1 bị chặn (đi vào danh sách nhiệm vụ bị chặn yêu thích của chúng tôi) và chỉ được thực hiện khi ai đó V là semaphore ít nhất một lần. Vì vậy, bộ đếm semaphore, thay vì dao động trong khoảng từ 0 đến 1, giờ đây nằm trong khoảng từ 0 đến N, cho phép N tác vụ tự do ra vào, không chặn ai!
Bây giờ trời ơi, tại sao bạn lại cần một thứ ngu ngốc như vậy? Không phải là toàn bộ quan điểm loại trừ lẫn nhau để không cho phép nhiều hơn một chàng trai truy cập vào một tài nguyên ?? (Gợi ý ... Bạn không phải lúc nào cũng chỉ có một ổ đĩa trong máy tính của mình, phải không ...?)
Để suy nghĩ về : Có loại trừ lẫn nhau đạt được bằng cách có một semaphore đếm một mình? Điều gì sẽ xảy ra nếu bạn có 10 thể hiện của một tài nguyên và 10 luồng đến (thông qua semaphore đếm) và thử sử dụng ví dụ đầu tiên?
Một semaphore là một đối tượng chứa một số tự nhiên (nghĩa là một số nguyên lớn hơn hoặc bằng 0) trên đó hai thao tác sửa đổi được xác định. Một thao tác, V
thêm 1 vào tự nhiên. Các hoạt động khác P
, làm giảm số tự nhiên xuống 1. Cả hai hoạt động đều là nguyên tử (nghĩa là không có hoạt động nào khác có thể được thực hiện cùng lúc với a V
hoặc a P
).
Bởi vì số tự nhiên 0 không thể giảm, việc gọi P
một semaphore chứa 0 sẽ chặn việc thực hiện quy trình gọi (/ luồng) cho đến một lúc mà số đó không còn là 0 và P
có thể được thực hiện thành công (và nguyên tử).
Như đã đề cập trong các câu trả lời khác, semaphores có thể được sử dụng để hạn chế quyền truy cập vào một tài nguyên nhất định với số lượng quy trình tối đa (nhưng thay đổi).
Tôi đã tạo ra sự trực quan hóa để giúp hiểu ý tưởng. Semaphore kiểm soát truy cập vào một tài nguyên chung trong môi trường đa luồng.
ExecutorService executor = Executors.newFixedThreadPool(7);
Semaphore semaphore = new Semaphore(4);
Runnable longRunningTask = () -> {
boolean permit = false;
try {
permit = semaphore.tryAcquire(1, TimeUnit.SECONDS);
if (permit) {
System.out.println("Semaphore acquired");
Thread.sleep(5);
} else {
System.out.println("Could not acquire semaphore");
}
} catch (InterruptedException e) {
throw new IllegalStateException(e);
} finally {
if (permit) {
semaphore.release();
}
}
};
// execute tasks
for (int j = 0; j < 10; j++) {
executor.submit(longRunningTask);
}
executor.shutdown();
Đầu ra
Semaphore acquired
Semaphore acquired
Semaphore acquired
Semaphore acquired
Could not acquire semaphore
Could not acquire semaphore
Could not acquire semaphore
Mã mẫu từ bài viết
Một cờ phần cứng hoặc phần mềm. Trong các hệ thống đa tác vụ, một semaphore là biến có giá trị biểu thị trạng thái của tài nguyên chung. Quá trình cần tài nguyên kiểm tra semaphore để xác định trạng thái tài nguyên và sau đó quyết định cách tiến hành.
Semaphores hoạt động như bộ hạn chế chủ đề.
Ví dụ: Nếu bạn có một nhóm gồm 100 luồng và bạn muốn thực hiện một số thao tác DB. Nếu 100 luồng truy cập DB tại một thời điểm nhất định, thì có thể có sự cố khóa trong DB để chúng ta có thể sử dụng semaphore chỉ cho phép luồng bị giới hạn tại một thời điểm. Ví dụ về chỉ cho phép một luồng tại một thời điểm. Khi một luồng gọi acquire()
phương thức, nó sẽ nhận được quyền truy cập và sau khi gọi release()
phương thức, nó sẽ giải phóng acccess để luồng tiếp theo sẽ có quyền truy cập.
package practice;
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
Semaphore s = new Semaphore(1);
semaphoreTask s1 = new semaphoreTask(s);
semaphoreTask s2 = new semaphoreTask(s);
semaphoreTask s3 = new semaphoreTask(s);
semaphoreTask s4 = new semaphoreTask(s);
semaphoreTask s5 = new semaphoreTask(s);
s1.start();
s2.start();
s3.start();
s4.start();
s5.start();
}
}
class semaphoreTask extends Thread {
Semaphore s;
public semaphoreTask(Semaphore s) {
this.s = s;
}
@Override
public void run() {
try {
s.acquire();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" Going to perform some operation");
s.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Vì vậy, hãy tưởng tượng mọi người đang cố gắng đi vào phòng tắm và chỉ có một số chìa khóa nhất định cho phòng tắm. Bây giờ nếu không còn đủ chìa khóa, người đó cần chờ. Vì vậy, hãy nghĩ về semaphore như đại diện cho các bộ chìa khóa có sẵn cho phòng tắm (tài nguyên hệ thống) mà các quy trình khác nhau (người đi trong phòng tắm) có thể yêu cầu quyền truy cập.
Bây giờ hãy tưởng tượng hai quá trình cố gắng đi vào phòng tắm cùng một lúc. Đó không phải là một tình huống tốt và semaphores được sử dụng để ngăn chặn điều này. Thật không may, semaphore là một cơ chế và quy trình tự nguyện (người đi trong phòng tắm của chúng tôi) có thể bỏ qua nó (tức là ngay cả khi có chìa khóa, ai đó vẫn có thể mở cửa).
Cũng có sự khác biệt giữa nhị phân / mutex & đếm semaphores.
Kiểm tra các ghi chú bài giảng tại http://www.cs.columbia.edu/~jae/4118/lect/L05-ipc.html .
Đây là một câu hỏi cũ nhưng một trong những cách sử dụng semaphore thú vị nhất là khóa đọc / ghi và nó chưa được đề cập rõ ràng.
Các khóa r / w hoạt động theo cách đơn giản: tiêu thụ một giấy phép cho người đọc và tất cả các giấy phép cho các nhà văn. Thật vậy, việc triển khai khóa ar / w tầm thường nhưng yêu cầu sửa đổi siêu dữ liệu khi đọc (thực tế là hai lần) có thể trở thành cổ chai, vẫn tốt hơn đáng kể so với khóa hoặc khóa.
Một nhược điểm khác là các nhà văn cũng có thể được bắt đầu khá dễ dàng trừ khi semaphore là một công bằng hoặc văn bản có được giấy phép trong nhiều yêu cầu, trong trường hợp đó họ cần một sự thay đổi rõ ràng giữa họ.
Hơn nữa đọc :
Một semaphore là một cách để khóa một tài nguyên để nó được đảm bảo rằng trong khi một đoạn mã được thực thi, chỉ có đoạn mã này có quyền truy cập vào tài nguyên đó. Điều này giữ cho hai luồng không đồng thời tích lũy một tài nguyên, có thể gây ra vấn đề.