【C++】线程库

欢迎来到Cefler的博客😁

🕌博客主页:折纸花满衣

🏠个人专栏:C++


前言

C++11标准引入了线程库,通过其可以在C++程序中方便地创建和管理线程。以下是一些常用的线程库组件:

  1. std::threadstd::thread类用于表示一个线程,通过其构造函数可以传入一个可调用对象(函数或者lambda表达式)作为线程的入口点,然后调用join()detach()来等待线程结束或者将线程分离。

  2. std::mutexstd::mutex类用于实现互斥锁防止多个线程同时访问共享资源导致数据竞争 。可以通过std::lock_guardstd::unique_lock来管理std::mutex的锁定和解锁。

  3. std::condition_variablestd::condition_variable类用于线程间的条件变量通信,一个线程可以等待另一个线程满足特定条件后再继续执行。

  4. std::atomicstd::atomic模板类提供了原子操作,确保在多线程环境下对共享变量的操作是原子的,避免竞态条件。

  5. std::future 和 std::promisestd::futurestd::promise用于在线程间传递异步操作的结果。std::promise用于设置值,而std::future用于获取这个值。

这些组件提供了丰富的功能,使得在C++程序中使用多线程变得更加容易和安全。


目录

👉🏻thread

std::thread 是 C++11 标准库中用于创建和管理线程的类。下面是 std::thread 类中一些重要的接口函数:

  1. 构造函数

    • explicit thread(Args&&... args);:接受线程函数参数,并在构造函数中启动新线程。
  2. 成员函数

    • void join();:等待线程结束,阻塞当前线程直到被调用的线程执行完毕。
    • bool joinable() const noexcept;:检查线程是否可加入(joinable),即线程对象是否与实际的线程相关联。
    • void detach();:分离线程,允许线程在后台继续执行,线程结束时自动释放资源。
    • std::thread::id get_id() const noexcept;:获取线程的唯一标识符。
    • static unsigned int hardware_concurrency() noexcept;:获取当前系统支持的并发线程数。
  3. 静态成员函数

    • static void yield();:提示调度器放弃当前时间片,允许其他线程执行。
    • static void sleep_for(const std::chrono::duration& rel_time);:使当前线程休眠一段时间。
    • static void sleep_until(const std::chrono::time_point& abs_time);:使当前线程休眠直到指定的时间点。

这些是 std::thread 类中最常用的接口函数,可以帮助你创建、管理和操作线程。当使用 std::thread 时,确保理解这些函数的作用和用法,以便有效地编写多线程程序。

成员函数

join() 函数:

  • 函数原型

    cpp 复制代码
    void join();
  • 参数意义join() 函数没有参数。

  • 返回值void

  • 示例代码

    cpp 复制代码
    #include <iostream>
    #include <thread>
    
    void threadFunction() {
        std::cout << "Thread running\n";
    }
    
    int main() {
        std::thread t(threadFunction);
        t.join(); // 等待线程执行完毕
        std::cout << "Main thread\n";
        return 0;
    }

    在上面的示例中,join() 函数会阻塞主线程,直到线程 t 执行完毕后才继续执行主线程。

joinable()函数

  • 函数原型

    cpp 复制代码
    bool joinable() const noexcept;
  • 参数意义joinable() 函数没有参数。

  • 返回值bool,如果线程对象与实际线程相关联,则返回 true;否则返回 false

  • 示例代码

    cpp 复制代码
    #include <iostream>
    #include <thread>
    
    int main() {
        std::thread t;
        std::cout << "Is thread joinable? " << (t.joinable() ? "Yes" : "No") << std::endl;
        
        t = std::thread([](){
            std::cout << "Thread running\n";
        });
        
        std::cout << "Is thread joinable? " << (t.joinable() ? "Yes" : "No") << std::endl;
        
        t.join();
        std::cout << "Is thread joinable? " << (t.joinable() ? "Yes" : "No") << std::endl;
        
        return 0;
    }

    在这个示例中,先输出 No,表示初始时线程 t 不可加入;然后创建线程并输出 Yes;最后再次输出 No,表示线程在调用 join() 后不可再次加入。

