C++ 线程间通信(一)

目录

一、线程同步与线程异步

[1.1 定义](#1.1 定义)

[线程同步(Thread Synchronization)](#线程同步(Thread Synchronization))

[线程异步(Asynchronous Thread Operations)](#线程异步(Asynchronous Thread Operations))

[1.2 关键区别总结](#1.2 关键区别总结)

补充说明

二、同步机制

[2.1 互斥锁(Mutex)](#2.1 互斥锁(Mutex))

[2.1.1 定义](#2.1.1 定义)

[2.1.2 核心用途](#2.1.2 核心用途)

[2.1.3 工作原理](#2.1.3 工作原理)

[2.1.4 实现方式](#2.1.4 实现方式)

(1)基础锁:std::mutex

(2)RAII封装:std::lock_guard

(3)高级锁:std::unique_lock

[2.1.5 关键注意事项](#2.1.5 关键注意事项)

(1)死锁预防

(2)性能优化

(3)异常安全

[2.1.6 总结](#2.1.6 总结)

[2.2 条件变量(std::condition_variable)](#2.2 条件变量(std::condition_variable))

[2.2.1 核心作用](#2.2.1 核心作用)

[2.2.2 关键接口与行为](#2.2.2 关键接口与行为)

(1)等待函数

(2)通知函数

[2.2.3 典型使用流程](#2.2.3 典型使用流程)

[2.2.4 注意事项](#2.2.4 注意事项)

[2.2.5 底层原理](#2.2.5 底层原理)

[2.3 信号量(Semaphore)](#2.3 信号量(Semaphore))

[2.3.1 限制并发访问数量](#2.3.1 限制并发访问数量)

[2.3.2 资源管理](#2.3.2 资源管理)

[2.3.3 线程间的事件通知](#2.3.3 线程间的事件通知)

[2.4 原子操作(Atomic Operation)](#2.4 原子操作(Atomic Operation))

[2.4.1 C++原子操作](#2.4.1 C++原子操作)

核心特性

主要应用场景

优势

劣势

[2.4.2 C++互斥锁](#2.4.2 C++互斥锁)

核心特性

主要应用场景

优势

劣势

[2.4.3 对比总结](#2.4.3 对比总结)

[2.5 屏障(std::barrier / std::latch)](#2.5 屏障(std::barrier / std::latch))

[2.5.1 std::latch(单次屏障)](#2.5.1 std::latch(单次屏障))

[2.5.2 std::barrier(可复用屏障)](#2.5.2 std::barrier(可复用屏障))

[2.5.3 关键区别总结](#2.5.3 关键区别总结)

何时选择?


一、线程同步与线程异步

在C++中,线程同步和线程异步是处理多线程任务协作的两种核心机制。

1.1 定义

线程同步(Thread Synchronization)

定义:线程同步通过协调多个线程的执行顺序或资源访问,确保数据一致性和避免竞争条件(Race Condition)。同步机制会强制线程按特定规则执行,可能引发阻塞。

线程异步(Asynchronous Thread Operations)

定义 :线程异步指主线程无需等待子线程完成即可继续执行,子线程通过回调、事件或返回值(如std::future)通知结果。异步操作通常用于提升并发性能。

1.2 关键区别总结

特性 线程同步 线程异步
目的 协调线程执行顺序/资源访问 提升并发性能,避免主线程阻塞
阻塞行为 可能阻塞(如等待锁、条件变量) 非阻塞(主线程继续执行)
结果获取 直接访问共享数据或通过同步机制 通过回调、std::future或事件
复杂度 较高(需处理死锁、活锁等问题) 较高(需管理任务生命周期)

补充说明

  • 同步的代价:过度使用同步(如粗粒度锁)可能导致性能下降,需尽量缩小临界区范围。
  • 异步的挑战:需处理任务取消、异常传播等问题,C++20的协程(Coroutines)可简化异步代码。
  • 工具选择
    • 同步:std::mutexstd::lock_guardstd::condition_variable
    • 异步:std::asyncstd::future、线程池(如Intel TBB、Boost.Asio)。

通过合理结合同步与异步机制,可以构建高效且正确的多线程程序。

二、同步机制

2.1 互斥锁(Mutex)

C++互斥锁是一种用于多线程同步的核心机制,确保同一时刻只有一个线程能访问共享资源,避免数据竞争

2.1.1 定义

互斥锁(Mutex,全称Mutual Exclusion)通过"锁定-解锁"机制实现线程同步,确保共享资源(如全局变量、文件、内存等)的互斥访问

2.1.2 核心用途

  • 保护共享资源:防止多个线程同时读写导致数据损坏。
  • 线程同步:控制操作顺序(如初始化、状态修改)。
  • 避免数据竞争:解决多线程无序访问同一数据的不可预测行为。

2.1.3 工作原理

锁定(Lock)

  • 线程尝试获取锁时,若锁未被占用,则成功获取并进入临界区;否则阻塞等待。
  • 示例:mtx.lock()std::mutex)。

访问共享资源

  • 持有锁的线程安全操作共享数据。

解锁(Unlock)

  • 线程释放锁,唤醒等待队列中的其他线程。
  • 示例:mtx.unlock()

2.1.4 实现方式

(1)基础锁:std::mutex
  • 手动管理 :需显式调用lock()unlock(),易因异常或遗漏导致死锁。
  • 示例
cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int counter = 0;

void increment() {
    mtx.lock();
    ++counter;
    mtx.unlock();
}

int main() {
    std::thread t1(increment), t2(increment);
    t1.join(); t2.join();
    std::cout << "Counter: " << counter << std::endl; // 输出2
}
(2)RAII封装:std::lock_guard
  • 自动管理:构造时加锁,离开作用域(析构时)解锁,避免忘记释放。
  • 示例
cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;

void safe_increment(int& counter) {
    std::lock_guard<std::mutex> lock(mtx);
    ++counter;
} // 自动解锁

int main() {
    int counter = 0;
    std::thread t1(safe_increment, std::ref(counter)), 
                t2(safe_increment, std::ref(counter));
    t1.join(); t2.join();
    std::cout << "Counter: " << counter << std::endl; // 输出2
}
(3)高级锁:std::unique_lock
  • 灵活性:支持延迟锁定、手动解锁、条件变量配合。
  • 示例
cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void worker() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return ready; }); // 等待条件满足
    std::cout << "Worker processed data." << std::endl;
}

int main() {
    std::thread t(worker);
    {
        std::lock_guard<std::mutex> lock(mtx);
        ready = true;
    }
    cv.notify_one(); // 唤醒线程
    t.join();
}
  • **std::unique_lock**比 lock_guard 强在哪

    • 控制权更灵活 ‌:std::lock_guard 构造时就必须加锁,析构时才解锁,中间无法干预;std::unique_lock 允许在作用域内手动调用 unlock() 提前释放锁,用完再 lock() 重新加锁,适合临界区中间夹杂耗时操作的场景 。

    • 支持锁所有权转移‌:std::unique_lock 对象不可复制但支持移动(std::move),可以将锁的管理权从一个对象转移到另一个对象,而 std::lock_guard 既不可复制也不可移动 。

    • 条件变量必选搭档‌:在使用 std::condition_variable 进行线程等待或通知时,必须搭配 std::unique_lock,std::lock_guard 无法配合使用,这是实现生产者 - 消费者模型的基础 。‌‌‌

2.1.5 关键注意事项

(1)死锁预防
  • 避免嵌套锁 :确保加锁顺序一致,或使用std::lock同时锁定多个互斥量。
  • 示例
cpp 复制代码
std::mutex mtx1, mtx2;
std::lock(mtx1, mtx2); // 原子化锁定,避免死锁
std::lock_guard<std::mutex> lock1(mtx1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mtx2, std::adopt_lock);
(2)性能优化
  • 缩小临界区:仅保护必要代码,减少锁持有时间。
  • 读写锁 :读多写少场景使用std::shared_mutex提升并发性。
(3)异常安全
  • 优先使用std::lock_guardstd::unique_lock,确保异常时自动释放锁。

2.1.6 总结

  • 选择工具
    • 简单场景:std::mutex + std::lock_guard
    • 复杂同步:std::unique_lock + 条件变量。
  • 核心原则最小化锁粒度、避免死锁、确保异常安全

通过合理使用互斥锁,可以高效解决多线程数据竞争问题,同时平衡性能与安全性。

2.2 条件变量(std::condition_variable

C++条件变量(std::condition_variable)是多线程同步的核心工具,用于实现线程间的高效通信和条件等待

2.2.1 核心作用

条件变量与互斥锁(std::unique_lock)配合使用,允许线程在满足特定条件前主动阻塞,避免忙等待(busy-waiting),提升性能。典型场景包括:

  • 生产者-消费者模型:消费者等待生产者填充数据。
  • 任务调度:工作线程等待任务队列非空。
  • 资源同步:线程等待共享资源可用。

2.2.2 关键接口与行为

(1)等待函数
  • wait(unique_lock<mutex>& lock)
    • 原子操作:释放锁 → 阻塞线程 → 被唤醒后重新加锁。
    • 虚假唤醒:可能被无理由唤醒,需结合谓词(Predicate)检查条件。
  • wait(unique_lock<mutex>& lock, Predicate pred)
    • 仅当 pred() 返回 false 时阻塞,唤醒后重新检查 pred(),避免虚假唤醒。
cpp 复制代码
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

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

void worker() {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return data_ready; }); // 谓词检查
    std::cout << "Data processed!\n";
}

int main() {
    std::thread t(worker);
    {
        std::lock_guard<std::mutex> lock(mtx);
        data_ready = true;
    }
    cv.notify_one();
    t.join();
    return 0;
}
  • wait_for() / wait_until()
    • 支持超时等待,返回 cv_status::timeoutcv_status::no_timeout
(2)通知函数
  • notify_one()
    唤醒一个等待线程(具体哪个不确定)。
  • notify_all()
    唤醒所有等待线程,适用于多消费者场景。

2.2.3 典型使用流程

  1. 等待线程
    • 获取 std::unique_lock<std::mutex>
    • 调用 cv.wait(lock, predicate) 检查条件。
  2. 通知线程
    • 修改共享变量(需持有锁)。
    • 调用 cv.notify_one()cv.notify_all()

示例:生产者-消费者模型

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

std::mutex mtx;
std::condition_variable cv;
std::queue<int> task_queue;
bool stop_flag = false;

void producer() {
    for (int i = 0; i < 5; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        task_queue.push(i);
        std::cout << "Produced: " << i << "\n";
        cv.notify_one();
    }
    {
        std::lock_guard<std::mutex> lock(mtx);
        stop_flag = true;
        cv.notify_all();
    }
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [] { return !task_queue.empty() || stop_flag; });
        if (stop_flag && task_queue.empty()) break;
        int task = task_queue.front();
        task_queue.pop();
        std::cout << "Consumed: " << task << "\n";
    }
}

int main() {
    std::thread p(producer);
    std::thread c(consumer);
    p.join();
    c.join();
    return 0;
}

2.2.4 注意事项

  • 必须与 std::unique_lock 配合std::condition_variable 仅支持 unique_lock,而非 lock_guard
  • 避免唤醒丢失:确保通知在等待之后发出(通常通过锁保护共享状态)。
  • 性能权衡notify_all() 可能引发大量线程竞争,需根据场景选择。
  • 通用版本std::condition_variable_any 支持任意锁类型,但性能较低。

2.2.5 底层原理

  • 线程阻塞与唤醒 :依赖操作系统提供的条件变量机制(如 Linux 的 futex)。
  • 原子性保证wait() 的锁释放与线程阻塞是原子的,防止竞态条件。

通过合理使用条件变量,可以高效实现线程同步,避免资源浪费和死锁风险。

2.3 信号量(Semaphore)

在C++多线程编程中,信号量(Semaphore) 是一种非常重要的同步原机制。主要支持限制并发数和资源管理。当然也支持线程间事件通知。

C++对信号量的支持主要分为两个阶段:

  1. C++20 及以后:标准库直接提供了 <semaphore> 头文件,包含标准的信号量类。

  2. C++20 以前:没有直接的信号量类,需要使用 std::mutex 和 std::condition_variable 手动封装。

这边我讲C++20以前的写法,C++20以后更简单触类旁通。

在 C++11/14/17 中,由于没有标准的 <semaphore>,通常的做法是结合 互斥锁 (std::mutex)条件变量 (std::condition_variable) 来实现。

2.3.1 限制并发访问数量

由于 C++11 未引入 std::semaphore,开发者通常通过以下方式模拟信号量:

  • 互斥锁:std::unique_lock 保护临界区。
  • 原子计数器 :使用 std::atomic<int> 记录资源数量。
  • 条件变量 :通过 std::condition_variable 实现线程阻塞与唤醒。

适用于线程池限流或同步的辅助工具。

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

class Semaphore {
private:
    std::mutex mtx;
    std::condition_variable cv;
    std::atomic<int> count;

public:
    Semaphore(int initial_count) : count(initial_count) {}

    void acquire() {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [this]() { return count > 0; });
        --count;
    }

    void release() {
        std::unique_lock<std::mutex> lock(mtx);
        ++count;
        cv.notify_one();
    }
};

// 使用示例:限制并发数
Semaphore pool(3); // 最大并发数 3

void worker(int id) {
    pool.acquire();
    std::cout << "Thread " << id << " is working.\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Thread " << id << " finished.\n";
    pool.release();
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 6; ++i) {
        threads.emplace_back(worker, i);
    }
    for (auto& t : threads) {
        t.join();
    }
    return 0;
}

输出:

cpp 复制代码
Thread 0 is working.
Thread 1 is working.
Thread 2 is working.
Thread 0 finished.
Thread 3 is working.  // 线程 3 等待线程 0 释放许可后执行
...

2.3.2 资源管理

协调生产者和消费者的速度,避免缓冲区溢出或下溢。

  • 使用两个信号量:empty_slots(空位数量)和filled_slots(数据项数量)。
  • 生产者等待empty_slots,消费者等待filled_slots

示例代码(生产者-消费者模型,C++11 实现)

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

class Semaphore {
private:
    std::mutex mtx;
    std::condition_variable cv;
    int count;

public:
    Semaphore(int initial_count) : count(initial_count) {}

    void acquire() {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [this]() { return count > 0; });
        --count;
    }

    void release() {
        std::unique_lock<std::mutex> lock(mtx);
        ++count;
        cv.notify_one();
    }
};

std::queue<int> buffer;
const int buffer_size = 3;
Semaphore empty_slots(buffer_size);// 空位信号量
Semaphore filled_slots(0);         // 数据信号量

void producer() {
    for (int i = 0; i < 5; ++i) {
        empty_slots.acquire();    // 等待空位
        buffer.push(i);
        std::cout << "Produced: " << i << "\n";
        filled_slots.release();   // 通知消费者
    }
}

void consumer() {
    for (int i = 0; i < 5; ++i) {
        filled_slots.acquire();  // 等待数据
        int item = buffer.front();
        buffer.pop();
        std::cout << "Consumed: " << item << "\n";
        empty_slots.release();   // 通知生产者
    }
}

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

输出:

cpp 复制代码
Produced: 0
Consumed: 0
Produced: 1
Consumed: 1
...  // 严格交替执行,缓冲区大小限制为 3

2.3.3 线程间的事件通知

  • 场景:一个线程等待另一个线程完成特定任务后再继续执行。
  • 示例
    • 线程A等待线程B初始化完成后才开始工作。
cpp 复制代码
#include <iostream>
#include <thread>
#include <semaphore>

std::binary_semaphore ready(0); // 二进制信号量(初始为0)

void worker() {
    std::cout << "Worker initializing...\n";
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "Worker ready!\n";
    ready.release(); // 通知主线程
}

int main() {
    std::thread t(worker);
    ready.acquire(); // 等待worker就绪
    std::cout << "Main thread continues after worker is ready!\n";
    t.join();
    return 0;
}

2.4 原子操作(Atomic Operation)

讲原子操作那就不得不跟锁比较一番。

2.4.1 C++原子操作

核心特性
  • 不可分割性:操作要么完全执行,要么完全不执行,不存在中间状态。
  • 内存可见性:一个线程对原子变量的修改能被其他线程及时看到。
  • 指令有序性:通过内存序控制编译器和CPU的指令重排,保证操作执行顺序符合预期。
主要应用场景
  • 简单的变量读写、累加、状态切换:例如计数器、标志位更新等。
  • 无锁数据结构:如无锁队列、无锁栈等。
优势
  • 高性能:原子操作通常在用户态完成,无内核切换开销,延迟远低于互斥锁。
  • 无死锁风险:不涉及锁的获取和释放,避免了死锁问题。
  • 可控性强:允许精细控制内存对齐、CPU缓存等底层细节,适合对延迟要求极高的场景(如自动驾驶)。
劣势
  • 功能受限:仅保护单个变量的单次操作,无法保护复杂的临界区代码块。
  • 调试难度大:内存序、ABA问题等导致的bug极难调试。
cpp 复制代码
#include <atomic>
#include <thread>
#include <iostream>

std::atomic<int> counter(0);

void increment() {
    for (int i = 0; i < 100000; ++i) {
        counter++;  // 原子自增
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Final counter value: " << counter << std::endl;  // 输出: 200000
    return 0;
}

2.4.2 C++互斥锁

核心特性
  • 独占访问:同一时间只有一个线程能进入临界区,保证操作的独占性。
  • 多种锁类型 :包括基本互斥锁(std::mutex)、递归锁(std::recursive_mutex)、带超时的锁(std::timed_mutex)和读写锁(std::shared_mutex)等。
主要应用场景
  • 复杂的多步操作:如修改多个关联变量、操作复杂结构体等。
  • 资源访问控制:需要保护任意复杂的临界区代码块时。
优势
  • 功能全面:可保护任意复杂的临界区代码块,支持多种锁类型以满足不同需求。
  • 调试相对简单:临界区逻辑清晰,调试难度低于原子操作。
劣势
  • 性能开销大:加锁/解锁会触发内核态上下文切换,高并发下锁竞争会导致性能急剧下降。
  • 死锁风险:多锁场景下存在死锁风险,需谨慎设计加锁顺序。
cpp 复制代码
#include <mutex>
#include <thread>
#include <iostream>

std::mutex mtx;
int shared_data = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);  // 自动加锁和解锁
        ++shared_data;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Final shared_data value: " << shared_data << std::endl;  // 输出: 200000
    return 0;
}

2.4.3 对比总结

特性 原子操作 互斥锁
执行模式 用户态完成,无线程阻塞 内核态同步,线程阻塞会触发上下文切换
保护范围 仅保护单个变量的单次操作 可保护任意复杂的临界区代码块
死锁风险 无死锁风险 多锁场景下有死锁风险
性能表现 低延迟,高并发下性能稳定 高并发下锁竞争激烈,性能急剧下降
调试难度 内存序、ABA问题导致的bug极难调试 临界区逻辑清晰,调试相对简单
适用场景 简单变量操作、无

2.5 屏障(std::barrier / std::latch)

std::latchstd::barrier都是C++20引入的线程同步工具,但它们的用途和行为有显著区别

2.5.1 std::latch(单次屏障)

  • 用途:用于同步多个线程到某个单次执行的点(计数器归零后不可复用)。
  • 特点
    • 初始化时设置计数器值,线程通过count_down()减少计数器。
    • 提供wait()arrive_and_wait()阻塞线程,直到计数器归零。
    • 不可复用:计数器归零后无法重置或增加。
    • 适用于一次性任务(如初始化阶段、任务完成后的清理)。
  • 示例代码
cpp 复制代码
#include <iostream>
#include <latch>
#include <thread>
#include <vector>

int main() {
    const int N = 3;
    std::latch latch(N); // 初始化计数器为3

    auto worker = [&latch](int id) {
        std::cout << "Worker " << id << " started\n";
        latch.count_down(); // 减少计数器
        std::cout << "Worker " << id << " finished\n";
    };

    std::vector<std::thread> threads;
    for (int i = 0; i < N; ++i) {
        threads.emplace_back(worker, i);
    }

    latch.wait(); // 阻塞主线程直到计数器归零
    std::cout << "All workers completed\n";

    for (auto& t : threads) {
        t.join();
    }
    return 0;
}

输出:

cpp 复制代码
Worker 0 started
Worker 1 started
Worker 2 started
Worker 2 finished
Worker 0 finished
Worker 1 finished
All workers completed

2.5.2 std::barrier(可复用屏障)

  • 用途:用于同步多个线程到周期性执行的点(计数器可重置)。
  • 特点
    • 初始化时设置计数器值,线程通过arrive_and_wait()减少计数器并阻塞。
    • 可复用:计数器归零后自动重置(或通过回调函数重置)。
    • 适用于循环任务(如迭代计算、帧渲染)。
  • 示例代码
cpp 复制代码
#include <iostream>
#include <barrier>
#include <thread>
#include <vector>

int main() {
    const int N = 3;
    const int iterations = 2;
    std::barrier barrier(N); // 初始化计数器为3

    auto worker = [&barrier](int id) {
        for (int i = 0; i < iterations; ++i) {
            std::cout << "Worker " << id << " iteration " << i << "\n";
            barrier.arrive_and_wait(); // 等待所有线程到达
        }
    };

    std::vector<std::thread> threads;
    for (int i = 0; i < N; ++i) {
        threads.emplace_back(worker, i);
    }

    for (auto& t : threads) {
        t.join();
    }
    return 0;
}

输出:

cpp 复制代码
Worker 0 iteration 0
Worker 1 iteration 0
Worker 2 iteration 0
Worker 0 iteration 1
Worker 1 iteration 1
Worker 2 iteration 1

2.5.3 关键区别总结

特性 std::latch std::barrier
复用性 单次使用(计数器不可重置) 可复用(计数器自动或通过回调重置)
主要方法 count_down(), wait() arrive_and_wait()
典型场景 一次性任务(如初始化、清理) 周期性任务(如迭代计算)
初始化 std::latch(N) std::barrier(N)

何时选择?

  • std::latch:当需要同步线程到一个固定点(如所有线程完成初始化后继续执行)。
  • std::barrier:当需要同步线程到多个周期性点(如每轮迭代后等待其他线程)。

通过合理选择这两个工具,可以高效实现复杂的线程同步逻辑。

相关推荐
hautcyh2 小时前
C++new和delete
c++
不会C语言的男孩2 小时前
C++ Primer Plus 第10章:对象和类
开发语言·c++
不会C语言的男孩2 小时前
C++ Primer Plus 第11章:使用类
开发语言·c++
comedate3 小时前
FMT_UNICODE 与 CUDA 编码配置专栏技术文档
c++·utf-8·nvcc
玖玥拾3 小时前
C/C++ 基础笔记(二)
c语言·c++
故事和你914 小时前
洛谷-【动态规划2】线性状态动态规划4
开发语言·数据结构·c++·算法·动态规划·图论
不吃土豆的马铃薯4 小时前
Socket 网络编程实战教程
linux·服务器·开发语言·网络·c++·算法
零号全栈寒江独钓4 小时前
c++跨平台实现日志重定向
linux·c++·windows
小成202303202654 小时前
从C到C++
开发语言·c++