当前位置: 首页 > news >正文

C++ :实现多线程编程

多线程

  • 1. 多线程基本原理
    • 1.1 核心原理
      • 线程与进程的关系
      • 并发与并行
      • 线程的调度与切换
      • 共享资源与同步
    • 1.2 多线程的优势与挑战
  • 2. C++ 标准库多线程(`<thread>`)
    • 2.1 thread主要函数
      • 1.构造函数(创建线程)
      • 2.`join()` 与 `detach()`(线程生命周期管理)
      • 3.`joinable()`(检查线程状态)
      • 4.`swap()`(交换线程对象)
      • 5.`get_id()`(获取线程 ID)
      • 6.静态函数 `hardware_concurrency()`
      • 7.移动赋值运算符
      • 8. 总结
    • 2.2 std::mutex线程同步类
      • 1.核心成员函数
        • **`lock()`**
        • **`unlock()`**
        • **`try_lock()`**
      • 2. 配套的锁管理工具(RAII 机制)
        • **`std::lock_guard`(简单自动锁)**
        • **`std::unique_lock`(灵活自动锁)**
      • 3.其他互斥锁类型
        • **`std::recursive_mutex`**
        • **`std::timed_mutex`**
        • 3. **`std::recursive_timed_mutex`**
      • 4. 适用场景总结
    • 2.3 基本用法
      • 1. 创建线程
      • 2. 线程同步:避免数据竞争
        • 互斥锁(`std::mutex`)
        • 条件变量(`std::condition_variable`)
        • 原子变量(`std::atomic`)
  • 3. 平台特定的多线程实现
    • 3.1 Windows 平台:`CreateThread`(Win32 API)
    • 3.2 Linux 平台:`pthread`(POSIX 线程)**
  • 4. 多线程编程注意事项**

在 C++ 中实现多线程操作主要依赖于 C++11 及后续标准引入的 <thread> 库,这是跨平台的标准方案。此外,也可以使用平台特定的 API(如 Windows 的 CreateThread 或 Linux 的 pthread),但推荐优先使用标准库以保证跨平台兼容性。

1. 多线程基本原理

多线程的基本原理是在单个进程中并发多个执行流(线程),这些线程共享进程的资源(如内存空间、文件描述符等),但拥有独立的执行上下文(如程序计数器、栈空间),从而实现并发执行

1.1 核心原理

线程与进程的关系

  • 进程:操作系统分配资源的基本单位(拥有独立的内存空间、文件句柄等)。
  • 线程:进程内的执行单元,是CPU调度的基本单位。一个进程可以包含多个线程,所有线程共享进程的资源,但各自有独立的执行路径。

举例
一个文本编辑器进程中,可能有3个线程:

  • 线程1:处理用户输入(键盘/鼠标)
  • 线程2:实时自动保存文件
  • 线程3:拼写检查

这些线程共享编辑器的内存(如当前编辑的文本内容),但各自独立运行,用户输入时不会阻塞自动保存。

并发与并行

  • 并发:多个线程“交替执行”(宏观上同时推进,微观上CPU在多个线程间快速切换)。
    例如:单核CPU中,线程A执行10ms → 切换到线程B执行10ms → 切换回线程A,看起来像同时运行。
  • 并行:多个线程“真正同时执行”(需要多核CPU支持)。
    例如:双核CPU中,线程A在核心1运行,线程B在核心2运行,物理上同时进行。

多线程的核心价值是通过并发或并行提高程序效率,尤其是在I/O密集型(如网络请求、文件读写)或CPU密集型(如数据计算)场景。

线程的调度与切换

操作系统通过线程调度器决定哪个线程获得CPU时间,调度策略通常有:

  • 时间片轮转:每个线程分配固定时间片(如10ms),超时后切换到下一线程。
  • 优先级调度:高优先级线程优先获得CPU(如实时任务)。

线程切换时,操作系统会保存当前线程的上下文(程序计数器、寄存器、栈指针等),加载新线程的上下文,这个过程称为上下文切换。切换开销远小于进程切换(因线程共享资源,无需重新分配内存等)。

