【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() 函数:

  • 函数原型

    void join();
    
  • 参数意义join() 函数没有参数。

  • 返回值void

  • 示例代码

    #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()函数

  • 函数原型

    bool joinable() const noexcept;
    
  • 参数意义joinable() 函数没有参数。

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

  • 示例代码

    #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函数

  • 函数原型

    void detach();
    
  • 参数意义detach() 函数没有参数。

  • 返回值void

  • 示例代码

    #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()函数

  • 函数原型

    std::thread::id get_id() const noexcept;
    
  • 参数意义get_id() 函数没有参数。

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

  • 示例代码

    #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()函数

  • 函数原型

    void yield();
    
  • 参数意义yield() 函数没有参数。

  • 返回值void

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

  • 示例代码

    #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()函数

  • 函数原型

    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() 函数会使当前线程休眠指定的时间段。

  • 示例代码

    #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()函数

  • 函数原型

    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() 函数会使当前线程休眠直到指定的时间点。

  • 示例代码

    #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的引用传递给这个函数,可以这样做:

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 构造函数

    • 函数原型
      mutex();
      
    • 功能:创建一个新的互斥量对象。
  2. lock() 函数

    • 函数原型
      void lock();
      
    • 功能:尝试锁定互斥量,如果互斥量当前没有被其他线程锁定,则当前线程将锁定互斥量;如果互斥量已经被锁定,当前线程将被阻塞直到互斥量可用。
  3. try_lock() 函数

    • 函数原型
      bool try_lock();
      
    • 功能:尝试去锁定互斥量,如果互斥量当前没有被其他线程锁定,则当前线程将锁定互斥量并返回 true;如果互斥量已经被锁定,try_lock() 立即返回 false 而不会阻塞当前线程。
  4. unlock() 函数

    • 函数原型
      void unlock();
      
    • 功能:解锁互斥量,允许其他线程尝试锁定该互斥量。

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

#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 来管理互斥锁:

#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 构造函数

    • 函数原型
      condition_variable();
      
    • 功能:创建一个新的条件变量对象。
  2. wait() 函数

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

    • 函数原型
      void notify_one();
      
    • 功能:唤醒等待在条件变量上的一个线程,如果有多个线程在等待,则只会唤醒其中一个线程。
  4. notify_all() 函数

    • 函数原型
      void notify_all();
      
    • 功能:唤醒等待在条件变量上的所有线程。

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

#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 上的工作线程。工作线程在收到通知后被唤醒,然后开始处理任务。


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

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

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

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

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


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

在这里插入图片描述
在这里插入图片描述

相关推荐

  1. c++11线的使用】

    2024-03-21 10:34:02       16 阅读
  2. C/C++ 16】C++11线

    2024-03-21 10:34:02       31 阅读
  3. C++11 Thead线的基本使用

    2024-03-21 10:34:02       33 阅读
  4. 并发编程2-掌握C#线的使用

    2024-03-21 10:34:02       19 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-03-21 10:34:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-21 10:34:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-21 10:34:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-21 10:34:02       20 阅读

热门阅读

  1. 树形递归模板

    2024-03-21 10:34:02       23 阅读
  2. 从零学算法23

    2024-03-21 10:34:02       15 阅读
  3. Mysql查询与统计

    2024-03-21 10:34:02       23 阅读
  4. 5G和Wi-Fi 6E/7是如何结合在一起的?

    2024-03-21 10:34:02       18 阅读
  5. 开源计算机视觉库OpenCV详解

    2024-03-21 10:34:02       21 阅读
  6. OSI/RM参考模型

    2024-03-21 10:34:02       17 阅读
  7. element ui el-table分页多选功能失效

    2024-03-21 10:34:02       22 阅读
  8. Python断言

    2024-03-21 10:34:02       22 阅读
  9. 嵌入式相机WEB,用C直接处理?

    2024-03-21 10:34:02       20 阅读
  10. 【python】(02)初识迭代器Iterator

    2024-03-21 10:34:02       21 阅读
  11. 【开发工具】Git入门

    2024-03-21 10:34:02       20 阅读
  12. 了解测试用例与测试场景

    2024-03-21 10:34:02       18 阅读