detach函数

  • 函数原型

    cpp 复制代码
    void detach();
  • 参数意义detach() 函数没有参数。

  • 返回值void

  • 示例代码

    cpp 复制代码
    #include <iostream>
    #include <thread>
    
    void threadFunction() {
        std::cout << "Thread running\n";
    }
    
    int main() {
        std::thread t(threadFunction);
        t.detach(); // 分离线程
        // 注意:分离后不能再调用join(),否则会导致程序终止
        
        std::this_thread::sleep_for(std::chrono::seconds(1)); // 等待子线程执行完毕(非最佳做法)
        
        std::cout << "Main thread\n";
        return 0;
    }

    在这个示例中,线程 t 被分离后,主线程不再等待其执行完毕,而是继续执行后续代码。

get_id()函数

  • 函数原型

    cpp 复制代码
    std::thread::id get_id() const noexcept;
  • 参数意义get_id() 函数没有参数。

  • 返回值std::thread::id,表示线程的唯一标识符。

  • 示例代码

    cpp 复制代码
    #include <iostream>
    #include <thread>
    
    int main() {
        std::thread t([]{
            std::cout << "Thread ID: " << std::this_thread::get_id() << std::endl;
        });
        
        std::cout << "Main thread ID: " << std::this_thread::get_id() << std::endl;
        
        t.join();
        return 0;
    }

    这个示例演示了如何使用 get_id() 函数获取线程的唯一标识符,并输出主线程和子线程的 ID。

静态成员函数

std::this_thread::yield()函数

  • 函数原型

    cpp 复制代码
    void yield();
  • 参数意义yield() 函数没有参数。

  • 返回值void

  • 功能yield() 函数会将当前线程放弃处理器,以便其他线程有机会执行。调用此函数会暗示操作系统调度器立即切换到另一个可运行的线程。

  • 示例代码

    cpp 复制代码
    #include <iostream>
    #include <thread>
    
    void threadFunction() {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Thread running\n";
            std::this_thread::yield(); // 放弃处理器
        }
    }
    
    int main() {
        std::thread t(threadFunction);
        for (int i = 0; i < 5; ++i) {
            std::cout << "Main thread\n";
            std::this_thread::yield(); // 放弃处理器
        }
        t.join();
        return 0;
    }

    在这个示例中,yield() 函数被用来让出处理器,使得线程间能够交替执行。

std::this_thread::sleep_for()函数

  • 函数原型

    cpp 复制代码
    template< class Rep, class Period >
    void sleep_for( const std::chrono::duration<Rep,Period>& sleep_duration );
  • 参数意义sleep_duration 表示休眠的时间段,可以是 std::chrono::milliseconds, std::chrono::seconds 等类型。

  • 返回值void

  • 功能sleep_for() 函数会使当前线程休眠指定的时间段。

  • 示例代码

    cpp 复制代码
    #include <iostream>
    #include <thread>
    #include <chrono>
    
    int main() {
        std::cout << "Main thread starts sleeping\n";
        std::this_thread::sleep_for(std::chrono::seconds(3)); // 休眠3秒
        std::cout << "Main thread wakes up\n";
        return 0;
    }

    在这个示例中,主线程调用 sleep_for() 函数休眠3秒后再继续执行。

std::this_thread::sleep_until()函数

  • 函数原型

    cpp 复制代码
    template< class Clock, class Duration >
    void sleep_until( const std::chrono::time_point<Clock,Duration>& sleep_time );
  • 参数意义sleep_time 表示将要休眠到的时间点,通常通过 std::chrono::system_clock::now() + duration 来计算。

  • 返回值void

  • 功能sleep_until() 函数会使当前线程休眠直到指定的时间点。

  • 示例代码

    cpp 复制代码
    #include <iostream>
    #include <thread>
    #include <chrono>
    
    int main() {
        auto wakeUpTime = std::chrono::system_clock::now() + std::chrono::seconds(5);
        std::cout << "Main thread starts sleeping\n";
        std::this_thread::sleep_until(wakeUpTime); // 休眠直到指定时间点
        std::cout << "Main thread wakes up\n";
        return 0;
    }

    在这个示例中,主线程调用 sleep_until() 函数休眠直到指定的时间点后再继续执行。