共享资源与同步

线程共享进程资源(如全局变量、堆内存),但多线程同时操作共享资源会导致数据竞争(结果不一致)。例如:

// 两个线程同时执行:
int count = 0;
void increment() {count++; // 非原子操作(读取→加1→写入)
}

若两个线程同时读取 count=0,都加1后写入,最终结果可能是 1 而非预期的 2

因此需要同步机制保证共享资源的安全访问,如:

  • 互斥锁(Mutex):同一时间只允许一个线程访问临界区。
  • 信号量(Semaphore):控制同时访问资源的线程数量。
  • 条件变量(Condition Variable):实现线程间的等待/通知(如“生产者-消费者”模型)。

1.2 多线程的优势与挑战

  • 优势

    1. 提高资源利用率:CPU在等待I/O(如文件读取)时,可切换到其他线程执行。
    2. 响应速度提升:UI线程不被耗时操作阻塞(如后台下载时界面仍可交互)。
    3. 简化程序设计:将不同任务拆分到独立线程,逻辑更清晰。
  • 挑战

    1. 同步复杂性:不当的锁机制可能导致死锁(如线程A持有锁1等待锁2,线程B持有锁2等待锁1)。
    2. 上下文切换开销:频繁切换线程会消耗CPU资源,过多线程反而降低效率。
    3. 调试难度:多线程的执行顺序不确定,可能出现偶发的“线程安全”问题。

多线程的核心是在共享资源的基础上实现并发执行,通过操作系统的调度机制实现宏观上的“同时运行”,并通过同步机制解决共享资源的冲突问题。其设计目标是充分利用CPU资源,提升程序的执行效率和响应速度。

2. C++ 标准库多线程(<thread>

C++11 引入的 <thread> 库提供了简洁的多线程接口,核心类是 std::thread,配合同步机制(如互斥锁、条件变量)可实现安全的多线程操作。

2.1 thread主要函数

在 C++11 及后续标准中,std::thread 是多线程编程的核心类,定义于 <thread> 头文件,用于创建和管理线程。以下是其主要成员函数及参数的详细讲解:

1.构造函数(创建线程)

std::thread 的构造函数用于创建线程对象并关联线程函数(或可调用对象),线程在构造完成后立即启动(除非使用默认构造函数)。

  • 默认构造函数
std::thread t;  // 创建一个空线程对象(不关联任何线程)

功能:创建一个未执行任何任务的线程对象,可后续通过 swap 或移动赋值关联线程。

  • 带函数参数的构造函数
template <class F, class... Args>
explicit thread(F&& f, Args&&... args);

f:线程要执行的函数、lambda 表达式、函数对象等可调用对象。
args...:传递给函数 f 的参数(可变参数列表)。
功能:创建线程对象,立即执行 f(args...)
示例

// 普通函数
void func(int a, std::string b) { /* ... */ }
std::thread t1(func, 10, "hello");  // 线程执行 func(10, "hello")// lambda 表达式
std::thread t2([](int x) { /* ... */ }, 20);  // 线程执行 lambda(20)
  • 移动构造函数
thread(thread&& other) noexcept;

参数:other 是另一个 std::thread 对象(右值引用)。
功能:转移线程所有权,other 会变为空线程(不再关联原线程)。
示例:

std::thread t1(func);
std::thread t2 = std::move(t1);  // t2 接管 t1 的线程,t1 变为空

2.join()detach()(线程生命周期管理)

这两个函数是线程管理的核心,必须在线程对象销毁前调用其中一个,否则程序会调用 std::terminate() 终止。

void join();

功能:阻塞当前线程(通常是主线程),等待被调用的线程执行完毕,然后回收线程资源。
注意

  • 只能对可 joinable 的线程调用(即非空线程),否则抛出 std::system_error
  • 调用后线程对象变为不可 joinablejoinable() 返回 false)。
    示例
std::thread t(func);
t.join();  // 主线程等待 t 执行完毕
void detach();

