c++11 多线程编程

多线程编程

线程库的基本使用

本文详细介绍C++11 Thead线程库的基本使用,包括如何创建线程、启动线程、等待线程完成以及如何分离线程等。

基本概念

  • 进程: 运行中的程序
  • 线程: 进程中的进程
  • 线程的最大数量取决于cpu的核心数

创建线程

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

#include <thread>std::thread t(function_name, args...);
function_name: 线程入口点的函数或可调用对象
args: 传递给函数的参数

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

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

#include <iostream>
#include <thread>
#include <string>

void print_message(std::string msg)
{
    std::cout << msg << std::endl;
    return;
}

int main()
{
    // 创建线程
    std::thread t(print_message, "Hello C++ Thread");

    // 主线程等待子线程结束再结束 join()
    t.join(); 
    
    return 0;
}

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

传递参数

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

#include <iostream>
#include <thread>
#include <string>

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>
#include <string>

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块来捕获异常,并在适当的地方处理它。

总结

C++11提供了一个强大的线程库,即std::thread。它可以在C++程序中创建和管理线程,提供了一种更加现代化的方式来处理多线程编程。在本文中,我们介绍了std::thread库的基本使用,包括如何创建、启动和管理线程,以及如何等待线程完成和分离线程。同时,我们也提到了一些常见的错误,需要注意避免。

线程函数中数据未定义错误

  • 传递临时变量的问题
#include <iostream>
#include <thread>

void foo(int& x)
{
    ++x;
}

int main()
{
    std::thread t(foo, 1); // 传递临时变量
    t.join();
    std::cout << "x = " << x << std::endl;
    return 0;
}

在这个例子中,我们定义了一个名为foo的函数,它接受一个整数引用作为参数,并将该引用加1。然后,我们创建了一个名为t的线程,将foo函数以及一个临时变量1作为参数传递给它。这样会导致在线程函数执行时,临时变量1被销毁,从而导致未定义行为。

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

#include <iostream>
#include <thread>

void foo(int& x)
{
    x++;
}

int main()
{   
    int x = 1;
    std::thread t(foo, std::ref(x)); // 创建线程,并传入x的引用
    t.join(); // 等待线程结束
    std::cout << x << std::endl;
    return 0;
}
  • 传递指针或引用指向局部变量的问题
#include <iostream>
#include <thread>

std::thread t;
void foo(int* p)
{
    std::cout << *p << std::endl;
}

void test()
{
    int x = 1;
    t = std::thread(foo, &x);
}

int main()
{
    test();
    t.join();
    return 0;
}

在这个例子中,我们定义了一个名为foo的函数,它接受一个整型指针作为参数,并输出该指针所指向的整数值。然后,我们创建了一个名为t的线程,将foo函数以及指向局部变量x的指针作为参数传递给它。这样会导致在线程函数执行时,指向局部变量x的指针已经被销毁,从而导致未定义行为。

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

#include <iostream>
#include <thread>

std::thread t;
void foo(int* p)
{
    std::cout << *p << std::endl;
    delete p;
}

void test()
{
    int *x = new int(1);
    t = std::thread(foo, x);
}

int main()
{
    test();
    t.join();
    return 0;
}
  • 传递指针或引用指向已释放的内存的问题
#include <iostream>
#include <thread>

void foo(int& x)
{
    std::cout << x << std::endl;
}

int main()
{
    int *x = new int(1);
    std::thread t(foo, *x); // 传递已经释放掉的内存
    delete x;

    t.join();
    return 0;
}

在这个例子中,我们定义了一个名为foo的函数,它接受一个整数引用作为参数,并输出该引用的值。然后,我们创建了一个名为t的线程,将foo函数以及一个已经被释放的指针所指向的整数值作为参数传递给它

解决方案是确保在线程函数执行期间,被传递的对象的生命周期是有效的。例如,在主线程中创建并初始化对象,然后将对象的引用传递给线程。

#include <iostream>
#include <thread>

void foo(int &x)
{
    x++;
}

int main()
{
    int x = 1;
    std::thread t(foo, std::ref(x));
    t.join();
    std::cout << x << std::endl;
    return 0;
}

