C++ 多线程编程

C++ 多线程编程

点击获取更多的C++学习笔记

1. 线程库的基本使用

创建线程

要创建线程,我们需要一个可调用的函数或函数对象,作为线程的入口点。在C++11中,我们可以使用函数指针、函数对象或lambda表达式来实现。创建线程的基本语法如下:

#include <thread>
std::thread t(function_name, args...);

function_name是线程入口点的函数或可调用对象

args...是传递给函数的参数

创建线程后,我们可以使用t.join()等待线程完成,或者使用t.detach()分离线程,让它在后台运行。

例如,下面的代码创建了一个线程,输出一条消息:

#include <iostream>
#include <thread>
void print_message() {    std::cout << "Hello, world!" << std::endl;
}
int main() {    std::thread t(print_message);    t.join();    return 0;
}

在这个例子中,我们定义了一个名为print_message的函数,它输出一条消息。然后,我们创建了一个名为t的线程,将print_message函数作为入口点。最后,我们使用t.join()等待线程完成。

传递参数

我们可以使用多种方式向线程传递参数,例如使用函数参数、全局变量、引用等。如:

#include <iostream>
#include <thread>
void print_message(const std::string& message) {std::cout << message << std::endl;
}
void increment(int& x) {    ++x;
}
int main() {    std::string message = "Hello, world!";    std::thread t(print_message, message);    t.join();    int x = 0;    std::thread t2(increment, std::ref(x));    t2.join();    std::cout << x << std::endl;    return 0;
}

在第一个例子中,我们使用了一个字符串作为函数参数,传递给线程。在第二个例子中,我们使用了一个引用来传递一个整数变量。需要注意的是,当我们使用引用传递参数时,我们需要使用std::ref来包装引用,否则编译器会报错。

等待线程完成

当我们创建一个线程后,我们可能需要等待它完成,以便获取线程的执行结果或执行清理操作。我们可以使用t.join()方法来等待线程完成。例如,下面的代码创建了两个线程,等待它们完成后输出一条消息:

#include <iostream>
#include <thread>
void print_message(const std::string& message) {    std::cout << message << std::endl;
}
int main() {    std::thread t1(print_message, "Thread 1");    std::thread t2(print_message, "Thread 2");    t1.join();    t2.join();    std::cout << "All threads joined" << std::endl;    return 0;
}

在这个例子中,我们创建了两个线程t1t2,它们都调用print_message函数输出一条消息。然后,我们使用t1.join()t2.join()等待它们完成。最后,我们输出一条消息,表示所有线程都已经完成。

分离线程

有时候我们可能不需要等待线程完成,而是希望它在后台运行。这时候我们可以使用t.detach()方法来分离线程。例如,下面的代码创建了一个线程,分离它后输出一条消息:

#include <iostream>
#include <thread>
void print_message(const std::string& message) {    std::cout << message << std::endl;
}
int main() {    std::thread t(print_message, "Thread 1");    t.detach();    std::cout << "Thread detached" << std::endl;    return 0;
}

在这个例子中,我们创建了一个名为t的线程,调用print_message函数输出一条消息。然后,我们使用t.detach()方法分离线程,让它在后台运行。最后,我们输出一条消息,表示线程已经被分离。

需要注意的是,一旦线程被分离,就不能再使用t.join()方法等待它完成。而且,我们需要确保线程不会在主线程结束前退出,否则可能会导致未定义行为。

joinable()

joinable()方法返回一个布尔值,如果线程可以被join()或detach(),则返回true,否则返回false。如果我们试图对一个不可加入的线程调用join()或detach(),则会抛出一个std::system_error异常。

下面是一个使用joinable()方法的例子:

#include <iostream>
#include <thread>
void foo() {std::cout << "Thread started" << std::endl;
}
int main() {std::thread t(foo);if (t.joinable()) {t.join();}std::cout << "Thread joined" << std::endl;return 0;
}

常见错误(将在后续文章中详解以下错误的****解决方案)

在使用C++11线程库时,有一些常见的错误需要注意。例如:

  • 忘记等待线程完成或分离线程:如果我们创建了一个线程,但没有等待它完成或分离它,那么在主线程结束时,可能会导致未定义行为。

  • 访问共享数据时没有同步:如果我们在多个线程中访问共享数据,但没有使用同步机制,那么可能会导致数据竞争、死锁等问题。

  • 异常传递问题:如果在线程中发生了异常,但没有处理它,那么可能会导致程序崩溃。因此,我们应该在线程中使用try-catch块来捕获异常,并在适当的地方处理它。

#include<iostream>
#include<thread>
#include<string>using namespace std;void printHello(string msg)
{cout << msg;
}int main()
{// 1. 创建线程thread thread1(printHello,"hello thread");// 因为主程序在执行的时候,是不会等待线程执行完毕,再去执行剩下的主程序,所以没有join()会报错// 主程序等待线程执行完毕 join() join函数值阻塞的//thread1.join();// 分离线程 detach()   (分离主线程和子线程)(子线程在后台继续运行)//thread1.detach();// 判断这个线程能否调用 join() 或者 detach() 返回一个bool值bool isJon  = thread1.joinable();if (isJon){thread1.join();}return 0;
}

2. 线程函数中的数据未定义的错误

1. 传递临时变量的问题:

#include <iostream>
#include <thread>
void foo(int& x) {x += 1;
}
int main() {std::thread t(foo, 1); // 传递临时变量t.join();return 0;
}
  • 在这个例子中,我们定义了一个名为foo的函数,它接受一个整数引用作为参数,并将该引用加1。然后,我们创建了一个名为t的线程,将foo函数以及一个临时变量1作为参数传递给它。这样会导致在线程函数执行时,临时变量1被销毁,从而导致未定义行为。

  • 解决方案是将变量复制到一个持久的对象中,然后将该对象传递给线程。例如,我们可以将1复制到一个int类型的变量中,然后将该变量的引用传递给线程。

#include <iostream>
#include <thread>
void foo(int& x) {x += 1;
}
int main() {int x = 1; // 将变量复制到一个持久的对象中std::thread t(foo, std::ref(x)); // 将变量的引用传递给线程t.join();return 0;
}

2. 传递指针或引用指向局部变量的问题:

#include <iostream>
#include <thread>
void foo(int* ptr) {std::cout << *ptr << std::endl; // 访问已经被销毁的指针
}
int main() {int x = 1;std::thread t(foo, &x); // 传递指向局部变量的指针t.join();return 0;
}
  • 在这个例子中,我们定义了一个名为foo的函数,它接受一个整型指针作为参数,并输出该指针所指向的整数值。然后,我们创建了一个名为t的线程,将foo函数以及指向局部变量x的指针作为参数传递给它。这样会导致在线程函数执行时,指向局部变量x的指针已经被销毁,从而导致未定义行为。

  • 解决方案是将指针或引用指向堆上的变量,或使用std::shared_ptr等智能指针来管理对象的生命周期。例如,我们可以使用new运算符在堆上分配一个整数变量,并将指针指向该变量。

#include <iostream>
#include <thread>
void foo(int* ptr) {std::cout << *ptr << std::endl;delete ptr; // 在使用完指针后,需要手动释放内存
}
int main() {int* ptr = new int(1); // 在堆上分配一个整数变量std::thread t(foo, ptr); // 将指针传递给线程t.join();return 0;
}

3. 传递指针或引用指向已释放的内存的问题:

#include <iostream>
#include <thread>
void foo(int& x) {std::cout << x << std::endl; // 访问已经被释放的内存
}
int main() {int* ptr = new int(1);std::thread t(foo, *ptr); // 传递已经释放的内存delete ptr;t.join();return 0;
}
  • 在这个例子中,我们定义了一个名为foo的函数,它接受一个整数引用作为参数,并输出该引用的值。然后,我们创建了一个名为t的线程,将foo函数以及一个已经被释放的指针所指向的整数值作为参数传递给它解决方案是确保在线程函数执行期间,被传递的对象的生命周期是有效的。例如,在主线程中创建并初始化对象,然后将对象的引用传递给线程。
#include <iostream>
#include <thread>
void foo(int& x) {std::cout << x << std::endl;
}
int main() {int x = 1;std::thread t(foo, std::ref(x)); // 将变量的引用传递给线程t.join();return 0;
}
  • 在这个例子中,我们创建了一个名为x的整数变量,并初始化为1。然后,我们创建了一个名为t的线程,将foo函数以及变量x的引用作为参数传递给它。这样可以确保在线程函数执行期间,变量x的生命周期是有效的。

4. 类成员函数作为入口函数,类对象被提前释放

错误示例:

#include <iostream>
#include <thread>class MyClass {
public:void func() {std::cout << "Thread " << std::this_thread::get_id() << " started" << std::endl;// do some workstd::cout << "Thread " << std::this_thread::get_id() << " finished" << std::endl;}
};int main() {MyClass obj;std::thread t(&MyClass::func, &obj);// obj 被提前销毁了,会导致未定义的行为return 0;
}
  • 上面的代码中,在创建线程之后,obj 对象立即被销毁了,这会导致在线程执行时无法访问 obj 对象,可能会导致程序崩溃或者产生未定义的行为。

  • 为了避免这个问题,可以使用 std::shared_ptr 来管理类对象的生命周期,确保在线程执行期间对象不会被销毁。具体来说,可以在创建线程之前,将类对象的指针封装在一个 std::shared_ptr 对象中,并将其作为参数传递给线程。这样,在线程执行期间,即使类对象的所有者释放了其所有权,std::shared_ptr 仍然会保持对象的生命周期,直到线程结束。

  • 以下是使用 std::shared_ptr 修复上面错误的示例:

#include <iostream>
#include <thread>
#include <memory>class MyClass {
public:void func() {std::cout << "Thread " << std::this_thread::get_id() << " started" << std::endl;// do some workstd::cout << "Thread " << std::this_thread::get_id() << " finished" << std::endl;}
};int main() {std::shared_ptr<MyClass> obj = std::make_shared<MyClass>();std::thread t(&MyClass::func, obj);t.join();return 0;
}
  • 上面的代码中,使用 std::make_shared 创建了一个 MyClass 类对象,并将其封装在一个 std::shared_ptr 对象中。然后,将 std::shared_ptr 对象作为参数传递给线程。这样,在线程执行期间,即使 obj 对象的所有者释放了其所有权,std::shared_ptr 仍然会保持对象的生命周期,直到线程结束。

5.入口函数为类的私有成员函数

#include <iostream>
#include <thread>class MyClass {
private:
friend void myThreadFunc(MyClass* obj);
void privateFunc(){
std::cout << "Thread " 
<< std::this_thread::get_id() << " privateFunc" << std::endl;
}
};void myThreadFunc(MyClass* obj) {
obj->privateFunc();
}int main() {
MyClass obj;
std::thread thread_1(myThreadFunc, &obj);
thread_1.join();
return 0;
}
  • 上面的代码中,将 myThreadFunc 定义为 MyClass 类的友元函数,并在函数中调用 privateFunc 函数。在创建线程时,需要将类对象的指针作为参数传递给线程。