功能:将线程与线程对象分离,线程成为“后台线程”,由系统自动回收资源,调用者无需等待。
注意

  • 分离后,线程对象不再关联该线程(joinable() 返回 false)。
  • 必须确保线程访问的资源(如局部变量)生命周期长于线程,否则会导致未定义行为。
    示例
std::thread t(func);
t.detach();  // 线程在后台运行,主线程无需等待

3.joinable()(检查线程状态)

bool joinable() const noexcept;
  • 功能:判断线程对象是否关联一个可执行的线程(即是否可以调用 join()detach())。
  • 返回值
    • true:线程正在运行或等待执行(可 join/detach)。
    • false:空线程、已 join/detach 的线程。
  • 示例
    std::thread t;
    std::cout << t.joinable() << std::endl;  // 输出 0(空线程)std::thread t2(func);
    std::cout << t2.joinable() << std::endl;  // 输出 1(可 join/detach)
    t2.join();
    std::cout << t2.joinable() << std::endl;  // 输出 0(已 join)
    

4.swap()(交换线程对象)

void swap(thread& other) noexcept;
  • 参数:另一个 std::thread 对象 other
  • 功能:交换两个线程对象关联的线程。
  • 示例
    std::thread t1(func1);
    std::thread t2(func2);
    t1.swap(t2);  // t1 现在关联 func2 的线程,t2 关联 func1 的线程
    

5.get_id()(获取线程 ID)

std::thread::id get_id() const noexcept;
  • 功能:返回线程的唯一标识符(std::thread::id 类型),空线程返回默认 id
  • 用途:用于区分不同线程,调试或日志记录。
  • 示例
    std::thread t(func);
    std::cout << "线程 ID: " << t.get_id() << std::endl;
    

6.静态函数 hardware_concurrency()

static unsigned int hardware_concurrency() noexcept;
  • 功能:返回当前系统的并发线程数(通常是 CPU 核心数),可用于决定创建线程的数量。
  • 返回值:核心数估计值(若无法确定则返回 0)。
  • 示例
    std::cout << "CPU 核心数: " << std::thread::hardware_concurrency() << std::endl;
    

7.移动赋值运算符

thread& operator=(thread&& other) noexcept;
  • 功能:将 other 的线程所有权转移给当前对象,若当前对象已关联线程,则先调用 terminate() 终止原线程。
  • 示例
    std::thread t1(func1);
    std::thread t2;
    t2 = std::move(t1);  // t2 接管 t1 的线程,t1 变为空
    

8. 总结

  • 注意事项
    1. 线程对象不可复制std::thread 禁用拷贝构造和拷贝赋值,只能移动(move)。
    2. 必须调用 join()detach():线程对象销毁前未调用这两个函数会导致程序异常终止。
    3. 线程函数的参数传递:参数会被复制到线程的内部存储,若需传递引用,需用 std::ref 包装(确保引用对象生命周期有效)。
    void func(int& x) { x++; }
    int a = 0;
    std::thread t(func, std::ref(a));  // 传递引用(a 需在 t 执行期间有效)
    t.join();
    
    1. 异常安全:若线程函数抛出未捕获的异常,程序会调用 std::terminate() 终止,需在函数内部捕获异常。

std::thread 的核心函数围绕线程的创建、生命周期管理和状态查询:

  • 构造函数用于创建线程并绑定任务。
  • join()detach() 控制线程的等待与分离。
  • joinable()get_id() 用于线程状态查询。
  • 移动语义支持线程所有权的转移。

掌握这些函数是 C++ 多线程编程的基础,配合互斥锁、条件变量等同步机制可实现安全高效的并发操作。

2.2 std::mutex线程同步类

std::mutex 是 C++ 标准库中用于线程同步的核心类,定义于 <mutex> 头文件,用于保护共享资源,避免多个多个线程同时访问导致的数据竞争。其主要函数及适用场景如下:

1.核心成员函数

lock()
void lock();
  • 功能:锁定互斥锁。
    • 若锁未被其他线程持有,当前线程会获得锁并立即返回。
    • 若锁已被其他线程持有,当前线程会阻塞(暂停执行),直到获得锁。
  • 注意
    • 同一线程对已锁定的 mutex 再次调用 lock() 会导致死锁(线程永久阻塞)。
    • 必须确保锁定后最终会解锁,否则其他线程会永久阻塞。
