c++高性能:std多线程 thread、mutex、condition_variable future

       线程的临界区(Critical Section)是指多个线程并发执行时,访问共享资源或执行一组共享操作的那部分代码区域。在临界区中,多个线程可能会竞争资源的访问,因此必须采取适当的同步措施,以确保线程之间的安全协作和正确性。在C++中,锁是一种用于管理并发访问共享资源的机制,用于避免多个线程对同一资源造成的冲突和竞争。C++中的标准库提供了多种不同类型的锁,包括:

互斥锁(Mutex) 把共享资源包裹起来,确保任何时候只有一个线程可以访问该资源
递归锁(Recursive Mutex) 递归锁是一种特殊的互斥锁,它允许同一线程多次对同一个锁进行锁定操作。递归锁可以用std::recursive_mutex进行实现。
独占锁(Unique Lock) 独占锁是一种智能锁,它允许程序员在控制块作用域内自动获取和释放锁。独占锁可以用std::unique_lock进行实现。
条件变量(Conditional Variable) 条件变量是一种允许线程等待特定条件的机制,直到满足某些条件才允许线程在访问共享资源之前进行访问。条件变量可以用std::condition_variable进行实现。 避免忙等待(短时间等待自旋锁,长时间等待条件变量):与忙等待(busy-waiting)相比,条件变量允许线程进入休眠状态,只有在条件满足时才会被唤醒。 避免竞争条件:条件变量通常与互斥锁(mutex)一起使用,以确保在检查条件和等待条件之间的操作是原子的。这有助于防止竞争条件(race condition)和数据竞争(data race)。
读写锁(Reader-Writer Lock) 读写锁是一种可以同时支持多个读操作和单个写操作的锁,它可以提高并发性能。读写锁可以用std::shared_timed_mutex进行实现。

mutex 互斥量,互斥锁

std::mutex

  • std::mutex mtx;,mtx.lock();,mtx.unlock();
// 也可以看https://cplusplus.com/reference/mutex/mutex/
#include 
#include 
#include 

std::mutex mtx;  // 创建一个互斥锁

void print_shared_var(int& shared_var) {
    mtx.lock();  // 加锁,如果锁已经被占用,则当前线程会阻塞等待
    std::cout << "The current value of shared_var is " << shared_var << std::endl;
    // 模拟一些操作
    shared_var++;  // 对共享变量进行修改
    std::cout << "After modifying, the value of shared_var is " << shared_var << std::endl;
    mtx.unlock(); // 解锁
}

int main() {
    int shared_var = 0;
    std::thread t1(print_shared_var, std::ref(shared_var));
    std::thread t2(print_shared_var, std::ref(shared_var));
    t1.join();
    t2.join();
    return 0;
}

递归锁(Recursive Mutex)

  • 什么时候需要使用递归锁(递归mutex)?
    std::mutexstd::recursive_mutex 都是 C++ 标准库中用于实现互斥锁的类,但它们在实现方式和使用方法上有所不同。

std::mutex 不同的是,std::recursive_mutex 允许同一线程多次对它进行锁定。也就是说,如果一个线程在获得了一个递归锁后,再次尝试获取同一个递归锁,它不会被锁定,而是会继续执行。

以下是 std::mutexstd::recursive_mutex 的简单例子:

#include 
#include 

std::mutex mtx;
std::recursive_mutex rmtx;

void use_mutex(int i) {
    std::cout << "Using mutex " << i << std::endl;
    mtx.lock();
    std::cout << "Locked mutex " << i << std::endl;
    mtx.unlock();
    std::cout << "Unlocked mutex " << i << std::endl;
}

void use_recursive_mutex(int i) {
    std::cout << "Using recursive mutex " << i << std::endl;
    rmtx.lock();
    std::cout << "Locked recursive mutex " << i << std::endl;
    // Try to lock again
    rmtx.lock();
    std::cout << "Locked recursive mutex " << i << " again" << std::endl;
    rmtx.unlock();
    std::cout << "Unlocked recursive mutex " << i << " once" << std::endl;
    rmtx.unlock();
    std::cout << "Unlocked recursive mutex " << i << " twice" << std::endl;
}