为什么thread的传参里面如果想传引用一定要用std::ref?

在C++中,std::ref是用于包装引用的模板函数,它位于<functional>头文件中。当你需要将一个引用传递给函数或者线程时,有时候需要使用std::ref来确保正确的引用传递语义。

通常情况下,当你将一个变量作为参数传递给函数或者线程时,会发生值复制。但是有时候你希望传递的实际上是引用本身,而不是引用指向的值。这时就可以使用std::ref来包装引用,以便进行正确的引用传递。

举个例子,假设有一个函数void func(int& val)接受一个整型引用作为参数,如果你想在创建线程时将某个整型变量x的引用传递给这个函数,可以这样做:

cpp 复制代码
int x = 42;
std::thread t(func, std::ref(x));

在这个例子中,std::ref(x)x的引用包装起来,然后将这个包装后的引用传递给func函数,确保了在新线程中对x的引用传递。


介绍完ref后,我们来回答开始的问题:

在C++中,当你创建线程时,传递参数给线程的方式是通过复制参数值来实现的。如果你想传递引用而不是值,直接将引用传递给std::thread是不安全的,因为线程的执行可能会在原始变量(引用指向的对象)被销毁之后才开始 ,导致悬空引用 或者未定义行为

使用std::ref包装引用的原因在于它可以延长被引用对象的生命周期std::ref返回一个std::reference_wrapper对象,该对象在复制时只是简单地复制了引用,而不是引用指向的内容。这样可以确保被引用对象在线程执行期间保持有效,并在线程完成后才会被销毁。

因此,为了避免悬空引用或者未定义行为,当你想要在线程中传递引用时,必须使用std::ref或者std::cref来保证被引用对象的有效性,从而确保线程安全地访问被引用的对象。

👉🏻mutex

基本接口函数

当涉及到多线程编程时,std::mutex 是一个常用的互斥量类,用于保护共享资源 ,避免多个线程同时访问导致数据竞争。以下是 std::mutex 类提供的一些主要接口函数:

  1. std::mutex 构造函数

    • 函数原型

      cpp 复制代码
      mutex();
    • 功能:创建一个新的互斥量对象。

  2. lock() 函数

    • 函数原型

      cpp 复制代码
      void lock();
    • 功能:尝试锁定互斥量,如果互斥量当前没有被其他线程锁定,则当前线程将锁定互斥量;如果互斥量已经被锁定,当前线程将被阻塞直到互斥量可用。

  3. try_lock() 函数

    • 函数原型

      cpp 复制代码
      bool try_lock();
    • 功能 :尝试去锁定互斥量,如果互斥量当前没有被其他线程锁定,则当前线程将锁定互斥量并返回 true;如果互斥量已经被锁定,try_lock() 立即返回 false 而不会阻塞当前线程。

  4. unlock() 函数

    • 函数原型

      cpp 复制代码
      void unlock();
    • 功能:解锁互斥量,允许其他线程尝试锁定该互斥量。

下面是一个简单的示例,演示了如何使用 std::mutex 来保护共享资源:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int sharedData = 0;

void updateSharedData() {
    mtx.lock();
    sharedData++;
    std::cout << "Thread ID: " << std::this_thread::get_id() << " updated sharedData to: " << sharedData << std::endl;
    mtx.unlock();
}

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

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

    return 0;
}

在这个示例中,两个线程 t1t2 分别尝试更新 sharedData 变量,通过 std::mutex 对这一操作进行保护。这样可以确保在任一时刻只有一个线程可以访问和修改 sharedData,避免了数据竞争问题。

lock_guard和unique_lock

