[C++ 20 Concurrency Tool std::barrier] Maîtrisez la programmation simultanée : compréhension approfondie de std::barrier de C++


1. Introduction

1.1. La définition et la fonction de std::barrier

En programmation concurrente (Concurrent Programming), nous avons souvent besoin de coordonner l'ordre d'exécution de plusieurs threads (Threads). C'est le concept de synchronisation. C++20 introduit une nouvelle fonctionnalité de synchronisation appelée std::barrier(barrière). std::barrierEst une primitive de synchronisation qui permet à plusieurs threads de s'attendre à un point de synchronisation spécifique (point de synchronisation). En d'autres termes, std::barrierun groupe de threads peut être bloqué jusqu'à ce que tous les threads aient atteint un point de synchronisation.

Dans la communication parlée, nous pouvons décrire std::barrierla fonction comme suit : "Le std::barrier est une primitive de synchronisation qui permet à plusieurs threads de s'attendre à un point de synchronisation spécifique." (std::barrier est une primitive de synchronisation, il Multiple les threads sont autorisés à s'attendre à un point de synchronisation spécifique.)

1.2. L'importance de std::barrier dans la programmation concurrente

En programmation concurrente, std::barrierson importance est évidente. Il nous fournit un moyen simple et efficace de coordonner l'ordre d'exécution de plusieurs threads. Ceci est particulièrement important lorsqu'il s'agit de problèmes de multi-threading, par exemple, cela peut être utile lorsque nous devons attendre que tous les threads aient terminé une certaine tâche avant de passer à l'étape suivante std::barrier.

Par exemple, supposons que nous ayons un programme concurrent composé de plusieurs threads, chacun devant effectuer des calculs. Nous voulons que tous les threads terminent leurs calculs avant de passer à l'étape suivante. Il s'agit std::barrierd'un scénario d'application typique.

Dans la communication orale, nous pouvons décrire std::barrierl'importance de la programmation concurrente de cette manière : "En programmation concurrente, la barrière std::barrier est cruciale car elle fournit un moyen simple et efficace de coordonner l'ordre d'exécution de plusieurs threads. Elle est particulièrement importante lorsque traiter les problèmes de multi-threading où nous devons passer à l'opération suivante uniquement après que tous les threads ont terminé une certaine tâche." (En programmation concurrente, l'importance de std::barrier est évidente. Elle nous fournit un outil simple et un moyen efficace de coordonner l'ordre d'exécution de plusieurs threads. Ceci est particulièrement important lorsqu'il s'agit de problèmes de multithreading. Par exemple, lorsque nous devons terminer une tâche dans tous les threads avant de passer à l'étape suivante, std:: barrier sera utile. )

Chapitre 2 : Le principe de std::barrier

2.1 Le mécanisme interne de std::barrier

std::barrier est une nouvelle primitive de synchronisation introduite par C++20, qui permet à plusieurs threads d'attendre à un point de synchronisation spécifique jusqu'à ce que tous les threads aient atteint ce point, puis tous les threads peuvent continuer à s'exécuter. Cette fonctionnalité est très importante dans la programmation concurrente car elle garantit que tous les threads ont terminé le travail nécessaire avant de poursuivre l'exécution.

En interne, std::barrier utilise un compteur pour suivre le nombre de threads ayant atteint un point de synchronisation. Lorsqu'un thread atteint un point de synchronisation, il appelle la fonction membre std::barrier arrive(), qui décrémente le compteur. Lorsque le compteur atteint zéro, tous les threads en attente sont libérés et l'exécution peut continuer.

Voici un exemple d'utilisation simple de std::barrier :

#include <barrier>
#include <thread>
#include <vector>

std::barrier b(3); // 创建一个barrier,需要3个线程到达同步点

void worker() {
    
    
    // ... do some work ...

    b.arrive_and_wait(); // 到达同步点并等待

    // ... continue with other work ...
}

int main() {
    
    
    std::vector<std::thread> threads;
    for (int i = 0; i < 3; ++i) {
    
    
        threads.emplace_back(worker);
    }

    for (auto& t : threads) {
    
    
        t.join();
    }

    return 0;
}

Dans cet exemple, nous créons une barrière qui nécessite 3 threads pour atteindre un point de synchronisation. Nous créons ensuite 3 threads, chacun d'entre eux, après avoir effectué un certain travail, atteint un point de synchronisation et attend. Lorsque tous les threads ont atteint le point de synchronisation, ils peuvent continuer à s'exécuter.

2.2 Comment utiliser std::barrier pour la synchronisation

std::barrier fournit plusieurs fonctions membres pour la synchronisation :

  • arrive() : indique qu'un thread a atteint le point de synchronisation. Cela décrémente le compteur interne. Si le compteur atteint zéro, tous les threads en attente sont libérés.
  • wait() : fait attendre un thread au point de synchronisation jusqu'à ce que tous les threads atteignent le point de synchronisation.
  • arrive_and_wait() : Il s'agit d'une combinaison de arrive() et wait(). Il indique qu'un thread a atteint le point de synchronisation et attend sur le point de synchronisation jusqu'à ce que tous les threads aient atteint le point de synchronisation.
  • arrive_and_drop() : Ceci indique qu'un thread a atteint le point de synchronisation et n'a plus besoin d'être synchronisé. Cela décrémente le compteur interne et décrémente le nombre total de threads de la barrière de un.

Voici un exemple de synchronisation avec std::barrier :

#include <barrier>
#include <thread>
#include <vector>

std::barrier b(3); // 创建一个barrier,需要3个线程到达同步点

void worker(int id) {
    
    
    // ... do some work ...

    if (id == 2) {
    
    
        b.arrive_and_drop(); // 线程2到达同步点,并且不再需要同步
    } else {
    
    
        b.arrive_and_wait(); // 其他线程到达同步点并等待
    }

    // ... continue with other work ...
}

int main() {
    
    
    std::vector<std::thread> threads;
    for (int i = 0; i < 3; ++i) {
    
    
        threads.emplace_back(worker, i);
    }

    for (auto& t : threads) {
    
    
        t.join();
    }

    return 0;
}

Dans cet exemple, nous créons une barrière qui nécessite 3 threads pour atteindre un point de synchronisation. Nous créons ensuite 3 threads, chacun atteignant un point de synchronisation après avoir effectué un certain travail. Une fois que le thread 2 a atteint le point de synchronisation, il indique qu'il n'a plus besoin de se synchroniser, il appelle donc arrive_and_drop(). Une fois que d'autres threads ont atteint le point de synchronisation, ils appellent arrive_and_wait() pour attendre. Lorsque tous les threads ont atteint le point de synchronisation, ils peuvent continuer à s'exécuter.

Dans cet exemple, nous pouvons voir la flexibilité de std::barrier. Il peut accueillir un nombre dynamique de threads, ce qui est très important dans la programmation concurrente.

Dans la communication orale, nous pouvons décrire le principe de fonctionnement de std::barrier comme suit : "std::barrier fonctionne en gardant un compte du nombre de threads qui sont arrivés à un point de synchronisation. Lorsqu'un thread arrive au point de synchronisation, il appelle la fonction arrive(), qui diminue le nombre. Lorsque le nombre atteint zéro, tous les threads en attente sont libérés et peuvent continuer l'exécution." (std::barrier fonctionne en gardant une trace du nombre de threads qui ont atteint un point de synchronisation . Lorsqu'un thread atteint Lorsque le point de synchronisation est atteint, il appelle la fonction arrive(), qui décrémente le compteur. Lorsque le compteur atteint zéro, tous les threads en attente sont libérés et l'exécution peut continuer.)

Dans cette phrase, "fonctionne par" est une expression courante utilisée pour décrire comment ou comment quelque chose fonctionne. « Tenir un compte de » est une expression courante pour décrire le fait de tenir un compte de. "Arrive à" est une expression courante pour décrire l'arrivée à un endroit ou à un état. "appelle la fonction" est une expression courante pour décrire l'appel d'une fonction. "diminue le compte" est une expression courante pour décrire la diminution d'une quantité ou d'un compte. "atteint zéro" est une expression courante pour décrire une quantité ou un comptage atteignant zéro. "sont libérés" est une expression courante pour décrire le fait d'être libéré ou libéré. "peut continuer l'exécution" est une expression courante pour décrire que l'exécution peut continuer ou continuer.

3. Comparaison de std::barrier avec d'autres outils de synchronisation

En programmation concurrente, nous avons une variété d'outils de synchronisation qui peuvent être utilisés, y compris std::mutex (mutex), std::condition_variable (variable de condition), std::latch (loquet de porte) et std::semaphore (sémaphore) . Dans ce chapitre, nous allons comparer en profondeur std::barrier avec ces outils.

3.1. Comparaison de std::barrier et std::mutex

std::mutex (mutex) est un outil de synchronisation utilisé pour protéger les ressources partagées, qui empêche plusieurs threads d'accéder à la même ressource en même temps. Le std::barrier (barrière) est un outil de synchronisation qui peut synchroniser plusieurs threads à un certain point et ne continuera pas tant que tous les threads n'auront pas atteint ce point.

Dans la communication orale en anglais, nous disons généralement "Un mutex est utilisé pour protéger les ressources partagées des accès concurrents" (le mutex est utilisé pour protéger les ressources partagées des accès concurrents), et pour std::barrier, nous dirions "Une barrière est utilisée pour synchroniser plusieurs threads à un certain point" (la barrière est utilisée pour synchroniser plusieurs threads à un certain point).

Dans les deux phrases, nous avons utilisé l'expression "est habitué à" pour décrire le but de ces deux outils. En anglais américain, "is used to" est une expression courante utilisée pour décrire le but ou la fonction de quelque chose.

Voici un exemple utilisant std::mutex et std::barrier :

std::mutex mtx;
std::barrier bar(2);

void thread_func() {
    
    
    std::lock_guard<std::mutex> lock(mtx);
    // 对共享资源进行操作
    // ...
    bar.arrive_and_wait();  // 等待其他线程
}

int main() {
    
    
    std::thread t1(thread_func);
    std::thread t2(thread_func);
    t1.join();
    t2.join();
    return 0;
}

Dans cet exemple, nous utilisons std::mutex pour protéger les ressources partagées et std::barrier pour synchroniser deux threads.

3.2. Comparaison de std::barrier et std::condition_variable

std::condition_variable (variable de condition) est un outil de synchronisation qui permet à un thread d'attendre une condition spécifique jusqu'à ce qu'un autre thread le notifie. Le std::barrier (barrière) est un outil de synchronisation qui peut synchroniser plusieurs threads à un certain point et ne continuera pas tant que tous les threads n'auront pas atteint ce point.

Dans la communication orale en anglais, nous disons généralement "Une variable de condition est utilisée pour faire attendre un thread sous certaines conditions" (les variables de condition sont utilisées pour faire attendre les threads sous certaines conditions), et pour std::barrier, nous dirions " Une barrière est utilisé pour synchroniser plusieurs threads à un certain point" (la barrière est utilisée pour synchroniser plusieurs threads à un certain point).

Dans les deux phrases, nous avons utilisé l'expression "est habitué à" pour décrire le but de ces deux outils. En anglais américain, "is used to" est une expression courante utilisée pour décrire le but ou la fonction de quelque chose.

Voici un exemple utilisant std::condition_variable et std::barrier :

std::mutex mtx;
std::condition_variable cv;
bool ready = false;
std::barrier bar(2);

void thread_func() {
    
    
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, []{
    
     return ready; });  // 等待条件满足
    // ...
    bar.arrive_and_wait();  // 等待其他线程
}

