[C++ 20 同時実行ツール std::barrier] マスター同時プログラミング: C++ の std::barrier を深く理解する


1 はじめに

1.1. std::barrier の定義と機能

並行プログラミング (Concurrent Programming) では、多くの場合、複数のスレッド (Thread) の実行順序を調整する必要があります。これが同期の概念です。C++20 では、(バリア) と呼ばれる新しい同期機能が導入されていますstd::barrierstd::barrier複数のスレッドが特定の同期ポイント (同期ポイント) で相互に待機できるようにする同期プリミティブです。つまり、std::barrierすべてのスレッドが同期ポイントに到達するまで、スレッドのグループをブロックできます。

std::barrier音声コミュニケーションでは、この関数を次のように説明できます: 「std::barrier は、複数のスレッドが特定の同期ポイントで互いに待機できるようにする同期プリミティブです。」 (std::barrier は同期プリミティブですスレッドは特定の同期ポイントで相互に待機することができます。)

1.2. 同時プログラミングにおける std::barrier の重要性

同時プログラミングでは、std::barrierその重要性は自明のことです。これは、複数のスレッドの実行順序を調整するためのシンプルかつ効率的な方法を提供します。これは、マルチスレッドの問題を扱うときに特に重要です。たとえば、次のステップに進む前に、すべてのスレッドが特定のタスクを完了するのを待つ必要がある場合に便利ですstd::barrier

たとえば、複数のスレッドで構成され、それぞれが何らかの計算を行う必要がある同時プログラムがあるとします。次のステップに進む前に、すべてのスレッドが計算を完了する必要があります。std::barrierこれは典型的なアプリケーション シナリオです。

std::barrier音声コミュニケーションでは、同時プログラミングの重要性を次のように説明できます。「同時プログラミングでは、std::barrier は複数のスレッドの実行順序を調整する簡単かつ効果的な方法を提供するため、非常に重要です。これは特に重要です。すべてのスレッドが特定のタスクを完了した後にのみ次の操作に進む必要があるマルチスレッドの問題に対処します。」 (同時プログラミングでは、std::barrier の重要性は自明のことです。複数のスレッドの実行順序を調整する効果的な方法です。これは、マルチスレッドの問題を扱うときに特に重要です。たとえば、次のステップに進む前にすべてのスレッドでタスクを完了する必要がある場合、std:: Barrier が役立ちます。 )

第 2 章: std::barrier の原理

2.1 std::barrier の内部メカニズム

std::barrier は、C++20 で導入された新しい同期プリミティブで、すべてのスレッドが特定の同期ポイントに到達するまで、複数のスレッドがその同期ポイントで待機できるようになり、その後、すべてのスレッドが実行を続行できるようになります。この機能は、実行を続行する前にすべてのスレッドが必要な作業を完了していることを保証するため、同時プログラミングにおいて非常に重要です。

内部的には、 std::barrier はカウンターを使用して、同期ポイントに到達したスレッドの数を追跡します。スレッドが同期ポイントに到達すると、カウンターをデクリメントする std::barrier メンバー関数 ask() を呼び出します。カウンタがゼロに達すると、待機中のスレッドはすべて解放され、実行を続行できます。

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;
}

この例では、同期ポイントに到達するために 3 つのスレッドを必要とするバリアを作成します。次に 3 つのスレッドを作成し、それぞれが何らかの作業を行った後、同期ポイントに到達して待機します。すべてのスレッドが同期ポイントに到達すると、実行を続行できます。

2.2 同期に std::barrier を使用する方法

std::barrier は、同期用のいくつかのメンバー関数を提供します。

  • 到着(): スレッドが同期ポイントに到達したことを示します。これにより、内部カウンターがデクリメントされます。カウンタがゼロに達すると、待機中のすべてのスレッドが解放されます。
  • wait(): すべてのスレッドが同期ポイントに到達するまで、スレッドを同期ポイントで待機させます。
  • delivery_and_wait(): これは、arrivy() と wait() の組み合わせです。これは、スレッドが同期ポイントに到達し、すべてのスレッドが同期ポイントに到達するまで同期ポイントで待機することを示します。
  • delivery_and_drop(): これは、スレッドが同期ポイントに到達し、同期する必要がなくなったことを示します。これにより、内部カウンターがデクリメントされ、バリアの合計スレッド数が 1 つ減ります。

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;
}

この例では、同期ポイントに到達するために 3 つのスレッドを必要とするバリアを作成します。次に 3 つのスレッドを作成し、それぞれのスレッドが何らかの作業を行った後に同期ポイントに到達します。スレッド 2 が同期ポイントに到達すると、同期する必要がなくなったことを示すため、arrive_and_drop() を呼び出します。他のスレッドが同期ポイントに到達すると、arrivy_and_wait() を呼び出して待機します。すべてのスレッドが同期ポイントに到達すると、実行を続行できます。

