[C++ 20 semaphore] C++ thread synchronization new features C++ 20 std::counting_semaphore semaphore usage controls concurrent access to shared resources


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_semaphoreis 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_semaphoreThe 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 calls release()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 returns true. Otherwise, this method does not block and returns immediately false.

  • release(n = 1): This method increments the count of the semaphore. The parameter nspecifies the amount to increase, which defaults to 1. If there are other threads waiting on the semaphore (i.e. they called acquire()the method and are blocked), then this method wakes up those threads.

Here's a simple usage std::counting_semaphoreexample:

#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, workerthe thread waits on the semaphore before starting execution. mainAfter a thread outputs a message, it increments the semaphore, which wakes up workerthe thread. Therefore, this program always outputs "Hello from main!" first, and then "Hello from worker!".

Here is std::counting_semaphoreanother 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 MyClassdeclare a std::counting_semaphore<1>member in semand initialize it to 0 in the constructor. Then we sleepThreadcall in the method sem.acquire(), which will block the thread until the value of the semaphore is greater than 0. In playThreadthe method, we call sem.release()to increment the value of the semaphore, which will wake up any waiting threads.

In mainthe function, we create an instance and call the and method MyClassin two different threads .sleepThreadplayThread

std::counting_semaphore<1>In 1is 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 1create 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.

Please note that this is just a simple example, the actual code may need to be modified according to your specific needs.

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, acquirethe method attempts to decrement the value of the semaphore. If the semaphore's value is greater than 0, then acquirewill succeed and the semaphore's value will be decremented by 1. If the value of the semaphore is 0, then acquirewill 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 acquireoperation will continue to execute, the value of the semaphore will decrease by 1 again, and become 0, and then acquirethe operation is completed.

So, when acquirethe 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::releasefunction increments the count of the semaphore. If the count of the semaphore has reached the maximum value, continuing to call releasethe 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_semaphorethe 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

POSIX semaphores

Semaphore

The Qt framework provides a QSemaphoreclass called , which implements the functionality of a semaphore. QSemaphoreis used in a std::counting_semaphorevery 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 QSemaphorea 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 releasethe method later, waking up the new thread.

QSemaphoreThe main difference between and std::counting_semaphoreis their interface and functionality. QSemaphoremethod 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, QSemaphorean overload is provided that waits on a semaphore for a period of time tryAcquire.

Another difference is that QSemaphorethere 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, QSemaphoreis 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:
  1. Platform compatibility : std::counting_semaphoreIt is part of the C++ standard library, so it can be used on any platform that supports C++. QSemaphoreIt 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.

  2. Error handling : std::counting_semaphoreand QSemaphoreboth use C++'s exception handling mechanism to report errors. POSIX semaphores and System V semaphores use return values ​​and errnovariables to report errors.

  3. Timeout support : QSemaphoreProvides an tryAcquireoverload 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 time sem_timedwait. std::counting_semaphoreAnd System V semaphores don't have built-in timeout support.

  4. Resource management : std::counting_semaphoreand QSemaphoreare 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.

  5. Integration : QSemaphorecan be used with other classes and functions of Qt such as signals and slots. std::counting_semaphoreCan 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.

  6. Named semaphores : POSIX semaphores and System V semaphores support named semaphores, which allow unrelated processes to share semaphores. std::counting_semaphoreand QSemaphoredoes not support named semaphores.

insert image description here

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
insert image description here

Guess you like

Origin blog.csdn.net/qq_21438461/article/details/131819470