void set_ready() {
    
    
    std::lock_guard<std::mutex> lock(mtx);
    ready = true;
    cv.notify_all();  // 通知所有等待的线程
}

int main() {
    
    
    std::thread t1(thread_func);
    std::thread t2(set_ready);
    t1.join();
    t2.join();
    return 0;
}

Dans cet exemple, nous utilisons std::condition_variable pour faire attendre un thread sur une certaine condition et std::barrier pour synchroniser deux threads.

3.3. Comparaison de std::barrier avec std::latch et std::semaphore

std::latch (loquet de porte) et std::semaphore (sémaphore) sont de nouveaux outils de synchronisation introduits par C++20. std::latch permet de décrémenter le compte une ou plusieurs fois jusqu'à ce que le compte atteigne zéro, après quoi tous les threads en attente peuvent passer. std::semaphore est un outil de synchronisation plus général, qui peut limiter le nombre de threads accédant à une ressource ou à un ensemble de ressources en même temps.

La principale différence entre std::barrier et ces deux-là est que std::barrier est automatiquement réinitialisé chaque fois que tous les threads ont atteint la barrière, contrairement à std::latch. De plus, std::semaphore peut autoriser n'importe quel nombre de threads à passer, tandis que std::barrier nécessite que tous les threads soient atteints.