int main() {
    use_mutex(1);
    use_recursive_mutex(2);
    return 0;
}

在上面的代码中,use_mutex()use_recursive_mutex() 函数都是使用锁保护代码执行过程的简单例子。在 use_mutex() 中,使用 std::mutex 对资源进行保护,而在 use_recursive_mutex() 中,则使用 std::recursive_mutex 进行保护。注意到,use_recursive_mutex() 中使用了两次 rmtx.lock() 操作,由于 std::recursive_mutex 的特性,这两次操作都会成功,不会导致线程死锁。

总之, std::mutexstd::recursive_mutex 都可以用于线程同步,但在使用时应该根据自己的需求进行选择。如果资源被多个线程访问,并且不会有递归锁定的情况,则使用 std::mutex 更为合适;如果存在需求相同资源多次访问的情况,可以考虑使用 std::recursive_mutex

std::unique_lock ,具有了自动上锁和解锁的功能

  • std::mutex mtx;
  • std::unique_lock lck(mtx);

在C++中,std::lock_guard和std::unique_lock是用于互斥锁(mutex)的两个常见的RAII(Resource Acquisition Is Initialization)类。

std::lock_guard是一个轻量级的RAII包装器,用于提供基本的互斥锁的自动上锁和解锁。它只提供了构造函数和析构函数,不能手动进行锁定和解锁操作。当std::lock_guard对象被创建时,它会自动对互斥锁进行上锁;当std::lock_guard对象离开其作用域时,会自动对互斥锁进行解锁。

使用std::lock_guard时,可以确保互斥锁在作用域结束时可靠地被解锁,避免了忘记解锁的问题,从而提高了代码的可靠性和安全性。

示例代码如下:

std::mutex mutex;

void Function()
{
    std::lock_guard<std::mutex> lock(mutex);
    // 使用互斥锁保护的代码块
}  // lock_guard在这里自动解锁互斥锁

std::unique_lock是一个更灵活的RAII包装器,与std::lock_guard相比,它提供了更多的控制和配置选项。std::unique_lock可以手动进行锁定和解锁操作,并且可以在锁定期间释放锁。

std::unique_lock提供了一些额外的功能,例如支持递归锁、条件变量等。同时,它还可以灵活地指定锁的获取策略,如std::defer_lock(延迟锁定)和std::try_to_lock(尝试锁定)等。

示例代码如下:

std::mutex mutex;

void Function()
{
    std::unique_lock<std::mutex> lock(mutex);
    // 使用互斥锁保护的代码块

    lock.unlock();  // 手动解锁
    // 在没有锁定的状态下执行其他操作

    lock.lock();  // 再次锁定
    // 继续使用互斥锁保护的代码块
}  // unique_lock在这里自动解锁互斥锁

总的来说,std::lock_guard适用于简单的互斥锁场景,提供了更简洁的语法和自动解锁的功能;而std::unique_lock更加灵活,适用于复杂的锁控制场景,提供了更多的配置和控制选项。具体使用哪个取决于特定的需求和设计。

atomic

  • std::atomic value;
  • int64_t x = 10;value.store(x,std::memory_order_relaxed)
  • int64_t x = value.load(std::memory_order_relaxed);
// atomic::load/store example
#include        // std::cout
#include          // std::atomic, std::memory_order_relaxed
#include          // std::thread

std::atomic<int> foo (0);# https://zh.cppreference.com/w/cpp/atomic/atomic !!!!

void set_foo(int x) {
  foo.store(x,std::memory_order_relaxed);     // set value atomically
}

void print_foo() {
  int x;
  do {
    x = foo.load(std::memory_order_relaxed);  // get value atomically
  } while (x==0);
  std::cout << "foo: " << x << '\n';
}

int main ()
{
  std::thread first (print_foo);
  std::thread second (set_foo,10);
  first.join();
  second.join();
  return 0;
}
 Edit & run on cpp.sh