在这个例子中,我们创建了一个名为x的整数变量,并初始化为1。然后,我们创建了一个名为t的线程,将foo函数以及变量x的引用作为参数传递给它。这样可以确保在线程函数执行期间,变量x的生命周期是有效的。

  • 类成员函数作为入口函数,类对象被提前释放
#include <iostream>
#include <thread>

class MyClass
{
public:
    void func()
    {
        std::cout << "Hello, World!" << std::endl;
    }
};

int main()
{
    MyClass obj;
    std::thread t(&MyClass::func, &obj);
    // 等待线程结束
    t.join();
    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 << "start : " << std::this_thread::get_id() << std::endl;
            std::this_thread::sleep_for(std::chrono::seconds(1));   
            std::cout << "end : " << std::this_thread::get_id() << std::endl;
        }
};

int main()
{
    std::shared_ptr<MyClass> p = std::make_shared<MyClass>();
    std::thread t(&MyClass::func, p);
    t.join();
    return 0;
}

上面的代码中,使用 std::make_shared 创建了一个 MyClass 类对象,并将其封装在一个 std::shared_ptr 对象中。然后,将 std::shared_ptr 对象作为参数传递给线程。这样,在线程执行期间,即使 obj 对象的所有者释放了其所有权,std::shared_ptr 仍然会保持对象的生命周期,直到线程结束。

  • 入口函数为类的私有成员函数
#include <iostream>
#include <thread>

class MyClass{
    friend void test();
private:
    void func(){
        std::cout << "Hello from thread" << std::endl;
    }
};

void test(){
    std::shared_ptr<MyClass> p = std::make_shared<MyClass>();
    std::thread t(&MyClass::func, p);
    t.join();
}

int main()
{
    test();
    return 0;
}

上面的代码中,将 test 定义为 MyClass 类的友元函数,并在函数中调用 func 函数。在创建线程时,需要将类对象的指针作为参数传递给线程。

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

  • 数据共享问题分析

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

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

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

#include <iostream>
#include <thread>

int data = 0;
void func()
{
    for (int i = 0; i < 1000000; ++ i)
    {
        ++data;
    }
}

int main()
{
    std::thread t1(func);
    std::thread t2(func);

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

    std::cout << "data: " << data << std::endl; // 输出data的值

}

上面的代码中,定义了一个名为 data 的全局变量,并在两个线程中对其进行累加操作。在 main 函数中,创建了两个线程,并分别调用了 func 函数。在 func 函数中,对 data 变量进行了累加操作。

由于 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 data = 0; // 共享数据
std::mutex mtx; // 互斥锁

void func(){
    for (int i = 0; i < 1000000; i++) {
        mtx.lock(); // 加锁
        ++data;
        mtx.unlock(); // 解锁
    }
}

int main()
{
    std::thread t1(func), t2(func);
    t1.join(); t2.join();
    std::cout << "Data: " << data << std::endl;

    return 0;
}

上面的代码中,定义了一个名为 data 的全局变量,并使用互斥量 mtx 来确保多个线程对其进行访问时的线程安全。在两个线程中,分别调用了 func 函数。在 func 函数中,先获取互斥量的所有权,然后对 data 变量进行累加操作,最后再释放互斥量的所有权。

互斥量死锁

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

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

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

#include <iostream>
#include <thread>
#include <mutex>

std::mutex m1, m2; // 定义两个互斥量

void func1(){
    for (int i = 0; i < 50000; ++ i){
        m1.lock();
        m2.lock();
        m1.unlock();
        m2.unlock();
    }
}

void func2(){
    for (int i = 0; i < 50000; ++ i){
        m2.lock();
        m1.lock();
        m2.unlock();
        m1.unlock();
    }
}

int main(){
    std::thread t1(func1);
    std::thread t2(func2);
    t1.join();  
    t2.join();

    std::cout << "Done." << std::endl;
    return 0;
}

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

修改后的代码:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex m1, m2;

void func1(){
    for (int i = 0; i < 5000; ++ i) {
        m1.lock();
        m2.lock();
        m1.unlock();
        m2.unlock();
    }
}

void func2(){
    for (int i = 0; i < 5000; ++ i) {
        m1.lock();
        m2.lock();
        m1.unlock();
        m2.unlock();
    }
}

int main(){
    std::thread t1(func1);
    std::thread t2(func2);
    t1.join();
    t2.join();

    std::cout << "Done" << std::endl;
    return 0;
}

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