在英语口语交流中,我们通常会说 “A latch allows a count to be decremented once or more times until it reaches zero, then all waiting threads can pass”(门闩允许计数一次或多次递减,直到达到零,然后所有等待的线程都可以通过),“A semaphore can limit the number of threads that can access a resource or set of resources at the same time”(信号量可以限制同时访问某一资源或资源集的线程数量),而对于std::barrier,我们会说 “A barrier is used to synchronize multiple threads at a certain point, and it resets automatically after all threads have arrived”(屏障用于在某一点同步多个线程,并在

所有线程都到达后自动重置)。

在这三个句子中,我们使用了 “allows”(允许)、“can limit”(可以限制)和 “is used to”(用于)这些短语来描述这三个工具的用途。在美式英语中,这些都是常见的表达方式,用于描述某物的用途或功能。

下面是一个使用std::latch、std::semaphore和std::barrier的示例:

std::latch lat(2);
std::counting_semaphore sem(1);
std::barrier bar(2);

void thread_func() {
    
    
    sem.acquire();  // 获取信号量
    // 对资源进行操作
    // ...
    sem.release();  // 释放信号量
    lat.count_down();  // 递减门闩计数
    bar.arrive_and_wait();  // 等待其他线程
}

int main() {
    
    
    std::thread t1(thread_func);
    std::thread t2(thread_func);
    t1.join();
    t2.join();
    return 0;
}

