C++多线程

  • 虚函数

在C++中, virtual 和 override 关键字用于支持多态,尤其是在涉及类继承和方法重写的情况下。正确地理解和使用这两个关键字对于编写可维护和易于理解的面向对象代码至关重要。

virtual 关键字

  1. 使用场景:在基类中声明虚函数。
  2. 目的:允许派生类重写该函数,实现多态。
  3. 行为:当通过基类的指针或引用调用一个虚函数时,调用的是对象实际类型的函数版本
  4. 示例:
class Base {
public:
    virtual void func() {
        std::cout << "Function in Base" << std::endl;
    }
};

override 关键字

  1. 使用场景:在派生类中重写虚函数。
  2. 目的:明确指示函数意图重写基类的虚函数。
  3. 行为:确保派生类的函数确实重写了基类中的一个虚函数。如果没有匹配的虚函数,编译器会报错。
  1. 示例
class Derived : public Base {
public:
    void func() override {
        std::cout << "Function in Derived" << std::endl;
    }
};

注意点

只在派生类中使用 override: override 应仅用于派生类中重写基类的虚函数。

虚析构函数:如果类中有虚函数,通常应该将析构函数也声明为虚的。

默认情况下,成员函数不是虚的:在C++中,成员函数默认不是虚函数。只有显式地使用 virtual关键字才会成为虚函数。

继承中的虚函数:一旦在基类中声明为虚函数,该函数在所有派生类中自动成为虚函数,无论是否使用 virtual 关键字。

正确使用 virtual 和 override 关键字有助于清晰地表达程序员的意图,并利用编译器检查来避免常见的错误,如签名不匹配导致的非预期的函数重写。


  • 抽象类

抽象类的特点

  1. 包含至少一个纯虚函数
  • 抽象类至少有一个纯虚函数。这是一种特殊的虚函数,在抽象类中没有具体实现,而是留给派生类去实现。
  • 纯虚函数的声明方式是在函数声明的末尾加上 = 0 。
  1. 不能直接实例化
  • 由于抽象类不完整,所以不能直接创建它的对象。就像你不能直接使用“交通工具”的概念去任何地方,你需要一个具体的交通工具。
  1. 用于提供基础结构
  • 抽象类的主要目的是为派生类提供一个共同的基础结构,确保所有派生类都有一致的接口和行为。

  • 纯虚函数-接口

一个类作为接口可以通过以下步骤来实现:

  1. 定义抽象类:创建一个包含纯虚函数的抽象类,这些函数构成了接口的一部分。这些函数在抽象类

中只有声明而没有具体的实现。

  1. 派生类实现接口:派生类继承抽象类,并实现其中的纯虚函数,以具体实现接口定义的方法。
class LiveMove{
public:
    virtual void eat() = 0;
    virtual void bite() = 0;
    virtual void drink() = 0;
    virtual void la() = 0;
};

class Dog : public LiveMove{
public:
    void eat() override{...};
    void bite() override{...};
    void drink() override{...};
    void la() override{...};
};

  • 线程的创建

  • 通过lambda表达式创建线程

    Lambda 格式:[捕获列表] (参数) 可选修饰 -> 返回类型 { 函数体 }  

    • 多线程场景中,最常用简化格式[&](){ 核心逻辑 }(引用捕获所有,无参无返回);
    • 捕获列表是关键:想修改外部变量用 [&],想只读用 [=],想精准控制用 [&var, num]
    • 线程安全:引用捕获要确保变量生命周期,值捕获要注意拷贝成本。

创建一个lambda表达式,定义变量i,将123传入lambda表达式,输出lambda表示式的内容

int main(int argc, char const *argv[]){
    std::thread th([](int i){
        std::cout << "test lambda" << i << std::endl;}, 123);
    th.join();    
    return 0;
}

 

在类成员函数里定义一个线程,访问类成员变量

class TestLambda{
public:
    void start(){
        std::string hr;
        std::thread th([this](){
            std::cout << this->name << std::endl;
        });
        th.join();
    }

private:
    std::string name = "hello";
};

int main(int argc, char const *argv[]){   
    TestLambda test;
    test.start();
    return 0;
}

  •  竞争状态、临界区、互斥锁

  1. 临界区:一段对「共享资源(如类成员变量、全局变量、堆内存)」进行读写操作的代码。核心特点:同一时间只能有一个线程执行这段代码,否则会出问题。
  2. 竞争状态:多个线程「同时进入临界区」,对共享资源进行读写操作,导致程序行为不可预测(结果不确定、数据损坏、崩溃)的现象。