unlock()
void unlock();
  • 功能:解锁互斥锁,释放对锁的所有权,允许其他线程获取该锁。
  • 注意
    • 只能对当前线程已锁定的 mutex 调用 unlock(),否则行为未定义(可能崩溃)。
    • 通常与 lock() 配对使用,且需确保在所有退出路径(如异常、分支)都能解锁。
try_lock()
bool try_lock();
  • 功能:尝试锁定互斥锁(非阻塞)。
    • 若锁未被持有,当前线程获得锁并返回 true
    • 若锁已被持有,立即返回 false(不阻塞)。
  • 适用场景:需要非阻塞获取锁的场景(如超时等待、尝试性操作)。

2. 配套的锁管理工具(RAII 机制)

直接使用 lock()unlock() 容易因忘记解锁或异常导致死锁,因此通常配合 RAII(资源获取即初始化)风格的工具类使用:

std::lock_guard(简单自动锁)
template <class Mutex>
class lock_guard;
  • 功能:构造时自动调用 mutex.lock(),析构时自动调用 mutex.unlock(),确保锁的释放。
  • 适用场景:临界区范围明确的情况(如一个函数或代码块)。
  • 示例
    std::mutex mtx;
    int shared_data = 0;void increment() {std::lock_guard<std::mutex> lock(mtx);  // 构造时锁定shared_data++;  // 临界区操作// 析构时自动解锁(无论是否发生异常)
    }
    
std::unique_lock(灵活自动锁)
template <class Mutex>
class unique_lock;
  • 功能:比 lock_guard 更灵活,支持手动加锁/解锁、延迟锁定、超时等待等。
  • 常用方法
    • lock():手动锁定。
    • unlock():手动解锁。
    • try_lock():尝试锁定(非阻塞)。
    • try_lock_for(duration):超时等待锁定(阻塞指定时长)。
  • 适用场景
    • 临界区需要中途解锁(如先加锁检查条件,不满足则解锁等待)。
    • 配合条件变量(std::condition_variable)使用(必须用 unique_lock)。
  • 示例
    std::mutex mtx;
    std::condition_variable cv;
    bool ready = false;void worker() {std::unique_lock<std::mutex> lock(mtx);  // 可手动控制的锁cv.wait(lock, []{ return ready; });  // 等待时会临时解锁// 被唤醒后重新获得锁,执行后续操作
    }
    

3.其他互斥锁类型

C++ 标准库还提供了针对特殊场景的互斥锁变种:

std::recursive_mutex
  • 特点:允许同一线程多次锁定(递归锁定),需对应次数的解锁。
  • 适用场景:递归函数中需要锁定同一资源(但应尽量避免,可能隐藏设计问题)。
std::timed_mutex
  • 特点:支持带超时的锁定(try_lock_fortry_lock_until)。
  • 适用场景:需要限制锁等待时间的场景,避免永久阻塞。
3. std::recursive_timed_mutex
  • 特点:结合 recursive_mutextimed_mutex 的功能,支持递归锁定和超时。

4. 适用场景总结

函数/类核心功能适用场景
mutex::lock()阻塞锁定确保临界区独占访问
mutex::try_lock()非阻塞尝试锁定避免线程长时间阻塞(如尝试获取锁失败后做其他事)
lock_guard自动加锁/解锁(RAII)简单临界区(范围明确,无需中途解锁)
unique_lock灵活控制的自动锁条件变量、中途解锁、超时等待等复杂场景
recursive_mutex支持递归锁定递归函数中访问共享资源(谨慎使用)
timed_mutex带超时的锁定需限制锁等待时间的场景
  1. 死锁风险

    • 避免多个线程以不同顺序获取多个锁(如线程 1 先锁 A 再锁 B,线程 2 先锁 B 再锁 A)。
    • 可使用 std::lock 函数同时锁定多个锁,避免顺序问题:
      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. 锁的粒度

    • 锁的范围应尽可能小(只保护必要的共享操作),避免影响并发性能。
    • 避免在锁持有期间执行耗时操作(如 I/O、sleep)。
  3. 线程安全设计

    • 共享资源必须始终被锁保护,避免“漏掉”的访问导致数据竞争。
    • 优先使用 lock_guard 而非手动 lock()/unlock(),减少人为错误。