std::lock_guard 与 std::unique_lock

  • std::lock_guard

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

std::lock_guard 的特点如下:

  • 当构造函数被调用时,该互斥量会被自动锁定。

  • 当析构函数被调用时,该互斥量会被自动解锁。

  • std::lock_guard 对象不能复制或移动,因此它只能在局部作用域中使用。

#include <iostream>
#include <thread>
#include <mutex>

int shared_data = 0;
std::mutex mutex;
void foo(){
    // 构造时, 互斥量锁定; 析构时, 互斥量解锁
    std::lock_guard<std::mutex> lock(mutex);
    shared_data++;
}

int main(){

    std::thread t1(foo);
    std::thread t2(foo);
    t1.join();
    t2.join();
    std::cout << "shared_data = " << shared_data << std::endl;
    return 0;
}
  • 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 进行初始化,并假设该互斥量已经被当前线程成功加锁。

#include <iostream>
#include <thread>
#include <mutex>

int shared_data = 0;
std::mutex mtx;

void foo(){
    for (int i=0; i<2; ++i){
        std::unique_lock<std::mutex> lck(mtx);
        ++shared_data;
    }
}

int main(){
    std::thread t1(foo);
    std::thread t2(foo);
    t1.join();
    t2.join();

    std::cout << "shared_data: " << shared_data << std::endl;
    return 0;
}
#include <iostream>
#include <thread>
#include <mutex>

std::timed_mutex mtx;
int shared_data = 0;

void foo(){
    for (int i=0; i<5; ++i){
        std::unique_lock<std::timed_mutex> lock(mtx, std::defer_lock); // 构造但不加锁
        if (lock.try_lock_for(std::chrono::seconds(1)) ){ // 尝试在1秒内获取锁
            std::this_thread::sleep_for(std::chrono::seconds(2)); // 当前线程等待2s
            shared_data++;
        }
    }
}

int main(){
    std::thread t1(foo);
    std::thread t2(foo);
    t1.join();
    t2.join();

    std::cout << "shared_data: " << shared_data << std::endl;
    return 0;
}

std::call_once与其使用场景

  • 单例模式

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

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

// 懒汉模式
#include <iostream>
#include <string>

class Singleton{
    private:
        Singleton(){}
    public:
        Singleton(const Singleton& other) = delete;
        Singleton& operator=(const Singleton& other) = delete;

        static Singleton& getInstance(){
            Singleton *instance = nullptr;
            if(instance == nullptr){
                instance = new Singleton();
            }
            return *instance;
        }

        void printMsg(std::string msg) {
            std::cout << __TIME__ << " " << msg << std::endl;
        }
};

int main(){
    Singleton::getInstance().printMsg("Hello C++");
    return 0;
}

在这个单例类中,我们使用了一个静态成员函数 getInstance() 来获取单例实例,该函数使用了一个静态局部变量 instance 来存储单例实例。由于静态局部变量只会被初始化一次,因此该实现可以确保单例实例只会被创建一次。

但是,该实现并不是线程安全的。如果多个线程同时调用 getInstance() 函数,可能会导致多个对象被创建,从而违反了单例模式的要求。

为了解决这些问题,我们可以使用 std::call_once 来实现一次性初始化,从而确保单例实例只会被创建一次。下面是一个使用 std::call_once 的单例实现:

#include <iostream>
#include <thread>
#include <string>
#include <mutex>


class Singleton{
    private:
        Singleton(){}
        static Singleton* instance;
        static std::once_flag initFlag;
    public:
        static Singleton& getInstance() {
            std::call_once(initFlag, init);
            return *instance;
        }

        static void init(){
            if (instance == nullptr) {
                instance = new Singleton();
            }
        }

        void printMessage(std::string msg) {
            std::cout << __TIME__ << " " << msg << std::endl;
        }

        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
};

std::once_flag Singleton::initFlag;
Singleton* Singleton::instance = nullptr;

void test() {
    Singleton::getInstance().printMessage("Hello from main");
}

int main(){

    std::thread t1(test);
    std::thread t2(test);
    t1.join();
    t2.join();
    return 0;
}

在这个实现中,我们使用 std::call_once 来确保 Singleton::init() 函数只会被调用一次。这样,我们就可以确保 Singleton 实例只会被创建一次,从而满足单例模式的要求。