condition_variable

  • std::condition_variable cv;
  • cv.notify_all();//cv.notify_one(); //进行唤醒
  • cv.wait(lck);//等待被唤醒
// https://cplusplus.com/reference/condition_variable/condition_variable/
// condition_variable example
#include            // std::cout
#include              // std::thread
#include               // std::mutex, std::unique_lock
#include  // std::condition_variable

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id (int id) {
  std::unique_lock<std::mutex> lck(mtx);//unique_lock具有了自动上锁和解锁的功能
  while (!ready) // if(!ready) 存在虚假唤醒
    cv.wait(lck);//等待被唤醒
  // ...
  std::cout << "thread " << id << '\n';
}

void go() {
  std::unique_lock<std::mutex> lck(mtx);
  ready = true;
  cv.notify_all();//cv.notify_one(); //进行唤醒
}

int main ()
{
  std::thread threads[10];
  // spawn 10 threads:
  for (int i=0; i<10; ++i)
    threads[i] = std::thread(print_id,i);

  std::cout << "10 threads ready to race...\n";
  go();                       // go!

  for (auto& th : threads) th.join();

  return 0;
}

promise (set and get)

  • std::promise accumulate_promise;
  • std::future accumulate_future = accumulate_promise.get_future();// 返回与承诺的结果关联的
  • accumulate_promise.set_value(sum);
  • accumulate_future.get()

simple use

// changed on https://en.cppreference.com/w/cpp/thread/promise
#include 
#include 
#include  // future object允许你取回数据,promise是让你提供数据。其中promise的声明位于头文件中
#include 
#include 
#include 

void accumulate(std::vector::iterator first,
                std::vector::iterator last,
                std::promise accumulate_promise)
{
    int sum = std::accumulate(first, last, 0);
    accumulate_promise.set_value(sum);  // Notify future
}



int main()
{
    // Demonstrate using promise to transmit a result between threads.
    std::promise accumulate_promise;
    std::future accumulate_future = accumulate_promise.get_future();// 返回与承诺的结果关联的 future,https://www.apiref.com/cpp-zh/cpp/thread/promise.html

    std::vector numbers = { 1, 2, 3, 4, 5, 6 };



    std::thread work_thread(accumulate, numbers.begin(), numbers.end(),
                            std::move(accumulate_promise));
    std::cout << "result=" << accumulate_future.get() << '\n';// wait std::promise accumulate_promise.set_value(sum);

    work_thread.join();  // wait for thread completion

}

more

#include 
#include 
#include 
#include 
#include 
#include 

void accumulate(std::vector<int>::iterator first,
                std::future<int>& last,
                std::promise<int> accumulate_promise)
{
    std::vector<int>::iterator begin = first;
    int value = last.get();
    while (*begin != value){
        begin++;
    }

    int sum = std::accumulate(first, begin, 0);
    accumulate_promise.set_value(sum);  // Notify future
}


int main()
{
    // Demonstrate using promise<int> to transmit a result between threads.
    std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };
    std::promise<int> accumulate_promise;
    std::future<int> accumulate_future = accumulate_promise.get_future();

    std::promise<int> last;      last.set_value(4);
    std::future<int> last_f = last.get_future();

    std::thread work_thread(accumulate, numbers.begin(), std::ref(last_f),
                            std::move(accumulate_promise));


    std::cout << "result=" << accumulate_future.get() << '\n';
    work_thread.join();

}

std::shared_future sharedfutureObjName; get 多次

semaphore信号量 c++20

信号量(Semaphore)是用于协调和同步多个线程之间访问共享资源的一种机制。它可以通过计数器来控制对资源的访问,即限制同时访问资源的线程数,从而避免线程间的竞争和资源的冲突。

信号量主要有两种,二元信号量(Binary Semaphore)和计数信号量(Counting Semaphore)。

二元信号量只能取两个值,0和1,通常用于控制对单个资源的访问。如果信号量的值为1,表示资源可用,线程可以获得该资源并将其设置为0;如果信号量的值为0,表示资源已经被占用,线程需要等待其他线程释放资源才能再次尝试访问。