この例では、std::barrier の柔軟性がわかります。動的スレッド数に対応できますが、これは同時プログラミングにおいて非常に重要です。

音声コミュニケーションでは、std::barrier の動作原理を次のように説明できます。「std::barrier は、同期ポイントに到着したスレッドの数をカウントすることによって機能します。スレッドが同期ポイントに到着すると、これにより、arrivy() 関数が呼び出され、カウントが減少します。カウントが 0 に達すると、待機中のすべてのスレッドが解放され、実行を継続できます。」 (std::barrier は、同期ポイントに到達したスレッドの数を追跡することによって機能します)スレッドが同期ポイントに到達すると、arrivy() 関数が呼び出され、カウンタがデクリメントされます。カウンタが 0 に達すると、待機中のすべてのスレッドが解放され、実行を続行できます。)

この文では、「によって動作する」は、何かがどのように機能するかを説明するために使用される一般的な表現です。「Keeping a count of」は、数を数えることを表す一般的な表現です。「到着する」は、場所または州に到着することを表す一般的な表現です。「関数を呼び出す」は、関数の呼び出しを説明する一般的な表現です。「カウントを減らす」は、数量またはカウントを減らすことを表す一般的な表現です。「ゼロに達する」は、量または数がゼロに達することを表す一般的な表現です。「解放される」は、解放されること、解放されることを表す一般的な表現です。「実行を継続できる」は、実行を継続または続行できることを表す一般的な表現です。

3. std::barrier と他の同期ツールの比較

並行プログラミングでは、std::mutex (ミューテックス)、std::condition_variable (条件変数)、std::latch (ドア ラッチ)、std::semaphore (セマフォ) など、使用できるさまざまな同期ツールがあります。 。この章では、std::barrier とこれらのツールを詳しく比較します。

3.1. std::barrier と std::mutex の比較

std::mutex (ミューテックス) は、共有リソースを保護するために使用される同期ツールで、複数のスレッドが同時に同じリソースにアクセスすることを防ぎます。std::barrier (バリア) は、特定の時点で複数のスレッドを同期できる同期ツールであり、すべてのスレッドがこの時点に到達するまで続行されません。

英語の口頭コミュニケーションでは通常、「ミューテックスは共有リソースを同時アクセスから保護するために使用されます」 (ミューテックスは共有リソースを同時アクセスから保護するために使用されます) と言い、std::barrier については「バリアは次のように使用されます」と言います。特定の時点で複数のスレッドを同期する」(バリアは、特定の時点で複数のスレッドを同期するために使用されます)。

どちらの文でも、これら 2 つのツールの目的を説明するために「is used to」というフレーズを使用しました。アメリカ英語では、「is used to」は、何かの目的や機能を説明するために使用される一般的な表現です。

std::mutex と 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;
}

この例では、 std::mutex を使用して共有リソースを保護し、 std::barrier を使用して 2 つのスレッドを同期します。

3.2. std::barrier と std::condition_variable の比較

std::condition_variable (条件変数) は、スレッドが別のスレッドから通知されるまで特定の条件で待機できるようにする同期ツールです。std::barrier (バリア) は、特定の時点で複数のスレッドを同期できる同期ツールであり、すべてのスレッドがこの時点に到達するまで続行されません。

英語の口頭コミュニケーションでは、通常、「条件変数は、特定の条件下でスレッドを待機させるために使用されます」 (条件変数は、特定の条件下でスレッドを待機させるために使用されます) と言い、std::barrier については、「バリア」と言います。特定の時点で複数のスレッドを同期するために使用されます」 (バリアは、特定の時点で複数のスレッドを同期するために使用されます)。

どちらの文でも、これら 2 つのツールの目的を説明するために「is used to」というフレーズを使用しました。アメリカ英語では、「is used to」は、何かの目的や機能を説明するために使用される一般的な表現です。

std::condition_variable と 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;
}

この例では、 std::condition_variable を使用して 1 つのスレッドを特定の条件で待機させ、 std::barrier を使用して 2 つのスレッドを同期します。

3.3. std::barrier と std::latch および std::semaphore の比較

std::latch (ドア ラッチ) と std::semaphore (セマフォ) は両方とも、C++20 で導入された新しい同期ツールです。std::latch を使用すると、カウントが 0 に達するまでカウントを 1 回以上デクリメントでき、その後、待機中のすべてのスレッドが通過できるようになります。std::semaphore はより一般的な同期ツールで、リソースまたはリソース セットに同時にアクセスするスレッドの数を制限できます。

std::barrier とこれら 2 つの主な違いは、std::barrier はすべてのスレッドがバリアに到達するたびに自動的にリセットされるのに対し、std::latch はリセットされないことです。さらに、 std::semaphore では任意の数のスレッドの通過を許可できますが、 std::barrier ではすべてのスレッドが到達する必要があります。

在英语口语交流中,我们通常会说 “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主页
在这里插入图片描述

おすすめ

転載: blog.csdn.net/qq_21438461/article/details/131851158