condition_variable 与其使用场景

condition_variable 是一个用于线程同步的模板类,它提供了一种机制,使得一个线程可以等待另一个线程完成特定的操作后再继续执行。它通常与互斥锁一起使用,以实现线程之间的同步和通信。

std::condition_variable 的使用步骤如下:

  • 创建一个 std::condition_variable 对象。

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

  • 在需要等待条件变量的地方使用 std::unique_lockstd::mutex 对象锁定互斥锁并调用 std::condition_variable::wait()、std::condition_variable::wait_for() 或 std::condition_variable::wait_until() 函数等待条件变量。

  • 在其他线程中需要通知等待的线程时,调用 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>

std::mutex mtx;
std::condition_variable g_cv;
std::queue<int> g_queue;

void consumer(){
    while (1) {
        std::unique_lock<std::mutex> lck(mtx);
        // 当任务队列中无任务时,需要阻塞
        g_cv.wait(lck, [](){ return !g_queue.empty(); });
        std::cout << "consumer : " << g_queue.front() << std::endl;
        g_queue.pop();
    }
}

void producer(){
    for (int i=0; i<10; ++i){
        std::unique_lock<std::mutex> lck(mtx);
        // 向任务队列中添加任务
        g_queue.push(i);
        // 任务队列有任务时,需要通知消费者
        g_cv.notify_one();
        std::cout << "producer : " << i << std::endl;

        // 模拟耗时
        std::this_thread::sleep_for(std::chrono::milliseconds(100));

    }
}

int main(){

    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}

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

C++11跨平台线程池

跨平台线程池的实现

#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>

class ThreadPool{
private:
    // 线程数组
    std::vector<std::thread> workers;
    // 任务队列
    std::queue<std::function<void()>> tasks;
    // 互斥量
    std::mutex mtx;
    // 条件变量
    std::condition_variable cv;
    // 线程池是否终止
    bool stop;

public:
    // 构造函数
    ThreadPool(size_t numThreads): stop(false){
        for (size_t i = 0; i < numThreads; ++i){
            // 创建线程加入线程数组
            workers.emplace_back([this]() {
                while (true) {
                    std::function<void()> task;
                    {
                        std::unique_lock<std::mutex> lck(mtx);
                        // 等待任务
                        cv.wait(lck, [this]{ return stop || !tasks.empty(); });
                        if (stop && tasks.empty()) {
                            return;
                        }
                        task = std::move(tasks.front());
                        tasks.pop();
                    }
                    // 执行任务
                    task();
                }
            });
        }      
    }

    // 析构函数
    ~ThreadPool(){
        {
            std::unique_lock<std::mutex> lck(mtx);
            stop = true;
        }
        cv.notify_all();
        for (auto &worker: workers) {
            worker.join();
        }
    }

    // 提交任务
    template<class F, class... Args>
    void enqueue(F &&f, Args &&... args){
        std::function<void()> task(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
        {
            std::unique_lock<std::mutex> lck(mtx);
            tasks.emplace(std::move(task));
        }
        cv.notify_one();
    }
};

int main(){
    ThreadPool tp(4);
    for (size_t i = 0; i < 8; i++){
        tp.enqueue([i](){ 
            std::cout << "task : " << i << " is running" << std::endl; 
            std::this_thread::sleep_for(std::chrono::seconds(1));
            std::cout << "task : " << i << " is done" << std::endl;
        });
    }
    return 0;
}
  • 知识点1 std::function

std::function是C++标准库中的一个通用、可调用函数封装器,它可以用来存储任意类型的函数,并将其作为参数传递给其他函数。std::function是C++11引入的,它在C++17中被提升为通用算法。

std::function的声明如下:

template <class T>
class function;

其中,T是一个可调用对象的类型。std::function模板接受一个类型参数,用于指定存储的函数的返回类型。

std::function的主要优点是它提供了一种灵活的方式来编写可以接受不同类型函数的代码,这使得代码更加通用和可重用。例如,你可以使用std::function来实现一个通用的排序函数,它可以对任何类型的元素进行排序。

std::function的用法主要包括以下几个方面:

  1. 创建std::function对象:可以使用std::function<R(Args...)>创建一个std::function对象,其中R表示函数的返回类型,Args...表示函数的参数类型。例如,std::function<int(int, int)>表示一个接受两个整数参数并返回整数的函数。

  2. 存储函数:可以将任何可调用对象存储到std::function对象中。例如,将一个lambda表达式存储到std::function对象中:

std::function<int(int, int)> add = [](int a, int b) { return a + b; };
  1. 调用函数:可以使用operator()调用std::function对象中存储的函数。例如,调用上面创建的add函数:
int result = add(3, 4); // result = 7
  1. 作为参数传递:可以将std::function对象作为参数传递给其他函数。例如,定义一个接受std::function对象的函数,并打印其返回值:
void print_result(std::function<int(int, int)> func) {
    int result = func(3, 4);
    std::cout << "Result: " << result << std::endl;
}

print_result(add); // 输出 "Result: 7"
  1. 异常安全:std::function提供了异常安全 guarantee,即使存储的函数抛出异常,也不会影响到其他函数的正常执行。

总之,std::function是C++标准库中一种非常有用的工具,可以用来编写通用、可重用的代码。

  • size_t

size_t是C++标准库中的一个类型,用于表示对象的大小(以字节为单位)。它是一个无符号整数类型,通常用于表示数组、字符串和其他容器的大小。

size_t的声明如下:

typedef unsigned long size_t;

这意味着size_t实际上是一个unsigned long类型的别名。size_t的主要优点是它可以保证能够存储任何对象的大小,即使该对象的大小超过了intlong类型的范围。

size_t的用法主要包括以下几个方面:

  1. 获取对象大小:可以使用sizeof运算符获取对象的大小,并将其转换为size_t类型。例如,获取一个int类型的对象的大小:
size_t int_size = sizeof(int); // int_size is of type size_t
  1. 计算数组或容器的大小:可以使用std::distance函数或std::beginstd::end迭代器来计算数组或容器的大小。例如,计算一个std::vector的大小:
std::vector<int> vec = {1, 2, 3, 4, 5};
size_t vec_size = std::distance(vec.begin(), vec.end()); // vec_size is of type size_t

或者:

size_t vec_size = vec.size(); // vec_size is of type size_t
  1. 迭代数组或容器:可以使用std::beginstd::end迭代器来遍历数组或容器中的元素。例如,遍历一个std::vector中的元素:
std::vector<int> vec = {1, 2, 3, 4, 5};
for (size_t i = 0; i < vec.size(); ++i) {
    std::cout << vec[i] << " ";
}

总之,size_t是C++标准库中一种非常有用的类型,可以用来表示对象的大小,并保证能够存储任何对象的大小。

  • lambda表达式

Lambda表达式是C++11引入的一种新特性,它允许在代码中定义匿名函数,可以捕获上下文中的变量,用于简化和优化代码。Lambda表达式的语法如下:

[捕获列表](参数列表) -> 返回类型 {
    函数体
}

其中,捕获列表用于捕获上下文中的变量,参数列表用于定义参数,返回类型用于指定函数的返回类型,函数体用于定义函数的实现。

Lambda表达式的用法主要包括以下几个方面:

  1. 创建Lambda表达式:可以使用方括号[]创建一个Lambda表达式。例如,定义一个Lambda表达式,接受两个整数参数并返回它们的和:
auto add = [](int a, int b) { return a + b; };
  1. 调用Lambda表达式:可以使用operator()调用Lambda表达式。例如,调用上面创建的addLambda表达式:
int result = add(3, 4); // result = 7
  1. 作为参数传递:可以将Lambda表达式作为参数传递给其他函数。例如,定义一个接受std::function对象的函数,并打印其返回值:
void print_result(std::function<int(int, int)> func) {
    int result = func(3, 4);
    std::cout << "Result: " << result << std::endl;
}

print_result(add); // 输出 "Result: 7"
  1. 捕获列表:Lambda表达式可以捕获上下文中的变量,用于在函数体中使用。捕获列表的语法如下:
[捕获方式1, 捕获方式2, ...]

其中,捕获方式可以是=(值捕获)、&(引用捕获)或[](捕获所有变量)。例如,定义一个Lambda表达式,接受一个整数参数并将其打印到控制台,同时捕获一个局部变量i

int i = 10;
auto print_i = [&i](int x) { std::cout << "i = " << i << ", x = " << x << std::endl; };
print_i(20); // 输出 "i = 10, x = 20"

总之,Lambda表达式是C++11中一种非常有用的特性,可以用来简化代码,提高代码的可读性和可维护性。

