directory title
Introduction
A semaphore is a lightweight synchronization primitive used to limit concurrent access to shared resources. In some cases, using semaphores can be more efficient than condition variables.
In the header file of the C++ standard library, the following two types of semaphores are defined:
counting_semaphore: This is a semaphore type that models non-negative resource counting. It is a class template that can be used to implement semaphores with different count values.
binary_semaphore: This is a semaphore type with only two states. It is a type overload, typically used to implement semaphores with mutually exclusive access to shared resources.
These semaphore types provide methods for waiting, releasing, and acquiring resources, depending on the semaphore's type and operation. Using semaphores can effectively control concurrent access to avoid race conditions and resource leaks.
counting_semaphore
std::counting_semaphore
is a class template in the C++20 standard library that implements a counting semaphore. A semaphore is a synchronization primitive used to control concurrent access to a shared resource, or to synchronize between multiple threads.
std::counting_semaphore
The main methods include:
-
acquire()
: If the semaphore's internal count is greater than 0, then this method decrements the count and returns immediately. Otherwise, this method blocks until another thread callsrelease()
the method to increment the count. -
try_acquire()
: This method attempts to decrement the count of the semaphore. If the count is greater than 0, then this method decrements the count and returnstrue
. Otherwise, this method does not block and returns immediatelyfalse
. -
release(n = 1)
: This method increments the count of the semaphore. The parametern
specifies the amount to increase, which defaults to 1. If there are other threads waiting on the semaphore (i.e. they calledacquire()
the method and are blocked), then this method wakes up those threads.
Here's a simple usage std::counting_semaphore
example:
#include <iostream>
#include <thread>
#include <semaphore>
std::counting_semaphore<1> sema(0); // 创建一个初始值为0的信号量
void worker() {
sema.acquire(); // 等待信号量
std::cout << "Hello from worker!\n";
}
int main() {
std::thread t(worker); // 创建一个新线程
std::cout << "Hello from main!\n";
sema.release(); // 增加信号量,唤醒worker线程
t.join(); // 等待worker线程结束
return 0;
}
In this example, worker
the thread waits on the semaphore before starting execution. main
After a thread outputs a message, it increments the semaphore, which wakes up worker
the thread. Therefore, this program always outputs "Hello from main!" first, and then "Hello from worker!".
Here is std::counting_semaphore
another example code using
#include <thread>
#include <semaphore>
#include <iostream>
class MyClass {
public:
MyClass() : sem(0) {
} // 初始化信号量为0
void sleepThread() {
std::cout << "Sleep thread waiting...\n";
sem.acquire(); // 如果信号量为0,这将阻塞线程
std::cout << "Sleep thread woke up!\n";
}
void playThread() {
std::cout << "Play thread releasing semaphore...\n";
sem.release(); // 增加信号量的值,如果有线程在等待,它将被唤醒
}
private:
std::counting_semaphore<1> sem; // 信号量
};
int main() {
MyClass myClass;
std::thread t1(&MyClass::sleepThread, &myClass);
std::thread t2(&MyClass::playThread, &myClass);
t1.join();
t2.join();
return 0;
}
In this code, we MyClass
declare a std::counting_semaphore<1>
member in sem
and initialize it to 0 in the constructor. Then we sleepThread
call in the method sem.acquire()
, which will block the thread until the value of the semaphore is greater than 0. In playThread
the method, we call sem.release()
to increment the value of the semaphore, which will wake up any waiting threads.
In main
the function, we create an instance and call the and method MyClass
in two different threads .sleepThread
playThread
std::counting_semaphore<1>
In 1
is the template parameter, which represents the maximum value of the semaphore. This parameter determines how many threads the semaphore can allow to access the shared resource at the same time.
In this example, we 1
create a binary semaphore using as a template parameter. A binary semaphore can only have a value of 0 or 1, so it can only allow one thread to access a shared resource. This is useful for mutual exclusion (mutex) operations, for example when you need to protect access to shared resources.
If you need to allow multiple threads to access shared resources at the same time, you can use a value greater than 1 as a template parameter. For example, std::counting_semaphore<3>
a semaphore is created that allows three threads to access a shared resource at the same time.
the value of the semaphore
In computer science, a semaphore is a synchronization mechanism used to control multiple threads accessing shared resources. A semaphore has a count value associated with it, which indicates the number of threads that can simultaneously access a resource.
The count value of the semaphore is not necessarily limited to 0 and 1. In fact, the count value of a semaphore can be any non-negative integer. When a thread tries to acquire a semaphore, if the semaphore count value is greater than 0, the thread can continue executing, and the semaphore count value will be decremented by 1. If the count value of the semaphore is 0, the thread will be blocked until the count value of the semaphore is greater than 0.
When we say std::counting_semaphore<1>
it is a binary semaphore, we mean that its count value can only be 0 or 1. This means it can only allow one thread to access the shared resource. This type of semaphore is often used for mutual exclusion (mutex) operations, that is, to protect access to shared resources.
However, if we create one std::counting_semaphore<3>
, then its count value can be 0, 1, 2 or 3. This means it can allow up to three threads to access shared resources simultaneously. This type of semaphore can be used in more complex synchronization scenarios, such as when you need to allow multiple threads to read a resource at the same time, but only allow one thread to write to it.
Therefore, the template parameters and initial value of the semaphore determine the number of threads that can access the shared resource at the same time.
Handling of upper and lower bounds
block at zero,
In std::counting_semaphore
, acquire
the method attempts to decrement the value of the semaphore. If the semaphore's value is greater than 0, then acquire
will succeed and the semaphore's value will be decremented by 1. If the value of the semaphore is 0, then acquire
will block until the value of the semaphore is greater than 0.
When you call semaphore.acquire()
and the value of the semaphore is 0, the thread will block. If another thread calls during this period, semaphore.release()
the value of the semaphore will increase by 1, the previously blocked acquire
operation will continue to execute, the value of the semaphore will decrease by 1 again, and become 0, and then acquire
the operation is completed.
So, when acquire
the operation completes, the value of the semaphore will be 0 (assuming no other threads changed the value of the semaphore during this time). If called again at this point acquire
, the thread will block again until another thread calls release
.
This is the basic working principle of the semaphore: it allows a certain number of threads to access a resource at the same time. When the resource is fully occupied (that is, the value of the semaphore is 0), other threads will be blocked until a resource is released.
Upper limit
std::counting_semaphore::release
function increments the count of the semaphore. If the count of the semaphore has reached the maximum value, continuing to call release
the function will result in undefined behavior (undefined behavior). In C++, undefined behavior means that the behavior of the program is not specified by the C++ standard, and may cause any result, including the program crashing, producing erroneous results, or seemingly normal behavior. Therefore, you should avoid calling the function again when the semaphore has already reached its maximum value release
.
This is because std::counting_semaphore
the design goal of is used to synchronize threads, not to be used as a counter. If you need a counter that is safe to overflow, you may need to use other data structures or algorithms.
Other commonly used semaphores
POSIX semaphores
Semaphore
The Qt framework provides a QSemaphore
class called , which implements the functionality of a semaphore. QSemaphore
is used in a std::counting_semaphore
very similar way to , but it provides some additional functionality, such as the ability to try to acquire a semaphore without blocking the thread, or to wait for a semaphore for a period of time.
Here is QSemaphore
a basic usage example of one:
#include <QSemaphore>
#include <QThread>
#include <QDebug>
class MyThread : public QThread
{
public:
MyThread(QSemaphore *semaphore) : sem(semaphore) {
}
void run() override {
sem->acquire();
qDebug() << "Thread woke up!";
}
private:
QSemaphore *sem;
};
int main() {
QSemaphore semaphore(0);
MyThread thread(&semaphore);
thread.start();
qDebug() << "Releasing semaphore...";
semaphore.release();
thread.wait();
return 0;
}
In this example, we create an initial value of 0 QSemaphore
, and then call the method in a new thread acquire
. The main thread calls release
the method later, waking up the new thread.
QSemaphore
The main difference between and std::counting_semaphore
is their interface and functionality. QSemaphore
method is provided tryAcquire
, which tries to acquire the semaphore, and if the semaphore's count value is 0, it will return immediately instead of blocking the thread. Additionally, QSemaphore
an overload is provided that waits on a semaphore for a period of time tryAcquire
.
Another difference is that QSemaphore
there is no template parameter, and its count value can be any non-negative integer, not just 0 and 1. This means that you can create a semaphore that allows multiple threads to access a shared resource at the same time.
Finally, QSemaphore
is part of the Qt framework, so it can be used with other Qt classes and features such as signals and slots. Rather,std::counting_semaphore
it is part of the C++ standard library, which integrates better with other features of C++ such as threads and locks.
Comparison of various semaphores
The following is a table comparing QSemaphore
, std::counting_semaphore
, POSIX semaphores and System V semaphores:
Semaphore | std::counting_semaphore | POSIX Semaphore | System V Semaphore | |
---|---|---|---|---|
interface | Provides a high-level, object-oriented interface, such as acquire() and release() methods |
Provides a similar interface, but as part of the C++ standard library | Provides a lower-level, C-style interface, such as sem_wait() and sem_post() functions |
Provides a C-style interface, but uses different functions, such assemop() |
Upper limit | can be any non-negative integer | can be any non-negative integer | can be any non-negative integer | can be any non-negative integer |
Behavior | If the value of the semaphore is 0, block the calling thread; if the value increases, unblock one or more threads | If the value of the semaphore is 0, block the calling thread; if the value increases, unblock one or more threads | If the value of the semaphore is 0, block the calling thread; if the value increases, unblock one or more threads | If the value of the semaphore is 0, block the calling thread; if the value increases, unblock one or more threads |
underlying principle | is part of the Qt framework and uses Qt's threading and synchronization primitives | is part of the C++ standard library and uses the threading and synchronization primitives provided by the C++ runtime | is part of the Unix API and uses the kernel's threading and synchronization primitives | is part of the Unix API and uses the kernel's threading and synchronization primitives |
limit | Limited by the capabilities of the C++ runtime and the Qt framework | Limited by the capabilities of the C++ runtime | Limited by the capabilities of the Unix kernel and may not be available on non-Unix systems | Limited by the capabilities of the Unix kernel and may not be available on non-Unix systems |
Here's an additional comparison of QSemaphore , std::counting_semaphore , POSIX semaphores and System V semaphores: |
-
Platform compatibility :
std::counting_semaphore
It is part of the C++ standard library, so it can be used on any platform that supports C++.QSemaphore
It is part of the Qt framework and therefore requires the support of the Qt library. POSIX semaphores and System V semaphores are part of the Unix API, so are available on Unix and Unix-like systems such as Linux and macOS, but may not be available on non-Unix systems such as Windows. -
Error handling :
std::counting_semaphore
andQSemaphore
both use C++'s exception handling mechanism to report errors. POSIX semaphores and System V semaphores use return values anderrno
variables to report errors. -
Timeout support :
QSemaphore
Provides antryAcquire
overload that waits on a semaphore for a certain amount of time. POSIX semaphores also provide a function that can wait on a semaphore for a period of timesem_timedwait
.std::counting_semaphore
And System V semaphores don't have built-in timeout support. -
Resource management :
std::counting_semaphore
andQSemaphore
are both classes, so C++ constructors and destructors can be used for resource management. POSIX semaphores and System V semaphores are managed through function calls and need to be created and destroyed manually. -
Integration :
QSemaphore
can be used with other classes and functions of Qt such as signals and slots.std::counting_semaphore
Can be better integrated with other features of C++ such as threads and locks. POSIX semaphores and System V semaphores can be used with other parts of the Unix API. -
Named semaphores : POSIX semaphores and System V semaphores support named semaphores, which allow unrelated processes to share semaphores.
std::counting_semaphore
andQSemaphore
does not support named semaphores.
epilogue
Comprehension is an important step towards the next level in our programming learning journey. However, mastering new skills and ideas always takes time and persistence. From a psychological point of view, learning is often accompanied by continuous trial and error and adjustment, which is like our brain gradually optimizing its "algorithm" for solving problems.
That's why when we encounter mistakes, we should see them as opportunities to learn and improve, not just obsessions. By understanding and solving these problems, we can not only fix the current code, but also improve our programming ability and prevent the same mistakes from being made in future projects.
I encourage everyone to actively participate and continuously improve their programming skills. Whether you are a beginner or an experienced developer, I hope my blog can help you in your learning journey. If you find this article useful, you may wish to click to bookmark it, or leave your comments to share your insights and experiences. You are also welcome to make suggestions and questions about the content of my blog. Every like, comment, share and follow is the greatest support for me and the motivation for me to continue to share and create.
Read my CSDN homepage to unlock more exciting content: Bubble's CSDN homepage