std::mutex 及其相关工具是 C++ 多线程同步的基础,核心作用是通过互斥访问保护共享资源。实际开发中,推荐优先使用 lock_guard(简单场景)和 unique_lock(复杂场景),结合具体需求选择合适的互斥锁类型,同时注意避免死锁和优化锁粒度。

2.3 基本用法

1. 创建线程

通过 std::thread 类创建线程,传入线程函数(或可调用对象)作为参数。

#include <iostream>
#include <thread>  // 标准线程库// 线程函数
void thread_func(int id) {std::cout << "线程 " << id << " 启动" << std::endl;// 线程任务...
}int main() {// 创建线程:传入函数和参数std::thread t1(thread_func, 1);std::thread t2(thread_func, 2);// 等待线程执行完毕(必须调用,否则程序可能提前退出)t1.join();  // 阻塞主线程,直到 t1 完成t2.join();  // 阻塞主线程,直到 t2 完成std::cout << "所有线程执行完毕" << std::endl;return 0;
}
  • 关键函数
    • join():主线程等待子线程完成,回收资源。
    • detach():将线程与主线程分离(后台运行),无需等待,但需确保线程访问的资源生命周期有效。

2. 线程同步:避免数据竞争

多线程同时操作共享资源会导致数据竞争(未定义行为),需通过同步机制保证线程安全。