std::lock_guardstd::unique_lock 都是 C++ 中用于管理互斥锁(mutex)的 RAII(资源获取即初始化)类,它们可以帮助确保在作用域结束时自动释放互斥锁,避免忘记手动解锁而导致的死锁等问题。

  1. std::lock_guard

    • std::lock_guard 是一个轻量级的互斥锁封装类,一旦被创建,它会自动锁定传入的互斥锁,并在其作用域结束时自动解锁。
    • std::lock_guard 适用于那些在同一作用域内需要加锁和解锁的场景,它不能手动释放锁,只能在作用域结束时自动释放锁。
  2. std::unique_lock

    • std::unique_lock 提供了比 std::lock_guard 更灵活的功能。它不仅可以管理互斥锁的锁定和解锁,还可以手动地进行锁定和解锁。
    • std::unique_lock 支持延迟加锁和条件变量,因此在需要更灵活控制锁的场景下更为适用。

在使用这两个类时,当创建一个 std::lock_guardstd::unique_lock 对象时,会在构造函数中锁定传入的互斥锁,并在对象销毁时(作用域结束)自动解锁。

以下是一个简单的示例,演示了如何使用 std::lock_guardstd::unique_lock 来管理互斥锁:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;

void workFunction() {
    std::lock_guard<std::mutex> lockGuard(mtx); // 使用 std::lock_guard 自动管理锁的加锁和解锁

    // 这里可以安全地访问共享资源,因为锁已经被自动加锁

    std::cout << "Worker thread is processing...\n";
}

int main() {
    std::thread workerThread(workFunction);

    // 主线程也可以使用 std::unique_lock 手动管理锁的加锁和解锁
    std::unique_lock<std::mutex> uniqueLock(mtx);
    
    // 这里可以安全地访问共享资源,因为锁已经被手动加锁

    // 在 uniqueLock 对象生命周期结束时,互斥锁会被自动解锁

    workerThread.join();

    return 0;
}

在这个示例中,workFunction 函数通过 std::lock_guard 管理互斥锁,而主线程通过 std::unique_lock 手动管理互斥锁


RAII(Resource Acquisition Is Initialization)是一种重要的 C++ 编程技术,用于管理资源的分配和释放。RAII 的核心思想是:通过在对象的构造函数中获取资源(如内存、文件句柄、互斥锁等),在对象的析构函数中释放资源,从而确保资源在对象生命周期结束时被正确释放。

使用 RAII 技术可以避免资源泄漏和忘记释放资源导致的问题,使得资源的管理更加安全和简单。当对象被创建时,它自动获取资源;当对象超出作用域时,其析构函数会自动调用,确保资源被释放。这种自动化的资源管理方式使得代码更加健壮并且易于维护。

常见的使用 RAII 的情况包括:

  1. 使用 std::unique_ptrstd::shared_ptr 等智能指针来管理动态分配的内存。
  2. 使用 std::lock_guardstd::unique_lock 等类来管理互斥锁的加锁和解锁。
  3. 使用文件流对象来管理文件的打开和关闭。
  4. 使用自定义的 RAII 类来管理其他资源,如数据库连接、网络连接等。

RTTI(Run-Time Type Information)是一个 C++ 的特性,用于在运行时获取对象的类型信息。通过 RTTI,你可以在程序运行时查询对象的实际类型,以及确定对象是否属于某个特定的类或子类。

C++ 中的 RTTI 主要通过两个关键字来实现:

  1. dynamic_cast:用于在继承体系中进行安全的向下转换(downcast),即将基类指针或引用转换为派生类指针或引用。如果转换不安全,dynamic_cast会返回空指针(对于指针)或抛出std::bad_cast异常(对于引用)。

  2. typeid:用于获取对象的类型信息。通过typeid操作符,可以获得一个指向std::type_info的对象,从而可以比较两个对象的类型是否相同。