在这个示例中,我们使用std::latch来同步两个线程,使用std::semaphore来限制对资源的并发访问,使用std::barrier来同步两个线程。

下表总结了std::barrier与其他同步工具的主要区别:

工具 用途 特性
std::mutex 保护共享资源免受并发访问 互斥
std::condition_variable 使线程在特定条件下等待 条件等待
std::latch 使线程在计数达到零时通过 一次性
std::semaphore 限制同时访问资源的线程数量 可重用
std::barrier 在某一点同步多个线程 自动重置

在理解这些同步工具的区别时,我们可以参考Bjarne Stroustrup的《C++ Programming Language》一书中的相关章节,其中详细介绍了这些工具的用途和使用方法。

4. std::barrier的实际应用

4.1. 使用std::barrier解决并发问题的实例

在并发编程中,我们经常会遇到需要多个线程(Threads)同时开始执行某项任务的情况。这时,我们可以使用std::barrier来实现这个需求。std::barrier(屏障)是C++20引入的一个新特性,它可以阻塞一组线程,直到所有线程都到达某个点,然后这些线程才会同时开始执行。

让我们来看一个例子。假设我们有一个需要多个线程同时开始的任务,我们可以使用std::barrier来实现这个需求。

#include <iostream>
#include <thread>
#include <barrier>

std::barrier b(3); // 创建一个屏障,需要3个线程到达

void task(const char* threadName) {
    
    
    std::cout << threadName << " is waiting\n";
    b.arrive_and_wait(); // 线程到达屏障并等待
    std::cout << threadName << " is processing\n";
}