示例:

创建了100个线程,并调到后台运行,产生竞争状态,导致运行结果不可预测

void TestThread(){
    //std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::cout << "====================" << std::endl;
    std::cout << "test 001" << std::endl;
    std::cout << "test 002" << std::endl;
    std::cout << "test 003" << std::endl;
    std::cout << "====================" << std::endl;
}

int main(int argc, char const *argv[]){
    //创建100个进程并调至后台运行,实现并行
    for(int i = 0; i < 100; ++i){
        std::thread th(TestThread);
        th.detach();
    }
    return 0;
}

为了解决竞争状态,设置互斥锁,建立临界区

通过定义锁,我们定义获取锁资源到释放锁资源的区间叫做临界区同一时间只能有一个进程访问临界区资源,只有当解锁后,其他被阻塞的线程才能重新获取重复上述操作

static std::mutex mtx;

void TestThread(){
    //获取锁资源,如果获取不到进去阻塞状态
    mtx.lock();
    std::cout << "====================" << std::endl;
    std::cout << "test 001" << std::endl;
    std::cout << "test 002" << std::endl;
    std::cout << "test 003" << std::endl;
    std::cout << "====================" << std::endl;
    mtx.unlock();
}

int main(int argc, char const *argv[]){
    for(int i = 0; i < 100; ++i){
        std::thread th(TestThread);
        th.detach();
    }
    return 0;
}

 

  • 常用的mutex函数

lock() 阻塞式获取锁:若锁未被占用则成功获取;若已被占用,线程阻塞直到锁释放。 必须获取锁才能执行临界区(等待可接受)
unlock() 手动释放锁:必须在 lock()/try_lock() 成功后调用,否则行为未定义(崩溃)。 手动管理锁时,临界区结束后释放锁
try_lock() 非阻塞式获取锁:锁未被占用则返回 true(成功);已被占用则返回 false(不阻塞)。 不想阻塞,可先做其他任务(如重试、跳过)
try_lock_for() 超时非阻塞获取:在 rel_time 时间内尝试获取锁,超时未获取则返回 false 允许短暂等待,但不想无限阻塞(如等待 1 秒)
try_lock_until() 绝对时间超时获取:尝试获取锁直到 abs_time 时间点,超时返回 false 需精确控制等待截止时间(如凌晨 3 点前)
  • 处理线程抢不到资源问题

我们总是会遇到,当一个线程解锁后,该进程又一次进入了临界区,使得原本等待解锁的被阻塞线程又得接着等待,使得处理效率变低。

通过每次线程解锁时让其线程睡眠,保证解锁后不会与其他被阻塞线程争夺。

示例:

void ThreadMainMtx(int i){
    for(;;){
        mtx.lock();
        std::cout << i << "[in]\n";
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        mtx.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(1));//解锁后,睡眠1s,再取争锁
    }
}

int main(int argc, char const *argv[]){
    for(int i = 0; i < 4; ++i){
        std::thread th(ThreadMainMtx, i + 1);
        th.detach();
    }
    return 0;
}

  • 超时锁

在 C++ 中,超时锁是一类支持限时尝试获取锁的互斥锁,核心作用是避免线程无限期阻塞在加锁操作上。如果在指定时间内成功获取锁,则执行临界区逻辑;若超时失败,则可执行备用逻辑(如打印日志、重试或放弃)。C++ 标准库提供了多种超时锁类型,核心基于 std::timed_mutex 及其变体实现。

超时锁的核心能力由两个成员函数提供:try_lock_for(相对时间)和 try_lock_until绝对时间)。

  • try_lock_for

   作用:在相对时间间隔内尝试获取锁,超时则返回 false

示例:

//超时锁
std::timed_mutex tmux;
void TestMainTime(int i){
    for(;;){
        //尝试在规定时间内获得锁
        if(!tmux.try_lock_for(std::chrono::seconds(1))){
            std::cout << "未能在规定时间内获得锁\n";
        }else{
            std::cout << std::this_thread::get_id() << std::endl;
            std::cout << i << " 在规定时间内获得锁\n";
            tmux.unlock();//获得锁后手动解锁
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    }
    
}
  • try_lock_until

  作用:在绝对时间点前尝试获取锁,超时则返回 false

 

  • 超时锁的核心类型

std::timed_mutex 基础超时互斥锁,不可递归(同一线程不可多次加锁) 大多数普通的超时锁场景
std::recursive_timed_mutex 递归超时互斥锁,允许同一线程多次加锁(需对应次数解锁)。 函数嵌套调用需要加锁的场景
std::shared_timed_mutex C++17 引入的共享超时互斥锁(读写锁),支持共享读锁独占写锁的超时获取。 读多写少的场景(如缓存、配置)

 


  • 递归锁

递归锁是 C++ 多线程中用于解决同一线程多次加锁场景的关键同步工具。

std::recursive_mutex(递归互斥锁)是 <mutex> 头文件中 std::mutex 的变体,其核心特性是:
 
  • 允许同一线程多次对其加锁(递归加锁),不会像普通 std::mutex 那样导致死锁;
  • 解锁次数必须与加锁次数严格一致(如加锁 3 次,需解锁 3 次),否则锁不会被真正释放;
  • 锁的所有权属于当前加锁线程,其他线程只有在当前线程完全解锁后才能获取锁。

  普通 std::mutex 的致命限制是同一线程多次加锁会立即死锁,而在嵌套函数加锁、递归函数加锁等场景中,同一线程不可避免需要多次获取同一把锁,此时递归锁是唯一选择。

  • 递归锁的核心类型

std::recursive_mutex 基础递归互斥锁,不支持超时加锁 C++11
std::recursive_timed_mutex 支持超时加锁(try_lock_for/try_lock_until C++11

示例:

//递归锁
std::timed_mutex mux;
std::recursive_mutex rmux;
void Task1(){
    rmux.lock();
    std::cout << "task1 is working" << std::endl;
    rmux.unlock(); 
}

void Task2(){
    rmux.lock();
    std::cout << "task2 is working" << std::endl;
    rmux.lock();
}

void ThreadMianSec(int i){
    for(;;){
        rmux.lock();
        Task1();
        std::cout << i << "[in]" << std::endl;
        std::this_thread::sleep_for(std::chrono::seconds(1));
        Task2();
        rmux.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

  • 共享锁

概念与本质

  • 定义:针对“多读少写”场景优化的同步原语,又称“读写锁
  • 核心思想:区分读/写操作的并发特性——读共享、写独占
  • 价值:解决普通互斥锁“读写均独占”的效率瓶颈,提升读并发性能

与普通互斥锁的核心差异

  • 普通互斥锁(std::mutex):无论读写,同一时间仅1线程持有,串行效率低
  • 共享锁:读操作可并发,写操作独占,平衡线程安全与并发效率
  • 适用场景边界:共享锁适合“读多写少”,普通互斥锁适合“读写频繁”或“单线程重入”
std::shared_timed_mutex
支持共享/独占锁 + 超时加锁接口
C++14
std::shared_mutex
仅基础共享/独占锁,无超时,性能略优
C++17

核心接口汇总

  • 共享锁(读锁)接口
lock_shared()
阻塞式获取共享锁,直到成功
unlock_shared()
释放共享锁,需与lock_shared()严格成对
try_lock_shared()
非阻塞获取,成功返回true,失败返回false
  • 独占锁(写锁)接口
lock()
阻塞式获取独占锁,与共享锁互斥
unlock()
释放独占锁,需与lock()严格成对
try_lock()
非阻塞获取独占锁
  • 超时接口(仅std::shared_timed_mutex)
try_lock_shared_for(chrono::duration)
指定时长内尝试获取共享锁
try_lock_shared_until(chrono::time_point)
指定时间点前尝试获取共享锁
try_lock_for()/try_lock_until()
独占锁的超时获取接口

示例:

// 共享互斥锁(C++14,兼容性更广)
std::shared_timed_mutex stmux;
// 共享数据
std::vector<int> g_data = {1, 2, 3};

// 读者线程:手动调用 lock_shared/unlock_shared
void reader(int id) {
    for (int i = 0; i < 3; ++i) { // 读3次后退出
        // 1. 获取共享锁(读锁)
        stmux.lock_shared();
        std::cout << "Reader " << id << " read: ";
        for (int num : g_data) std::cout << num << " ";
        std::cout << std::endl;
        // 2. 释放共享锁(必须成对调用)
        stmux.unlock_shared();

        // 无锁睡眠,避免频繁加锁
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
}

// 写者线程:用 lock/unlock 获取写锁
void writer(int id) {
    for (int i = 0; i < 2; ++i) { // 写2次后退出
        // 1. 获取独占锁(写锁)
        stmux.lock();
        g_data.push_back(id * 10 + i);
        std::cout << "Writer " << id << " wrote: " << id * 10 + i << std::endl;
        // 2. 释放独占锁
        stmux.unlock();

        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

 

posted @ 2025-12-10 00:44  菜鸡の编程日常  阅读(8)  评论(0)    收藏  举报