  • std::move

std::move() 是 C++11 引入的一个函数模板,位于头文件 <utility> 中。它用于将对象转换为右值引用,通常用于实现移动语义。

在 C++ 中,传统的赋值操作符(=)对于对象的复制,会涉及到深拷贝(如果没有特殊的优化)。而移动语义是一种更高效的资源管理方式,通过将资源的所有权从一个对象转移到另一个对象,而不需要复制资源。这对于大型对象或者对象拥有资源(如动态分配的内存、文件句柄等)时尤其有用。

std::move() 的使用方式很简单,它接受一个左值参数,返回一个右值引用,表示该参数可以被移动。它的定义如下:

namespace std {
    template<typename T>
    constexpr remove_reference_t<T>&& move(T&& t) noexcept;
}

在使用时,你可以将需要移动的对象传递给 std::move(),从而获取到一个右值引用,然后将该引用传递给接受右值引用的函数或构造函数,以触发移动语义。

下面是一个简单的示例:

#include <iostream>
#include <utility>

class MyClass {
public:
    MyClass() { std::cout << "Constructor\n"; }
    MyClass(const MyClass&) { std::cout << "Copy constructor\n"; }
    MyClass(MyClass&&) { std::cout << "Move constructor\n"; }
};

int main() {
    MyClass obj1;
    MyClass obj2 = std::move(obj1); // 调用移动构造函数

    return 0;
}

在这个示例中,obj1 被移动到 obj2 中,因此 obj2 的构造函数调用了移动构造函数。std::move() 的使用表明我们有意将 obj1 转换为右值引用,以便触发移动语义。

  • template<typename F, typename… Args>

这是一个C++模板函数的声明,它定义了一个可以接受任意数量和类型的参数的模板函数。

typename F 是一个模板参数,表示函数的第一个参数的类型。typename... Args 表示函数可以接受任意数量的其他参数,这些参数的类型将会在编译时根据传入的实参类型进行推断。

这是一个非常通用的模板函数声明,可以用来定义各种接受任意数量和类型参数的函数,例如标准库中的 std::applystd::bindstd::function 等。

下面是一个简单的例子,定义了一个接受任意数量整数参数的模板函数,计算它们的和:

#include <iostream>

template<typename F, typename... Args>
auto sum(F f, Args... args) {
    return (... + f(args));
}

int main() {
    std::cout << sum(std::plus<int>(), 1, 2, 3, 4, 5) << std::endl; // 输出 15
    return 0;
}

在这个例子中,sum 函数接受任意数量整数参数,使用 std::plus<int> 作为第一个参数,表示使用 + 运算符进行求和。... + f(args) 是一个折叠表达式,用于计算所有参数的和。

  • std::bind

std::bind 是 C++ 标准库中的一个实用函数,用于创建一个新的函数,该函数是另一个函数的绑定版本。绑定函数是指将一个函数的某些参数固定住,形成一个新的函数。

std::bind 的语法如下:

std::bind(f, args...)

其中,f 是需要绑定的函数,args... 是需要固定的参数。

std::bind 的主要用途是实现高阶函数,即接受函数作为参数的函数。例如,在标准库中,std::sort 函数接受一个比较函数作为参数,用于比较数组中的元素。可以使用 std::bind 将一个比较函数绑定到 std::sort,使其成为一个新的函数,用于对数组进行排序。

以下是一个简单的示例,展示了如何使用 std::bind 对数组进行排序:

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

int main() {
    std::vector<int> arr = {5, 3, 1, 4, 2};

    // 使用 std::bind 对数组进行排序
    std::sort(arr.begin(), arr.end(), std::bind(std::less<int>(), std::placeholder::_1, std::placeholder::_2));

    // 输出排序后的数组
    for (int i : arr) {
        std::cout << i << " ";
    }
    std::cout << std::endl;

    return 0;
}

在这个例子中,我们使用 std::bindstd::less<int> 绑定到 std::sort,使其成为一个新的比较函数,用于比较数组中的元素。std::placeholder::_1std::placeholder::_2 是占位符,表示在比较函数中使用的两个参数。

总之,std::bind 是 C++ 标准库中的一个实用函数,可以用来创建一个新的函数,该函数是另一个函数的绑定版本。绑定函数是指将一个函数的某些参数固定住,形成一个新的函数。std::bind 的主要用途是实现高阶函数,即接受函数作为参数的函数。

