C++中的多线程编程及线程同步

文章目录

前言

在当今计算领域,无论是追求极致响应速度的桌面应用、处理海量并发请求的服务器后端,还是需要实时处理数据的科学计算与游戏引擎,多线程技术 已然成为提升软件性能与用户体验的核心手段。它通过"分而治之"的策略,将应用程序的任务负载分配到多个执行流中 ,从而最大限度地挖掘现代多核处理器的并行计算潜力,对于提高软件的流畅度、响应能力和整体执行效率具有不可替代的重要作用。

本文旨在系统地介绍C++语言中的多线程编程,并给出案例。

多线程的核心价值

  • 提升性能与吞吐量:在拥有多个CPU核心的系统中,单线程程序只能利用其中一个核心,造成巨大的计算资源浪费。多线程程序可以将计算密集型任务(如图像处理、数据编码、物理模拟)分解成多个子任务,并由多个线程并行执行,从而显著缩短任务总耗时,实现近乎线性的性能加速。

  • 增强响应性与流畅度:在图形用户界面(GUI)应用程序中,如果将耗时操作(如文件读写、网络请求)放在主线程(通常是UI线程)中执行,会导致界面"冻结",无法响应用户操作。通过创建后台工作线程来处理这些阻塞性任务,可以确保UI线程始终保持流畅的交互响应,从而极大提升用户体验。

  • 简化异步任务模型:对于需要同时处理多个I/O操作(如网络通信、数据库访问)的服务端程序,多线程模型比传统的异步回调模型更直观、更易于理解和编码。每个连接可以分配一个独立的线程,使得代码逻辑清晰,接近于同步编程的思维方式。

C++多线程编程基础

在C++11标准之前,多线程编程严重依赖平台特定的API。C++11的引入将多线程支持纳入标准库,带来了可移植且类型安全的线程管理工具。

创建线程并启动

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

// 1. 普通函数作为线程入口
void background_task(int id) {
    std::cout << "线程 " << id << " 正在执行,线程ID: " 
              << std::this_thread::get_id() << std::endl;
}

// 2. Lambda表达式作为线程入口
auto lambda_task = [](const std::string& message) {
    std::cout << "Lambda线程: " << message << std::endl;
};

int main() {
    // 创建并启动线程
    std::thread t1(background_task, 1); // 传递函数指针和参数
    std::thread t2(lambda_task, "Hello from Lambda!"); // 传递Lambda和参数

    // 等待线程完成 (重要!)
    t1.join(); // 主线程阻塞,直到t1执行完毕
    t2.join(); // 主线程阻塞,直到t2执行完毕

    std::cout << "主线程结束。" << std::endl;
    return 0;
}

输出:

线程同步机制

当多个线程需要访问共享数据或资源时,如果不加控制,就会引发数据竞争,导致程序行为不确定、崩溃或产生错误结果。线程同步机制正是为了解决这一问题而生的,有以下几种方式:

1. 互斥锁std::mutex

互斥锁 是最基本的同步原语,它保证了同一时间只有一个线程可以进入被保护的代码段(临界区)

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

std::mutex g_mutex; // 全局互斥锁
int shared_counter = 0;

void increment_counter(int iterations) {
    for (int i = 0; i < iterations; ++i) {
        g_mutex.lock();   // 进入临界区前加锁
        ++shared_counter; // 安全地修改共享数据
        g_mutex.unlock(); // 离开临界区后解锁
    }
}

int main() {
    std::thread t1(increment_counter, 100000);
    std::thread t2(increment_counter, 100000);

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

    std::cout << "最终计数器值: " << shared_counter << std::endl; // 正确输出 200000
    return 0;
}

输出:

这种方式可能存在的问题 :直接使用lock()和unlock()容易因异常或提前返回而导致锁无法释放,造成死锁

2. 锁守卫std::lock_guardstd::unique_lock,可在一定程度上解决上述死锁问题

区别:

  • lock_guard 是基于互斥锁 std::mutex 实现的,unique_lock 是基于通用锁 std::unique_lock 实现。
  • unique_lock 可以实现比 lock_guard 更灵活的锁操作:lock_guard 是不可移动的(moveable),即不能拷贝、赋值、移动,只能通过构造函数初始化和析构函数销毁,unique_lock 是可移动的,可以拷贝、赋值、移动。
  • unique_lock 提供了更多的控制锁的行为,比如锁超时、不锁定、条件变量等。ORB-SLAM算法中常用这个
  • unique_locklock_guard 更重,因为它有更多的功能,更多的开销。如果只需要简单的互斥保护,使用 lock_guard 更好。

lock_guard 案例:

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

std::mutex g_mutex; // 全局互斥锁
int shared_counter = 0;

void safe_increment(int iterations) {
    for (int i = 0; i < iterations; ++i) {
        std::lock_guard<std::mutex> lock(g_mutex); // 构造即加锁
        ++shared_counter;
        // lock 析构时自动解锁
    }
}

int main() {
    // 使用安全的增量函数
    shared_counter = 0; // 重置计数器
    std::thread t1(safe_increment, 100000);
    std::thread t2(safe_increment, 100000);

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

    std::cout << "最终计数器值: " << shared_counter << std::endl; // 正确输出 200000
    return 0;
}

输出

unique_lock案例:

cpp 复制代码
#include <iostream>
#include <mutex>
#include <thread>
 
std::mutex g_mutex; // 全局互斥锁
int shared_counter = 0;
 
void worker(int iterations)
{
    std::lock_guard<std::mutex> lg(g_mutex);  // lock_guard 方式上锁
    for (int i = 0; i < iterations; ++i) {
        ++shared_counter; // 安全地修改共享数据
    }
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "worker thread is done." << std::endl;
}  // lock_guard 不支持手动解锁,会在此自动释放锁
 
void another_worker(int iterations)
{
    std::unique_lock<std::mutex> ul(g_mutex);  // unique_lock 方式上锁
    for (int i = 0; i < iterations; ++i) {
        ++shared_counter; // 安全地修改共享数据
    }
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "another worker thread is done." << std::endl;
    ul.unlock();  // 手动释放锁
    //do something...
}  // 如果锁未释放,unique_lock 会在此自动释放锁
 
int main()
{
    std::thread t1(worker, 100000);
    std::thread t2(another_worker, 100000);
    t1.join();
    t2.join();

    std::cout << "最终计数器值: " << shared_counter << std::endl; // 正确输出 200000
    return 0;
}

输出:

3. 条件变量std::condition_variable

条件变量用于实现线程间的等待与通知机制,允许一个线程等待某个条件成立,而另一个线程在条件改变时通知等待的线程。这是实现生产者-消费者模型等协作模式的关键。

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

std::queue<int> data_queue;
std::mutex queue_mutex;
std::condition_variable data_cond;

// 生产者线程
void data_producer() {
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        {
            std::lock_guard<std::mutex> lock(queue_mutex);
            data_queue.push(i);
            std::cout << "生产数据: " << i << std::endl;
        }
        data_cond.notify_one(); // 通知一个等待的消费者
    }
}

// 消费者线程
void data_consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(queue_mutex);
        // 等待条件成立:队列非空。等待时会自动释放锁,被唤醒后重新获取锁。
        data_cond.wait(lock, []{ return !data_queue.empty(); });

        int data = data_queue.front();
        data_queue.pop();
        lock.unlock(); // 尽早释放锁

        std::cout << "消费数据: " << data << std::endl;
        if (data == 9) break; // 结束条件
    }
}

int main() {
    std::thread producer(data_producer);
    std::thread consumer(data_consumer);

    producer.join();
    consumer.join();

    return 0;
}

输出

4. 原子操作std::atomic

对于简单的计数器、标志位等 ,使用互斥锁开销过大。C++提供了std::atomic模板,能够保证对该变量的操作是不可分割的,无需显式加锁,性能极高

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

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

void atomic_increment(int iterations) {
    for (int i = 0; i < iterations; ++i) {
        ++atomic_counter; // 原子操作,线程安全
    }
}

int main() {
    std::thread t1(atomic_increment, 100000);
    std::thread t2(atomic_increment, 100000);

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

    // 正确输出 200000
    std::cout << "最终计数器值: " << atomic_counter.load() << std::endl;
    return 0;
}

输出

5. 异步线程 std::future
std::future:提供了一种更高级的异步任务执行和结果获取方式,它抽象了线程管理的细节,让开发者更专注于任务本身。

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

// 模拟一个耗时的计算任务
int compute_heavy_task() {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 42;
}

int main() {
    // 异步启动任务
    std::future<int> result = std::async(std::launch::async, compute_heavy_task);

    // ... 主线程可以同时做其他工作 ...
    for(int i = 0; i < 5; ++i) {
        std::cout << "主线程工作中..." << std::endl;
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    
    // 获取异步任务结果(如果需要,会阻塞等待)
    int value = result.get();
    std::cout << "异步任务结果为: " << value << std::endl;
    return 0;
}

总结

多线程编程是C++开发者迈向高性能应用开发的必经之路 。它通过并行化极大地提升了软件的执行效率 ,并通过将阻塞操作移至后台显著增强了应用的流畅度 。C++标准库提供了一套强大而全面的工具集,从基础的std::thread到关键的同步原语(mutex, condition_variable),再到高效的std::atomic。然而,线程引入了复杂 性,尤其是数据竞争和死锁问题。成功的关键在于深刻理解并正确运用线程同步机制

相关推荐
拾光Ծ2 小时前
【C++哲学】面向对象的三大特性之 多态
开发语言·c++·面试
小欣加油2 小时前
leetcode 494 目标和
c++·算法·leetcode·职场和发展·深度优先
大飞pkz3 小时前
【设计模式】解释器模式
开发语言·设计模式·c#·解释器模式
Miki Makimura3 小时前
基于网络io的多线程TCP服务器
网络·c++·学习
Dyan_csdn3 小时前
Python系统设计选题-49
开发语言·python
jc06203 小时前
项目实战5:聊天室
c++
草莓熊Lotso3 小时前
《回溯 C++98:string 核心机制拆解 —— 从拷贝策略到高效 swap》
开发语言·c++
2401_831501733 小时前
Python学习之day01学习(变量定义和数据类型使用)
开发语言·python·学习
数智顾问4 小时前
Java坐标转换的多元实现路径:在线调用、百度与高德地图API集成及纯Java代码实现——纯Java代码实现与数学模型深度剖析
java·开发语言