这些特性允许你在运行时进行类型检查和类型转换,通常用于处理多态对象、工厂模式、插件系统等场景。需要注意的是,RTTI 的使用可能会引入一些运行时开销,并且过度依赖 RTTI 也可能暗示设计上的缺陷。因此,在使用 RTTI 时需要权衡好利弊,确保它符合程序设计的需要。

总的来说,RTTI 是 C++ 提供的一种功能强大的特性,可以在某些情况下帮助你更加灵活地处理对象的类型信息,但需要慎重使用以避免过度复杂化代码结构。


👉🏻condition_variable

当涉及到多线程编程中的线程同步和通信时,std::condition_variable 是一个重要的工具,用于实现线程间的条件变量等待和通知机制。下面是 std::condition_variable 类提供的一些主要接口函数:

  1. std::condition_variable 构造函数

    • 函数原型

      cpp 复制代码
      condition_variable();
    • 功能:创建一个新的条件变量对象。

  2. wait() 函数

    • 函数原型

      cpp 复制代码
      void wait(std::unique_lock<std::mutex>& lock);
    • 功能 :当前线程等待条件变量,同时释放互斥锁 lock,使得其他线程可以获取互斥锁并修改共享数据。当收到通知后,wait() 函数会重新获取互斥锁 lock 并继续执行。

  3. notify_one() 函数

    • 函数原型

      cpp 复制代码
      void notify_one();
    • 功能:唤醒等待在条件变量上的一个线程,如果有多个线程在等待,则只会唤醒其中一个线程。

  4. notify_all() 函数

    • 函数原型

      cpp 复制代码
      void notify_all();
    • 功能:唤醒等待在条件变量上的所有线程。

下面是一个简单的示例,演示了如何使用 std::condition_variable 进行线程同步和通信:

cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

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

void workerThread() {
    std::unique_lock<std::mutex> lock(mtx);
    while (!ready) {
        cv.wait(lock);
    }
    std::cout << "Worker thread is processing...\n";
}

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

    // 模拟一些工作的完成
    std::this_thread::sleep_for(std::chrono::seconds(2));
    
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
        std::cout << "Main thread notifies worker thread\n";
    }
    
    cv.notify_one();

    worker.join();

    return 0;
}

在这个示例中,主线程通过设置 readytrue,并调用 notify_one() 唤醒等待在条件变量 cv 上的工作线程。工作线程在收到通知后被唤醒,然后开始处理任务。


当谈到条件变量在多线程中的作用时,我们可以通过一个幽默的例子来理解它的用处。

假设有一个办公室里有两个员工:小明和小红。他们的工作是协作完成一份文件,小明负责写文档,小红负责审核文档。他们需要保持协调,即小明写完了文档,小红要及时审核,并且在小红审核完后,小明要能够继续写新的文档。

这里,小明和小红就相当于两个线程,他们需要协作完成工作,而条件变量就是用来协调这种工作流程的。具体来说:

  • 小明写完文档后,他会等待条件变量,即等待小红的审核通知。
  • 小红审核完文档后,她会通知小明,同时唤醒小明继续写新的文档。

这个例子中,小明和小红就像是两个线程,他们通过条件变量来同步工作,保证了写和审核工作的顺利进行,避免了资源的浪费和不必要的等待。同时,这个例子也生动地展示了条件变量在多线程场景下的作用,希望能够帮助你更好地理解条件变量的使用。


如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长


相关推荐
van叶~1 小时前
算法妙妙屋-------1.递归的深邃回响:二叉树的奇妙剪枝
c++·算法
knighthood20011 小时前
解决:ros进行gazebo仿真,rviz没有显示传感器数据
c++·ubuntu·ros
半盏茶香2 小时前
【C语言】分支和循环详解(下)猜数字游戏
c语言·开发语言·c++·算法·游戏
小堇不是码农2 小时前
在VScode中配置C_C++环境
c语言·c++·vscode
Jack黄从零学c++2 小时前
C++ 的异常处理详解
c++·经验分享
捕鲸叉7 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer7 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq7 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
青花瓷9 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
幺零九零零10 小时前
【C++】socket套接字编程
linux·服务器·网络·c++