互斥锁(std::mutex

通过 std::mutex 确保同一时间只有一个线程访问共享资源。

#include <iostream>
#include <thread>
#include <mutex>  // 互斥锁std::mutex mtx;  // 全局互斥锁
int shared_data = 0;void increment(int id) {for (int i = 0; i < 10000; ++i) {// 加锁:独占访问共享资源std::lock_guard<std::mutex> lock(mtx);  // 自动解锁(RAII 机制)shared_data++;// 离开作用域时,lock 自动析构并解锁}
}int main() {std::thread t1(increment, 1);std::thread t2(increment, 2);t1.join();t2.join();std::cout << "最终结果:" << shared_data << std::endl;  // 预期 20000return 0;
}
  • std::lock_guard:RAII 风格的锁管理,构造时加锁,析构时自动解锁,避免忘记解锁导致死锁。
  • std::unique_lock:更灵活的锁管理,支持手动加锁/解锁、超时等待等。
条件变量(std::condition_variable

用于线程间通信(如等待某个条件满足后再执行)。

#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 线程开始工作" << std::endl;
}int main() {std::thread t(worker);// 主线程准备数据...std::this_thread::sleep_for(std::chrono::seconds(2));  // 模拟准备时间{std::lock_guard<std::mutex> lock(mtx);ready = true;  // 更新条件}cv.notify_one();  // 唤醒等待的线程t.join();return 0;
}
原子变量(std::atomic

用于简单的数值共享变量,无需显式加锁,效率更高。

#include <iostream>
#include <thread>
#include <atomic>  // 原子变量std::atomic<int> atomic_data(0);  // 原子变量(线程安全)void increment() {for (int i = 0; i < 10000; ++i) {atomic_data++;  // 原子操作,无需加锁}
}int main() {std::thread t1(increment);std::thread t2(increment);t1.join();t2.join();std::cout << "原子变量结果:" << atomic_data << std::endl;  // 预期 20000return 0;
}

3. 平台特定的多线程实现

3.1 Windows 平台:CreateThread(Win32 API)

#include <iostream>
#include <windows.h>// 线程函数(Win32 要求 __stdcall 调用约定)
DWORD WINAPI ThreadFunc(LPVOID param) {int id = *(int*)param;std::cout << "线程 " << id << " 启动" << std::endl;return 0;
}int main() {int id1 = 1, id2 = 2;// 创建线程HANDLE hThread1 = CreateThread(NULL,           // 默认安全属性0,              // 默认栈大小ThreadFunc,     // 线程函数&id1,           // 传递给线程的参数0,              // 立即执行NULL            // 不需要线程 ID);HANDLE hThread2 = CreateThread(NULL, 0, ThreadFunc, &id2, 0, NULL);// 等待线程完成WaitForSingleObject(hThread1, INFINITE);WaitForSingleObject(hThread2, INFINITE);// 关闭线程句柄CloseHandle(hThread1);CloseHandle(hThread2);return 0;
}

3.2 Linux 平台:pthread(POSIX 线程)**

#include <iostream>
#include <pthread.h>// 线程函数
void* ThreadFunc(void* param) {int id = *(int*)param;std::cout << "线程 " << id << " 启动" << std::endl;return NULL;
}int main() {pthread_t tid1, tid2;int id1 = 1, id2 = 2;// 创建线程pthread_create(&tid1, NULL, ThreadFunc, &id1);pthread_create(&tid2, NULL, ThreadFunc, &id2);// 等待线程完成pthread_join(tid1, NULL);pthread_join(tid2, NULL);return 0;
}
  • 编译时需链接 pthread 库:g++ -o thread_demo thread_demo.cpp -lpthread

4. 多线程编程注意事项**

  1. 数据竞争:多个线程同时读写共享资源时必须同步(用互斥锁、原子变量等)。
  2. 死锁:避免线程间相互等待对方释放锁(如保持锁的获取顺序一致)。
  3. 线程生命周期detach() 后的线程需确保访问的资源(如局部变量)生命周期有效。
  4. 性能开销:线程创建/销毁有开销,频繁创建线程可考虑线程池(如 C++17 的 std::async 或第三方库)。
  5. 异常安全:确保线程函数抛出的异常被捕获,避免程序崩溃。
  • 推荐方案:优先使用 C++11 标准库 <thread>,配合 std::mutexstd::condition_variable 等实现跨平台多线程。
  • 核心同步机制:互斥锁(保护共享资源)、条件变量(线程通信)、原子变量(高效简单数值操作)。
  • 平台特定场景:仅在需要调用系统特有功能时使用 CreateThread(Windows)或 pthread(Linux)。

合理使用多线程可充分利用多核 CPU,提升程序性能,但需注意线程安全问题。

http://www.dtcms.com/a/388992.html

相关文章:

  • 嵌入式科普(40)浅谈“功能安全“概念,深悟“功能安全“本质
  • 分布式系统理论-CAP和BASE
  • SaaS 安全的原则、挑战及其最佳实践指南
  • Flink on Native K8S源码解析
  • VMwarea安装
  • HarmonyOS之Swiper全解析
  • React18中性能优化方式
  • X133核心板--智能教育平板的芯动力​
  • 下载flink和flink cdc jar
  • 华为三层交换技术
  • 潮起之江:算力创新与赋能开启AI产业新征程
  • 华为链路聚合技术基础
  • 百度智能云车牌识别API官方配置指南
  • Git 拉Github的仓库却要求登录GitLab
  • 【Kafka】Kafka如何开启sasl认证?
  • 国产化Excel开发组件Spire.XLS教程:C# 轻松将 DataSet 导出到 Excel
  • NLP情绪因子解构鲍威尔“风险管理降息”信号,黄金价格在3707高位触发量化抛售潮
  • 【Python办公】Excel多Sheet拆分工具
  • Unity_程序集_.asmdef_引用命名域失败
  • FPGA采集AD7606转SRIO传输,基于Serial Rapidlo Gen2,提供6套工程源码和技术支持
  • Cloudcompare实现在模型上进行点云(下)采样
  • 【Linux】聊聊文件那些事:从空文件占空间到系统调用怎么玩
  • 基于代码层对运动台性能提升实战
  • openfeigin配置相关
  • 网络传输协议解析及SSE补充
  • 视觉SLAM第12讲:建图
  • 2025编程技术学习网站大全:从入门到精通的免费资源指南
  • 刷题日记0918
  • emacs 如何显示断点和运行的行标
  • 【c++】继承(2)