  • std::forward

std::forward 是 C++ 标准库中的一个实用函数,用于在模板编程中实现完美转发(perfect forwarding)。完美转发是指在将一个函数的参数传递给另一个函数时,保持参数的类型不变。

std::forward 的语法如下:

std::forward(x)

其中,x 是需要转发的对象。

std::forward 的主要用途是在模板函数中,将传入的参数转发给另一个函数,同时保持参数的类型不变。这在需要将参数传递给另一个模板函数时非常有用,可以避免类型转换。

以下是一个简单的示例,展示了如何使用 std::forward 实现完美转发:

#include <iostream>
#include <utility>

template<typename T>
void print(T t) {
    std::cout << t << std::endl;
}

template<typename T>
void forward_print(T t) {
    print(std::forward<T>(t));
}

int main() {
    int x = 42;
    forward_print(x); // 输出 42
    return 0;
}

在这个例子中,我们有两个函数:printforward_printprint 函数接受一个任意类型的参数,并将其输出到控制台。forward_print 函数接受一个任意类型的参数,然后将其转发给 print 函数。使用 std::forward<T>(t) 将传入的参数 t 转发给 print 函数,同时保持参数的类型不变。

总之,std::forward 是 C++ 标准库中的一个实用函数,用于在模板编程中实现完美转发。完美转发是指在将一个函数的参数传递给另一个函数时,保持参数的类型不变。std::forward 的主要用途是在模板函数中,将传入的参数转发给另一个函数,同时保持参数的类型不变。

  • 左值引用 右值引用 万能引用

左值引用、右值引用和万能引用是 C++ 中与引用相关的重要概念,它们在 C++11 之后的版本中得到了加强和扩展。这些概念对于理解 C++ 的值类别、移动语义和模板编程十分重要。

  1. 左值引用(Lvalue Reference)

左值引用是 C++ 中最早的引用形式,它是对左值的引用。左值是具名的、有地址的、可寻址的对象,它们可以出现在赋值运算符的左边。左值引用的语法是 T&,其中 T 是被引用对象的类型。

int x = 42;
int& ref = x; // 左值引用

左值引用允许修改引用的对象,并且提供了对引用对象的别名。它通常用于传递参数或者作为返回值,以避免不必要的复制。

  1. 右值引用(Rvalue Reference)

右值引用是 C++11 引入的新概念,它是对右值的引用。右值是没有名称的、临时的、即将销毁的对象,通常出现在赋值运算符的右边。右值引用的语法是 T&&,其中 T 是被引用对象的类型。

int&& rref = 42; // 右值引用

右值引用的引入主要是为了支持移动语义和完美转发。它允许我们显式地标识出可以安全地“偷取”资源的对象,避免了不必要的资源拷贝。右值引用通常用于移动构造函数、移动赋值运算符和完美转发。

  1. 万能引用(Universal Reference)

万能引用是 Scott Meyers 在 C++11 中提出的概念,它是右值引用的一个特殊形式,在模板参数推导的上下文中表现出了一种“万能”的行为。它的语法是 T&&,其中 T 是模板类型参数。

template<typename T>
void func(T&& t);

在这种情况下,当 T 被推导为左值引用类型时,T&& 会变成左值引用;而当 T 被推导为非引用类型时,T&& 会变成右值引用。这种行为使得函数模板 func 能够接受任意类型的参数,并保持其值类别不变,从而实现了完美转发。

int x = 42;
func(x); // 推导出 T 为 int&,变成左值引用
func(42); // 推导出 T 为 int,变成右值引用

万能引用在实现泛型代码时非常有用,尤其是在模板函数中需要转发参数时,能够保持参数的值类别不变。