3. 互斥量解决多线程数据共享问题

数据共享问题分析

在多个线程中共享数据时,需要注意线程安全问题。如果多个线程同时访问同一个变量,并且其中至少有一个线程对该变量进行了写操作,那么就会出现数据竞争问题。数据竞争可能会导致程序崩溃、产生未定义的结果,或者得到错误的结果。

  • 为了避免数据竞争问题,需要使用同步机制来确保多个线程之间对共享数据的访问是安全的。常见的同步机制包括互斥量、条件变量、原子操作等。

  • 以下是一个简单的数据共享问题的示例代码

#include <iostream>
#include <thread>
int shared_data = 0;
void func() {for (int i = 0; i < 100000; ++i) {shared_data++;}
}
int main() {std::thread t1(func);std::thread t2(func);t1.join();t2.join();std::cout << "shared_data = " << shared_data << std::endl;    return 0;
}
  • 上面的代码中,定义了一个名为 shared_data 的全局变量,并在两个线程中对其进行累加操作。在 main 函数中,创建了两个线程,并分别调用了 func 函数。在 func 函数中,对 shared_data 变量进行了累加操作。

  • 由于 shared_data 变量是全局变量,因此在两个线程中共享。对于这种共享的情况,需要使用互斥量等同步机制来确保多个线程之间对共享数据的访问是安全的。如果不使用同步机制,就会出现数据竞争问题,导致得到错误的结果。

如果多线程程序每一次的运行结果和单线程运行的结果始终是一样的,那么你的线程就是安全的

互斥量概念

  • 互斥量(mutex)是一种用于实现多线程同步的机制,用于确保多个线程之间对共享资源的访问互斥。互斥量通常用于保护共享数据的访问,以避免多个线程同时访问同一个变量或者数据结构而导致的数据竞争问题。

  • 互斥量提供了两个基本操作:lock()unlock()。当一个线程调用 lock() 函数时,如果互斥量当前没有被其他线程占用,则该线程获得该互斥量的所有权,可以对共享资源进行访问。如果互斥量当前已经被其他线程占用,则调用 lock() 函数的线程会被阻塞,直到该互斥量被释放为止。

  • 上面的代码中,使用互斥量 mtx 来确保多个线程对 shared_data 变量的访问是安全的。在 func 函数中,先调用 mtx.lock() 来获取互斥量的所有权,然后对 shared_data 变量进行累加操作,最后再调用 mtx.unlock() 来释放互斥量的所有权。这样就可以确保多个线程之间对 shared_data 变量的访问是安全的。

案例代码

  • 以下是一个综合了创建多个线程和数据共享问题解决方案的示例代码:
#include <iostream>
#include <thread>
#include <mutex>
int shared_data = 0;
std::mutex mtx;
void func(int n) {for (int i = 0; i < 100000; ++i) {mtx.lock();shared_data++;        std::cout << "Thread " << n << " increment shared_data to " << shared_data << std::endl;mtx.unlock();}
}
int main() {std::thread t1(func, 1);std::thread t2(func, 2);t1.join();t2.join();    std::cout << "Final shared_data = " << shared_data << std::endl;    return 0;
}
  • 上面的代码中,定义了一个名为 shared_data 的全局变量,并使用互斥量 mtx 来确保多个线程对其进行访问时的线程安全。在两个线程中,分别调用了 func 函数,并传递了不同的参数。在 func 函数中,先获取互斥量的所有权,然后对 shared_data 变量进行累加操作,并输出变量的当前值。最后再释放互斥量的所有权。

4. 互斥量死锁

  • 假设有两个线程 T1 和 T2,它们需要对两个互斥量 mtx1 和 mtx2 进行访问,而且需要按照以下顺序获取互斥量的所有权:

T1 先获取 mtx1 的所有权,再获取 mtx2 的所有权。

T2 先获取 mtx2 的所有权,再获取 mtx1 的所有权。

  • 如果两个线程同时执行,就会出现死锁问题。因为 T1 获取了 mtx1 的所有权,但是无法获取 mtx2 的所有权,而 T2 获取了 mtx2 的所有权,但是无法获取 mtx1 的所有权,两个线程互相等待对方释放互斥量,导致死锁。

  • 为了解决这个问题,可以让两个线程按照相同的顺序获取互斥量的所有权。例如,都先获取 mtx1 的所有权,再获取 mtx2 的所有权,或者都先获取 mtx2 的所有权,再获取 mtx1 的所有权。这样就可以避免死锁问题。

以下是按照第二种方案修改后的代码:

#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1, mtx2;
void func1() {    mtx2.lock();    std::cout << "Thread 1 locked mutex 2" << std::endl;    mtx1.lock();    std::cout << "Thread 1 locked mutex 1" << std::endl;    mtx1.unlock();    std::cout << "Thread 1 unlocked mutex 1" << std::endl;    mtx2.unlock();    std::cout << "Thread 1 unlocked mutex 2" << std::endl;
}
void func2() {    mtx2.lock();    std::cout << "Thread 2 locked mutex 2" << std::endl;    mtx1.lock();    std::cout << "Thread 2 locked mutex 1" << std::endl;    mtx1.unlock();    std::cout << "Thread 2 unlocked mutex 1" << std::endl;    mtx2.unlock();    std::cout << "Thread 2 unlocked mutex 2" << std::endl;
}
int main() {    std::thread t1(func1);    std::thread t2(func2);    t1.join();    t2.join();    return 0;
}