计数信号量则可以取任何非负整数作为值,通常用于控制多个资源的访问。例如,可以使用计数信号量来限制同时执行某个任务的线程数。

信号量机制的核心是两个原子操作:P操作(Proberen,测试)和V操作(Verhogen,增加)。P操作试图对信号量进行减一并占有某个资源,如果信号量的值为0,则线程必须等待,否则就可以访问资源。V操作则将信号量的值加一,释放资源,此时其他线程可以对该资源进行占有。

信号量在操作系统中被广泛使用,可以用来控制多个进程或线程之间的同步和协调,例如在多线程程序中控制访问共享变量或共享数据结构。信号量的实现可以使用操作系统提供的系统调用或使用线程库提供的原语进行实现。

CG

  • https://www.bilibili.com/video/BV1XY41137Mh/?
  • https://cplusplus.com/reference/thread/thread/
  • 在这里插入图片描述
    c++高性能:std多线程 thread、mutex、condition_variable future_第1张图片
  • std::ref只是尝试模拟引用传递,并不能真正变成引用 thread的方法传递引用的时候,我们希望使用的是参数的引用,而不是浅拷贝,所以必须用ref来进行引用传递
#include 
#include 
#include  // future object允许你取回数据,promise是让你提供数据。其中promise的声明位于头文件中
#include 
#include 
#include 

void accumulate(std::vector::iterator first,
                std::vector::iterator last,
                std::promise& accumulate_promise)
{
    int sum = std::accumulate(first, last, 0);
    accumulate_promise.set_value(sum);  // Notify future
}



int main()
{
    // Demonstrate using promise to transmit a result between threads.
    std::promise accumulate_promise;
    std::future accumulate_future = accumulate_promise.get_future();// 返回与承诺的结果关联的 future,https://www.apiref.com/cpp-zh/cpp/thread/promise.html

    std::vector numbers = { 1, 2, 3, 4, 5, 6 };



    std::thread work_thread(accumulate, numbers.begin(), numbers.end(),
                            std::ref(accumulate_promise));
    std::cout << "result=" << accumulate_future.get() << '\n';// wait std::promise accumulate_promise.set_value(sum);

    work_thread.join();  // wait for thread completion

}

自旋锁和条件变量的使用情况

自旋锁和条件变量是两种不同的同步机制,它们在解决不同类型的问题上有各自的用途。自旋锁通常用于短期的争用情况,它不会使线程休眠,而是在一小段足时使线程休眠。因此,它们不是直接替代关系。

然而,在某些情况下,你可以将条件变量与自旋锁结合使用以实现更高级的同步需求。下面是一个示例,演示了如何使用条件变量来优化自旋锁以等待某个条件的发生:

#include 
#include 
#include 
#include 

std::mutex mtx;
std::condition_variable cv;
bool condition = false;

void worker_thread() {
    // 模拟一些工作
    std::this_thread::sleep_for(std::chrono::seconds(2));

    // 一旦工作完成,设置条件为true,并通知等待的线程
    {
        std::unique_lock<std::mutex> lock(mtx);
        condition = true;
    }
    cv.notify_one();
}

int main() {
    std::thread worker(worker_thread);

    // 自旋等待条件变为true
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        if (condition) {
            break;
        }
        lock.unlock();
        std::this_thread::yield(); // 让出CPU时间片
    }

    std::cout << "Worker thread has completed its work." << std::endl;

    worker.join();
    return 0;
}

在这个示例中,主线程使用自旋锁来等待条件变量 condition 变为 true,而不是使用条件变量的 wait 函数。自旋等待的过程中,主线程会周期性地让出CPU时间片,以避免忙等待的浪费。

这种方式的优势在于它在一定程度上结合了自旋锁和条件变量的特性,可以用于某些特殊情况下的性能优化。但要小心使用,因为它仍然是自旋等待,会消耗CPU资源,不适用于长时间的等待。此外,必须小心处理竞争条件和确保线程安全。

你可能感兴趣的:(语言学习笔记,c++,开发语言)