  • emplace
    emplace 是 C++ 标准库中的一个实用函数,用于在容器中直接构造对象,而不是拷贝或移动对象。这在插入新元素时非常有用,可以提高性能并减少内存使用。

emplace 的语法如下:

template<class T>
void emplace(args...);

其中,T 是需要构造的对象类型,args... 是构造对象时需要传递的参数。

emplace 的主要用途是在容器中插入新元素,同时直接构造对象,而不是拷贝或移动对象。这在插入新元素时非常有用,可以提高性能并减少内存使用。

以下是一个简单的示例,展示了如何使用 emplace 在容器中插入新元素:

#include <iostream>
#include <vector>

class MyClass {
public:
    MyClass(int x) : x(x) {}
    int x;
};

int main() {
    std::vector<MyClass> vec;

    // 使用 push_back 插入新元素,需要拷贝或移动对象
    vec.push_back(MyClass(1));
    vec.push_back(MyClass(2));

    // 使用 emplace 插入新元素,直接构造对象
    vec.emplace(vec.end(), 3);
    vec.emplace(vec.end(), 4);

    for (const auto& obj : vec) {
        std::cout << obj.x << " ";
    }
    std::cout << std::endl;

    return 0;
}

在这个例子中,我们有一个名为 MyClass 的简单类,构造函数接受一个整数参数。我们创建了一个 std::vector<MyClass> 容器,并使用 push_back 插入新元素。然后,我们使用 emplace 在容器中直接构造对象,而不是拷贝或移动对象。最后,我们遍历并输出容器中的元素。

总之,emplace 是 C++ 标准库中的一个实用函数,用于在容器中直接构造对象,而不是拷贝或移动对象。这在插入新元素时非常有用,可以提高性能并减少内存使用。

异步并发—— async future packaged_task promise

  • async、future

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

下面是一个使用std::async的案例:

#include <iostream>
#include <future>

int foo(){
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 42;
}

int main(){
    std::future<int> future_result = std::async(std::launch::async, foo);
    std::cout << foo() << std::endl; // 调用foo(),但不会阻塞
    // 获取异步操作的结果
    std::cout << future_result.get() << std::endl;
    return 0;
}

这个例子中,我们使用std::async函数异步执行了一个耗时的计算,这个计算可以在另一个线程中执行,不会阻塞主线程。同时,我们也避免了手动创建线程和管理线程的麻烦。

  • packaged_task

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

下面是一个使用std::packaged_task的例子:

#include <iostream>
#include <future>

int foo(int x){
    std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作
    return x;
}

int main(){

    std::packaged_task<int(int)> task(foo);
    std::future<int> result = task.get_future();
    /*  这里创建了一个新的线程,并在这个线程中执行异步操作。
        由于packaged_task对象是可移动的,
        因此需要使用std::move()函数将task对象转移至新线程中执行。
    */
    std::thread t(std::move(task), 10); // std::move的使用
    t.join();

    std::cout << result.get() << std::endl;
    return 0;
}

在这个例子中,我们首先创建了一个std::packaged_task对象,并将foo函数绑定到该对象上。然后,我们创建了一个std::future对象,并将其与packaged_task对象关联起来。接下来,我们创建了一个新的线程,并将packaged_task对象转移至新线程中执行。在新线程中,我们调用foo函数,并将结果存储到std::future对象中。最后,我们在主线程中使用std::future对象的get()方法获取异步操作的结果。

  • promise

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

以下是promise的用法:

#include <iostream>
#include <future>

void foo(std::promise<int> &p){
    p.set_value(100);
}

int main(){
    // 创建promise对象
    std::promise<int> p;
    // 获取future对象
    std::future<int> future_result = p.get_future();
    // 创建线程
    std::thread t(foo, std::ref(p));
    // 等待子线程执行完毕
    t.join();

    std::cout << future_result.get() << std::endl;
    return 0;
}

在主线程中,我们可以使用future对象的get()方法获取promise对象产生的值,并输出到控制台。

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

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 <iostream>
#include <atomic>
#include <thread>

std::atomic<int> data;

void foo(){
    for (int i=0; i<10000; ++i){
        data++;
    }
}

int main(){
    std::thread t1(foo);
    std::thread t2(foo);

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

    std::cout << "Data: " << data << std::endl;
    std::cout << "Data: " << data.load() << std::endl; // 输出 20000
    data.store(1);
    std::cout << "Data: " << data.load() << std::endl; // 输出 1
    return 0;
}

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

  • 24
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值