在上面的代码中,T1 先获取 mtx2 的所有权,再获取 mtx1 的所有权,而 T2 也是先获取 mtx2 的所有权,再获取 mtx1 的所有权,这样就避免了死锁问题。

5.lock_guard 与 std::unique_lock

lock_guard

std::lock_guard 是 C++ 标准库中的一种互斥量封装类,用于保护共享数据,防止多个线程同时访问同一资源而导致的数据竞争问题。

std::lock_guard 的特点如下:

  • 当构造函数被调用时,该互斥量会被自动锁定。
  • 当析构函数被调用时,该互斥量会被自动解锁。
  • std::lock_guard 对象不能复制或移动,因此它只能在局部作用域中使用。

std::unique_lock

std::unique_lock 是 C++ 标准库中提供的一个互斥量封装类,用于在多线程程序中对互斥量进行加锁和解锁操作。它的主要特点是可以对互斥量进行更加灵活的管理,包括延迟加锁、条件变量、超时等。

std::unique_lock 提供了以下几个成员函数:

  • lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。
  • try_lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回 false,否则返回 true
  • try_lock_for(const std::chrono::duration<Rep, Period>& rel_time)尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间。
  • try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点。
  • unlock():对互斥量进行解锁操作。

除了上述成员函数外,std::unique_lock 还提供了以下几个构造函数:

  • unique_lock() noexcept = default:默认构造函数,创建一个未关联任何互斥量的 std::unique_lock 对象。
  • explicit unique_lock(mutex_type& m):构造函数,使用给定的互斥量 m 进行初始化,并对该互斥量进行加锁操作。
  • unique_lock(mutex_type& m, defer_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,但不对该互斥量进行加锁操作。
  • unique_lock(mutex_type& m, try_to_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,并尝试对该互斥量进行加锁操作。如果加锁失败,则创建的 std::unique_lock 对象不与任何互斥量关联。
  • unique_lock(mutex_type& m, adopt_lock_t) noexcept:构造函数,使用给定的互斥量 m 进行初始化,并假设该互斥量已经被当前线程成功加锁。

std::unique_lock 使用非常灵活方便,上述操作的使用方式将会在课程视频中作详细介绍。

#include <iostream>
#include <thread>
#include <mutex>
int shared_data = 0;
//std::mutex mtx;
std::timed_mutex mtx;
void func(int n) {for (int i = 0; i < 2; ++i) {//std::lock_guard < std::mutex > lg(mtx);std::unique_lock < std::timed_mutex > lg(mtx, std:: defer_lock);if (lg.try_lock_for(std::chrono::seconds(2))) {std::this_thread::sleep_for(std::chrono::seconds(1));shared_data++;}}
}
int main() {std::thread t1(func, 1);std::thread t2(func, 2);t1.join();t2.join();std::cout << "Final shared_data = " << shared_data << std::endl;return 0;
}

6.std::call_once与其使用场景

单例设计模式是一种常见的设计模式,用于确保某个类只能创建一个实例。由于单例实例是全局唯一的,因此在多线程环境中使用单例模式时,需要考虑线程安全的问题。

call_once 只能在线程函数中使用

下面是一个简单的单例模式的实现:

class Singleton {
public:static Singleton& getInstance() {static Singleton instance;return instance;}    void setData(int data) {m_data = data;}    int getData() const {return m_data;}private:Singleton() {}Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;    int m_data = 0;
};
  • 在这个单例类中,我们使用了一个静态成员函数 getInstance() 来获取单例实例,该函数使用了一个静态局部变量 instance 来存储单例实例。由于静态局部变量只会被初始化一次,因此该实现可以确保单例实例只会被创建一次。

但是,该实现并不是线程安全的。如果多个线程同时调用 getInstance() 函数,可能会导致多个对象被创建,从而违反了单例模式的要求。此外,如果多个线程同时调用 setData() 函数来修改单例对象的数据成员 m_data,可能会导致数据不一致或不正确的结果。

  • 为了解决这些问题,我们可以使用 std::call_once 来实现一次性初始化,从而确保单例实例只会被创建一次。下面是一个使用 std::call_once 的单例实现:
class Singleton {
public:static Singleton& getInstance() {std::call_once(m_onceFlag, &Singleton::init);return *m_instance;}    void setData(int data) {m_data = data;}    int getData() const {        return m_data;}
private:Singleton() {}Singleton(const Singleton&) = delete;Singleton& operator=(const Singleton&) = delete;    static void init() {m_instance.reset(new Singleton);}    static std::unique_ptr<Singleton> m_instance;    static std::once_flag m_onceFlag;    int m_data = 0;
};
std::unique_ptr<Singleton> Singleton::m_instance;
std::once_flag Singleton::m_onceFlag;
  • 在这个实现中,我们使用了一个静态成员变量 m_instance 来存储单例实例,使用了一个静态成员变量 m_onceFlag 来标记初始化是否已经完成。在 getInstance() 函数中,我们使用 std::call_once 来调用 init() 函数,确保单例实例只会被创建一次。在 init() 函数中,我们使用了 std::unique_ptr 来创建单例实例。

  • 使用 std::call_once 可以确保单例实例只会被创建一次,从而避免了多个对象被创建的问题。此外,使用 std::unique_ptr 可以确保单例实例被正确地释放,避免了内存泄漏的问题。

  • std::call_once 是 C++11 标准库中的一个函数,用于确保某个函数只会被调用一次。其函数原型如下

template<class Callable, class... Args>void call_once(std::once_flag& flag, Callable&& func, Args&&... args);

其中,flag 是一个 std::once_flag 类型的对象,用于标记函数是否已经被调用;func 是需要被调用的函数或可调用对象;args 是函数或可调用对象的参数。

  • std::call_once 的作用是,确保在多个线程中同时调用 call_once 时,只有一个线程能够成功执行 func 函数,而其他线程则会等待该函数执行完成。

  • 使用 std::call_once 的过程中,需要注意以下几点:

  1. flag 参数必须是一个 std::once_flag 类型的对象,并且在多次调用 call_once 函数时需要使用同一个 flag 对象。

  2. func 参数是需要被调用的函数或可调用对象。该函数只会被调用一次,因此应该确保该函数是幂等的。

  3. args 参数是 func 函数或可调用对象的参数。如果 func 函数没有参数,则该参数可以省略。

  4. std::call_once 函数会抛出 std::system_error 异常,如果在调用 func 函数时发生了异常,则该异常会被传递给调用者。

使用 std::call_once 可以在多线程环境中实现一次性初始化,避免了多个线程同时初始化的问题。例如,在单例模式中,可以使用 std::call_once 来保证单例实例只会被创建一次。

#include <iostream>
#include <thread>
#include <mutex>
#include<string>using namespace std;class Log {
public:Log() { };Log(const Log& log) = delete;Log& operator=(const Log& log) = delete;static Log& GetInstance() {//static Log log; // 懒汉模式//return log;// 饿汉模式  提前不声明对象,需要的时候在newstatic Log* log = nullptr;if (!log) log = new Log;return *log;}void PrintfLog(string msg){cout << __TIME__ << "\t" << msg << endl;}
};int main() {Log::GetInstance().PrintfLog("error");return 0;
}

call_once

#include <iostream>
#include <thread>
#include <mutex>
#include<string>using namespace std;
static once_flag once;
static Log* log = nullptr;
class Log {
public:Log() { };Log(const Log& log) = delete;Log& operator=(const Log& log) = delete;static Log& GetInstance() {//static Log log; // 懒汉模式//return log;// 饿汉模式  提前不声明对象,需要的时候在newif (!log) log = new Log;call_once(once, init);return *log;}static void init() {if (!log) log = new Log;}void PrintfLog(string msg){cout << __TIME__ << "\t" << msg << endl;}
};
void print_error() {Log::GetInstance().PrintfLog("error");
}
int main() {//因为t1和t2是并行的状态,会new两次log——解决方法:call_oncethread t1(print_error);thread t2(print_error);t1.join();t2.join();return 0;
}

7.condition_variable 与其使用场景

std::condition_variable 的步骤如下:

  1. 创建一个 std::condition_variable 对象。

  2. 创建一个互斥锁 std::mutex 对象,用来保护共享资源的访问。

  3. 在需要等待条件变量的地方

    使用 std::unique_lock<std::mutex> 对象锁定互斥锁

    并调用 std::condition_variable::wait()std::condition_variable::wait_for()std::condition_variable::wait_until() 函数等待条件变量。

  4. 在其他线程中需要通知等待的线程时,调用 std::condition_variable::notify_one()std::condition_variable::notify_all() 函数通知等待的线程。

生产者与消费者模型

在这里插入图片描述

  • 下面是一个简单的生产者-消费者模型的案例,其中使用了 std::condition_variable 来实现线程的等待和通知机制:
#include <iostream>
#include <thread>
#include <mutex>
#include<string>
#include<condition_variable>
#include<queue>using namespace std;queue<int> g_queue;
condition_variable g_cv;
mutex mtx;
void Producer() {for (int i = 0; i < 10; i++){{unique_lock<mutex> lock(mtx);g_queue.push(i);// 通知消费者来取任务g_cv.notify_one();cout << "Producer: " << i << endl;}this_thread::sleep_for(chrono::microseconds(100));}
}void Consumer() {while (1){unique_lock<mutex> lock(mtx);// 如果队列为空  就要等待g_cv.wait(lock, []() {return !g_queue.empty(); });int value = g_queue.front();g_queue.pop();cout << "Consumer: " << value << endl;}
}int main()
{thread producer_thread(Producer);thread consumer_thread(Consumer);producer_thread.join();consumer_thread.join();return 0;
}

使用 std::condition_variable 可以实现线程的等待和通知机制,从而在多线程环境中实现同步操作。在生产者-消费者模型中,使用 std::condition_variable 可以让消费者线程等待生产者线程生产数据后再进行消费,避免了数据丢失或者数据不一致的问题。

8. C++11 跨平台线程池

跨平台线程池实现

在这里插入图片描述

它使用 C++11 标准库中的 std::thread、std::mutex、std::condition_variable、std::function 和 std::queue 等组件实现。

#include <iostream>
#include <thread>
#include <mutex>
#include<string>
#include<condition_variable>
#include<queue>
#include<vector>
#include<functional>using namespace std;class ThreadPool
{
public:ThreadPool(int numThreads) :stop(false) {for (int i = 0; i < numThreads; i++){threads.emplace_back([this] {while (true){unique_lock<mutex> lock(mtx);condition.wait(lock, [this] {return !tasks.empty() || stop;});if (stop && tasks.empty()) {return;}// move移动语义function<void()> task(move(tasks.front())); // queue最左边的值tasks.pop();lock.unlock();task();}});}}~ThreadPool() {{unique_lock<mutex> lock(mtx);stop = true;}condition.notify_all();for (auto& t : threads) {t.join();}}template<class F,class... Args>// && 右值引用  &左值引用// 在函数模板加 && 是万能引用void enqueue(F &&f,Args&&... args) {// bind函数适配器(函数和参数绑定到一起)// forward 完美转发function<void()>task = bind(forward<F>(f), forward<Args>(args)...);{unique_lock<mutex> local(mtx);tasks.emplace(move(task));}condition.notify_one();}
private: vector<thread> threads;queue<function<void()>> tasks;mutex mtx;condition_variable condition;bool stop;
};int main()
{ThreadPool pool(4);for (int i = 0; i < 10; i++){pool.enqueue([i] {cout << "task: " << i << " is running" << endl;this_thread::sleep_for(chrono::seconds(1));cout << "task: " << i << " is done" << endl;});}return 0;
}
  • 在这个示例中,我们同样定义了一个 ThreadPool 类,并且在构造函数中创建了指定数目的线程。在每个线程中,我们不断地从任务队列中获取任务并执行,直到线程池被停止。在 enqueue() 函数中,我们将任务封装成一个 std::function 对象,并将它添加到任务队列中。在 ThreadPool 的析构函数中,我们等待所有线程执行完成后再停止所有线程。

在主函数中,我们创建了一个 ThreadPool 对象,并向任务队列中添加了 8 个任务。每个任务会输出一些信息,并且在执行完后等待 1 秒钟。由于线程池中有 4 个线程,因此这 8 个任务会被分配到不同的线程中执行。在任务执行完成后,程序会退出。

9.异步并发—— async future packaged_task promise

1. async 、 future

是C++11引入的一个函数模板,用于异步执行一个函数,并返回一个std::future对象,表示异步操作的结果。使用std::async可以方便地进行异步编程,避免了手动创建线程和管理线程的麻烦。下面是一个使用std::async的案例:

#include <iostream>
#include <future>
int calculate() {// 模拟一个耗时的计算std::this_thread::sleep_for(std::chrono::seconds(1));return 42;
}
int main() {std::future<int> future_result = std::async(std::launch::async, calculate);// 在这里可以做其他的事情int result = future_result.get(); // 获取异步操作的结果std::cout << result << std::endl; // 输出42return 0;
}
  • 这个例子中,我们使用std::async函数异步执行了一个耗时的计算,这个计算可以在另一个线程中执行,不会阻塞主线程。同时,我们也避免了手动创建线程和管理线程的麻烦。

2. packaged_task

  • 在C++中,packaged_task是一个类模板,用于将一个可调用对象(如函数、函数对象或Lambda表达式)封装成一个异步操作,并返回一个std::future对象,表示异步操作的结果。packaged_task可以方便地将一个函数或可调用对象转换成一个异步操作,供其他线程使用。

  • 以下是packaged_task的基本用法:

  1. 定义可调用对象
int calculate(int x, int y) {return x + y;
}

这里定义了一个函数calculate,用于将两个整数相加。

  1. 创建packaged_task对象
std::packaged_task<int(int, int)> task(calculate);
std::future<int> future_result = task.get_future();

这里创建了一个packaged_task对象,将函数calculate封装成异步操作,并返回一个std::future对象,表示异步操作的结果。

  1. 在其他线程中执行异步操作
std::thread t(std::move(task), 1, 2);
t.join();

这里创建了一个新的线程,并在这个线程中执行异步操作。由于packaged_task对象是可移动的,因此需要使用std::move()函数将task对象转移至新线程中执行。

  1. 获取异步操作的结果
int result = future_result.get();
std::cout << result << std::endl; // 输出3
  • 在主线程中,我们可以使用future_result.get()方法获取异步操作的结果,并输出到控制台。

    在这个例子中,我们成功地将一个函数calculate封装成了一个异步操作,并在其他线程中执行。通过packaged_task和future对象,我们可以方便地实现异步编程,使得代码更加简洁和易于维护。

3. promise

  • 在C++中,promise是一个类模板,用于在一个线程中产生一个值,并在另一个线程中获取这个值。promise通常与future和async一起使用,用于实现异步编程。

  • 以下是promise的基本用法:

  1. 创建promise对象
std::promise<int> p;

这里创建了一个promise对象,用于产生一个整数值。

  1. 获取future对象
std::future<int> f = p.get_future();

通过promise对象的get_future()方法,可以获取与之关联的future对象,用于在另一个线程中获取promise对象产生的值。

  1. 在其他线程中设置值
std::thread t([&p]() {p.set_value(42);
});
t.join();

这里创建了一个新的线程,并在这个线程中,使用promise对象的set_value()方法设置一个整数值42。

  1. 在主线程中获取值
int result = f.get();
std::cout << result << std::endl; // 输出42
  • 在主线程中,我们可以使用future对象的get()方法获取promise对象产生的值,并输出到控制台。

在这个例子中,我们成功地使用promise和future对象实现了跨线程的值传递。通过promise和future对象,我们可以方便地实现异步编程,避免了手动创建线程和管理线程的麻烦。

#include<iostream>
#include<future>
using namespace std;void func(promise<int>& f) {f.set_value(1000);
}int main() {//future<int> future_res = async(launch::async, func);// 相当于会[自动开启]一个现成进行运行这个函数//packaged_task<int()>  task(func);//auto future_res = task.get_future();手动开辟线程move左值转换成右值因为packaged_task是一个可移动对象//thread t1(move(task));//cout << func() << endl;// 在主线程中运行//t1.join();//cout << future_res.get() << endl;promise<int> f;auto future_res = f.get_future();thread t1(func, ref(f));t1.join();cout << future_res.get() << endl;return 0;
}

10. std::atomic 原子操作

  • std::atomic 是 C++11 标准库中的一个模板类,用于实现多线程环境下的原子操作。它提供了一种线程安全的方式来访问和修改共享变量,可以避免多线程环境中的数据竞争问题。

  • std::atomic 的使用方式类似于普通的 C++ 变量,但是它的操作是原子性的。也就是说,在多线程环境下,多个线程同时对同一个 std::atomic 变量进行操作时,不会出现数据竞争问题。

  • 以下是一些常用的 std::atomic 操作:

    1. load():将 std::atomic 变量的值加载到当前线程的本地缓存中,并返回这个值。

    2. store(val):将 val 的值存储到 std::atomic 变量中,并保证这个操作是原子性的。

    3. exchange(val):将 val 的值存储到 std::atomic 变量中,并返回原先的值。

    4. compare_exchange_weak(expected, val)compare_exchange_strong(expected, val):比较 std::atomic 变量的值和 expected 的值是否相同,如果相同,则将 val 的值存储到 std::atomic 变量中,并返回 true;否则,将 std::atomic 变量的值存储到 expected 中,并返回 false

以下是一个示例,演示了如何使用 std::atomic 进行原子操作:

#include <atomic>
#include <iostream>
#include <thread>
std::atomic<int> count = 0;
void increment() {for (int i = 0; i < 1000000; ++i) {count++;}
}
int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << count << std::endl;return 0;
}

在这个示例中,我们定义了一个 std::atomic<int> 类型的变量 count,并将其初始化为 0。然后,我们启动两个线程分别执行 increment 函数,这个函数的作用是将 count 变量的值加一,执行一百万次。最后,我们在主线程中输出 count 变量的值。由于 count 变量是一个 std::atomic 类型的变量,因此对它进行操作是原子性的,不会出现数据竞争问题。在这个示例中,最终输出的 count 变量的值应该是 2000000

#include <atomic>
#include <iostream>
#include <thread>
using namespace std;std::atomic<int> share_data = 0;
void increment() {for (int i = 0; i < 1000000; ++i) {share_data++;}
}
int main() {auto last = chrono::duration_cast<chrono::microseconds>(chrono::system_clock::now().time_since_epoch()).count();std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();share_data.store(1);// 赋值保证是原子性auto cur = chrono::duration_cast<chrono::microseconds>(chrono::system_clock::now().time_since_epoch()).count();cout << cur-last << std::endl;cout << share_data << endl;return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/110484.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java.8 - java -overrideoverload 重写和重载

重写(Override) 重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变&#xff0c;核心重写&#xff01; 重写的好处在于子类可以根据需要&#xff0c;定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。 重写方法不…

如何判断一个java对象还活着

引用计数算法 引用计数器的算法是这样的&#xff1a;在对象中添加一个引用计数器&#xff0c;每当有一个地方引用它时&#xff0c;计数器值就加一&#xff1b;当引用失效时&#xff0c;计数器值就减一&#xff1b;任何时刻计数器为零的对象就是不可能再被使用的。 缺点&#x…

微软 Visual Studio 现已内置 Markdown 编辑器,可直接修改预览 .md 文件

Visual Studio Code V1.66.0 中文版 大小&#xff1a;75.30 MB类别&#xff1a;文字处理 本地下载 Markdown 是一种轻量级标记语言&#xff0c;当开发者想要格式化代码但又不想牺牲易读性时&#xff0c;Markdown 是一个很好的解决方案&#xff0c;比如 GitHub 就使用 Markdo…

深度学习1.卷积神经网络-CNN

目录 卷积神经网络 – CNN CNN 解决了什么问题&#xff1f; 需要处理的数据量太大 保留图像特征 人类的视觉原理 卷积神经网络-CNN 的基本原理 卷积——提取特征 池化层&#xff08;下采样&#xff09;——数据降维&#xff0c;避免过拟合 全连接层——输出结果 CNN …

【Go 基础篇】深入探索:Go语言中的二维数组

在计算机编程中&#xff0c;数组是一种基本的数据结构&#xff0c;用于存储相同类型的元素。而二维数组作为数组的一种扩展&#xff0c;允许我们以类似表格的方式存储和处理数据。在Go语言中&#xff0c;二维数组是一个重要的概念&#xff0c;本文将深入探讨Go语言中的二维数组…

CentOs下面安装jenkins记录

目录 一、安装jenkins 二、进入jenkins 三、安装和Gitee&#xff0c;Maven等插件 一、安装jenkins 1 wget -O /etc/yum.repos.d/jenkins.repo \ https://pkg.jenkins.io/redhat-stable/jenkins.repo 2 rpm --import https://pkg.jenkins.io/redhat-stable/…

javacv基础02-调用本机摄像头并预览摄像头图像画面视频

引入架包&#xff1a; <dependency><groupId>org.openpnp</groupId><artifactId>opencv</artifactId><version>4.5.5-1</version></dependency><dependency><groupId>org.bytedeco</groupId><artifactId…

Spark最后一课

1.Spark的提交过程(YarnCluster) 1.命令输入脚本启动,启动submit任务 2.解析参数 看是cluster还是yarn单点模式 3.创建客户端YarnClusterApplication 4.封装提交命令交给RM 5.RM在NM上启动ApplicationMaster(AM) 注意AM消耗的资源都是container的 6.AM根据参数启动Driver并且…

mac使用VsCode远程连接服务器总是自动断开并要求输入密码的解决办法

在mac中使用vscode远程连接服务器&#xff0c;时常会出现自动断开并要求重新输入服务器密码的问题&#xff0c;接下来让我们来解决它&#xff1a; 1、首先&#xff0c;在本地创建公钥&#xff1a; ssh-keygen 这条命令执行之后&#xff0c;出现提示直接回车即可&#xff1b;直…

【数学建模】清风数模正课5 相关性分析

相关系数 相关性分析的关键是计算相关系数&#xff0c;在本节课中将会介绍两种常用的相关系数&#xff1a;皮尔逊相关系数&#xff08;Pearson&#xff09;和斯皮尔曼相关系数&#xff08;Spearman&#xff09;。 它们可以用来衡量两个变量间相关性的大小&#xff0c;对于不同…

.jar中没有主清单属性【已解决】

原因 对jar解压缩&#xff0c;可以看到有一个MANIFEST.MF文件&#xff0c;此文件就是jar运行时要查找的清单目录。 主清单数据&#xff0c;就是我们要运行的主类即程序入口&#xff0c;缺少主清单属性&#xff0c;就不知道从哪开始运行。 因此我们需要对项目进行配置&#xff…

C++元编程——深度双向RNN实验

使用C的标准库实现了双向RNN的功能。最近对DRNN做了一些改进&#xff0c;同时进行了实验&#xff0c;首先DRNN的代码如下&#xff1a; #ifndef _RNN_HPP_ #define _RNN_HPP_ #include <stdio.h> #include <stdlib.h> #include <vector> #include "mat.…

jmeter+ant+jenkins接口自动化测试框架

大致思路&#xff1a;Jmeter可以做接口测试&#xff0c;也能做压力测试&#xff0c;而且是开源软件&#xff1b;Ant是基于Java的构建工具&#xff0c;完成脚本执行并收集结果生成报告&#xff0c;可以跨平台&#xff0c;Jenkins是持续集成工具。将这三者结合起来可以搭建一套We…

IET独立出版 | EI检索 | 2023年第三届机械、航空航天与汽车工程国际会议

会议简介 Brief Introduction 2023年第三届机械、航空航天与汽车工程国际会议&#xff08;CMAAE 2023&#xff09; 会议时间&#xff1a;2023年12月8 -10日 召开地点&#xff1a;中国南京 大会官网&#xff1a;www.cmaae.org 航天是当今世界最具挑战性和广泛带动性的高技术领域…

Lua基础知识

文章目录 1. Lua简介1.1 设计目的&#xff1a;1.2 特性1.3 应用场景 2. Lua脚本学习2.1 安装2.2 lua操作2.3 lua案例 学习lua主要是为了后续做高性能缓存架构所准备的基础技术。可以先了解下基础&#xff0c;在实际使用时&#xff0c;再查缺补漏。 1. Lua简介 Lua 是一种轻量小…

八路参考文献:[八一新书]许少辉.乡村振兴战略下传统村落文化旅游设计[M]北京:中国建筑工业出版社,2022.

八路参考文献&#xff1a;&#xff3b;八一新书&#xff3d;许少辉&#xff0e;乡村振兴战略下传统村落文化旅游设计&#xff3b;&#xff2d;&#xff3d;北京&#xff1a;中国建筑工业出版社&#xff0c;&#xff12;&#xff10;&#xff12;&#xff12;&#xff0e;

Python直接变快五倍?最新的优化解释器和内存管理

来自公众号&#xff1a;OSC开源社区 2020 年秋&#xff0c;CPython 核心开发者 Mark Shannon 提出了关于 Python 的几个性能改进&#xff0c;这个提议被称为 “香农计划” (Shannon Plan)。 Shannon 随后创建了 Faster Cpython 项目&#xff0c;他希望在 4 年的时间里&#xff…

Spring boot如何工作

越来越方便了 java技术生态发展近25年&#xff0c;框架也越来越方便使用了&#xff0c;简直so easy&#xff01;&#xff01;&#xff01;我就以Spring衍生出的Spring boot做演示&#xff0c;Spring boot会让你开发应用更快速。 快速启动spring boot 请参照官网 Spring | Quic…

银河麒麟V10(Tercel)服务器版安装 Docker

一、服务器环境 ## 查看系统版本&#xff0c;确认版本 cat /etc/kylin-release Kylin Linux Advanced Server release V10 (Tercel)## 操作系统 uname -p aarch64## 内核版本&#xff08;≥ 3.10&#xff09; uname -r 4.19.90-21.2.ky10.aarch64## iptables 版本&#xff08;…

Mysql--技术文档--B树-数据结构的认知

阿丹解读&#xff1a; B树&#xff08;B tree&#xff09;和B树&#xff08;B-tree&#xff09;都是常见的自平衡搜索树数据结构&#xff0c;用于在存储和检索大量数据时提供高效的操作。 基本概念-B树/B树 B树&#xff08;B-tree&#xff09;和B树&#xff08;B tree&#x…