int main() {
    
    
    std::thread t1(task, "Thread 1");
    std::thread t2(task, "Thread 2");
    std::thread t3(task, "Thread 3");

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

在这个例子中,我们创建了一个需要3个线程到达的屏障。每个线程在开始处理任务之前,都会先到达屏障并等待。只有当所有线程都到达屏障时,这些线程才会同时开始处理任务。

在口语交流中,我们可以这样描述这个过程:“We create a barrier that requires three threads to arrive. Each thread arrives at the barrier and waits before it starts processing the task. The threads only start processing the task when all of them have arrived at the barrier.”(我们创建了一个需要三个线程到达的屏障。每个线程在开始处理任务之前,都会先到达屏障并等待。只有当所有线程都到达屏障时,这些线程才会同时开始处理任务。)

4.2. std::barrier在Qt编程中的应用

在Qt编程中,我们也可以使用std::barrier来同步多个线程。例如,我们可以使用std::barrier来确保所有的GUI线程在开始更新界面之前,都已经完成了数据的加载。

#include <QThread>
#include <barrier>

std::barrier b(3); // 创建一个屏障,需要3个线程到达

class DataLoader : public QThread {
    
    
public:
    void run() override {
    
    
        // 加载数据...
        b.arrive_and_wait(); // 线程到达屏障并等待
    }
};

class GUIUpdater : public QThread {
    
    
public:
    void run() override {
    
    
        b.arrive_and_wait(); // 线程到达屏障并等待
        // 更新界面...
    }
};

int main() {
    
    
    DataLoader loader1, loader2;
    GUIUpdater updater;

    loader1.start();
    loader2.start();
    updater.start();

    loader1.wait();
    loader2.wait();
    updater.wait();

    return 0;
}

在这个例子中,我们有两个数据加载线程和一个GUI更新线程。我们使用std::barrier来确保所有的线程在开始更新界面之前,都已经完成了数据的加载。

在口语交流中,我们可以这样描述这个过程:“We have two data loading threads and one GUI updating thread. We use a barrier to ensure that all threads have finished loading data before they start updating the GUI.”(我们有两个数据加载线程和一个GUI更新线程。我们使用屏障来确保所有的线程在开始更新界面之前,都已经完成了数据的加载。)

在这两个例子中,我们可以看到std::barrier在实际应用中的强大功能。它可以帮助我们更容易地同步多个线程,使我们的代码更简洁,更易于理解和维护。

在接下来的章节中,我们将深入探讨std::barrier的内部工作原理,以及如何在实际编程中有效地使用std::barrier。

5. std::barrier的最佳实践和注意事项

5.1. 如何有效地使用std::barrier

在并发编程中,std::barrier(屏障)是一种非常有效的同步工具。它允许多个线程在一个预定的同步点上相互等待,直到所有线程都到达这个点,然后再继续执行。这种机制可以确保所有线程在进入下一阶段之前,都已经完成了当前阶段的工作。

在使用std::barrier时,我们需要注意以下几点:

  1. 初始化屏障的数量:在创建std::barrier时,我们需要指定一个参数,这个参数表示需要等待的线程数量。这个数量应该等于将要在屏障上等待的线程数量。如果这个数量设置得过大或过小,都可能导致程序的行为不符合预期。

  2. 避免在同一线程上多次等待std::barrier的设计是为了让多个线程在同一点上相互等待,而不是让同一线程在同一点上多次等待。如果在同一线程上多次调用std::barrier::wait(),可能会导致程序的行为不符合预期。

  3. 理解屏障的周期性std::barrier是周期性的,也就是说,当所有线程都到达屏障后,屏障会自动重置,可以被再次使用。这一点与std::latch(门闩)不同,std::latch在所有线程到达后就不能再被使用。

下面是一个使用std::barrier的示例:

#include <iostream>
#include <thread>
#include <barrier>

std::barrier b(2); // 创建一个需要等待两个线程的屏障

void print(const char* str) {
    
    
    std::cout << str << std::endl;
    b.arrive_and_wait(); // 到达屏障并等待
}

int main() {
    
    
    std::thread t1(print, "Hello from thread 1");
    std::thread t2(print, "Hello from thread 2");

    t1.join();
    t2.join();

    return 0;
}

在这个示例中,我们创建了两个线程,每个线程都会打印一条消息,然后到达屏障并等待。由于我们创建的屏障需要等待两个线程,所以当两个线程都到达屏障后,它们都会继续执行。

5.2. 避免常见的std::barrier使用错误

虽然std::barrier是一个强大的同步工具,但如果使用不当,也可能导致一些问题。下面我们来看一些常见的std::barrier使用错误,以及如何避免它们。

  1. 错误的屏障数量:如前所述,创建std::barrier时需要指定一个参数,表示需要等待的线程数量。如果这个数量设置得过大,那么可能会有一些线程永远无法到达屏障,导致其他线程在屏障上无限等待。如果这个数量设置得过小,那么可能会有一些线程在其他线程还没有到达屏障时就已经继续执行,这可能会导致数据竞争或其他并发问题。

  2. 在同一线程上多次等待:如前所述,std::barrier的设计是为了让多个线程在同一点上相互等待,而不是让同一线程在同一点上多次等待。如果在同一线程上多次调用std::barrier::wait(),可能会导致程序的行为不符合预期。

  3. 忽视屏障的周期性std::barrier是周期性的,也就是说,当所有线程都到达屏障后,屏障会自动重置,可以被再次使用。如果忽视了这一点,可能会导致程序的行为不符合预期。

为了避免这些错误,我们需要确保理解std::barrier的工作原理,并在使用时遵循上述的最佳实践。

在下一章节中,我们将探讨std::barrier在未来C++版本中的可能改进,以及它在并发编程中的未来角色。

6. std::barrier的未来展望

6.1. std::barrier在未来C++版本中的可能改进

在C++20中,std::barrier(屏障)已经成为了并发编程的重要工具。然而,C++的发展从未停止过,我们可以预期在未来的C++版本中,std::barrier可能会有一些改进。

首先,我们可能会看到对std::barrier的性能优化。这可能包括减少内存占用,提高同步效率等。例如,我们可能会看到一种新的std::barrier实现,它使用更少的内存,但仍然能够提供相同的同步功能。

其次,std::barrier可能会增加更多的功能。例如,它可能会支持超时,这样如果一组线程在指定的时间内没有达到屏障,那么屏障就会自动解除。这将使得std::barrier在处理一些需要时间限制的并发问题时更加方便。

最后,std::barrier可能会有更好的错误处理机制。目前,如果一个线程在达到屏障之前异常终止,那么其他等待在屏障处的线程可能会永远等待下去。未来的std::barrier可能会提供一种机制,使得在这种情况下,其他线程可以得到通知并继续执行。

6.2. std::barrier在并发编程中的未来角色

std::barrier在并发编程中的角色可能会随着并发编程模型的发展而发展。随着多核处理器的普及和并发编程的重要性日益增加,我们可能会看到std::barrier在更多的场景中被使用。

例如,随着异步编程模型的发展,我们可能会看到std::barrier在异步编程中扮演更重要的角色。在异步编程模型中,多个任务可能会在不同的时间点开始和结束,这使得同步变得更加复杂。std::barrier可以帮助解决这种复杂性,使得异步任务可以在特定的时间点进行同步。

此外,std::barrier可能会在分布式系统中扮演更重要的角色。在分布式系统中,多个节点需要进行协调以完成一项任务。std::barrier可以帮助实现这种协调,使得所有节点可以在同一时间点开始执行下一步操作。

总的来说,std::barrier在并发编程中的未来角色将取决于并发编程模型的发展和并发问题的复杂性。我们可以期待std::barrier在未来的并发编程中扮演更重要的角色。

注意:以上内容是基于对C++和并发编程的深入理解和预测,实际的发展可能会有所不同。

结语

在我们的编程学习之旅中,理解是我们迈向更高层次的重要一步。然而,掌握新技能、新理念,始终需要时间和坚持。从心理学的角度看,学习往往伴随着不断的试错和调整,这就像是我们的大脑在逐渐优化其解决问题的“算法”。

这就是为什么当我们遇到错误,我们应该将其视为学习和进步的机会,而不仅仅是困扰。通过理解和解决这些问题,我们不仅可以修复当前的代码,更可以提升我们的编程能力,防止在未来的项目中犯相同的错误。

我鼓励大家积极参与进来,不断提升自己的编程技术。无论你是初学者还是有经验的开发者,我希望我的博客能对你的学习之路有所帮助。如果你觉得这篇文章有用,不妨点击收藏,或者留下你的评论分享你的见解和经验,也欢迎你对我博客的内容提出建议和问题。每一次的点赞、评论、分享和关注都是对我的最大支持,也是对我持续分享和创作的动力。


阅读我的CSDN主页,解锁更多精彩内容:泡沫的CSDN主页
在这里插入图片描述

Guess you like

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