C++多线程(thread)

C++线程(thread)

C++11引入多线程支持,开发者通过<thread>库实现并行处理。

线程是操作系统执行调度的最小单位,也是程序执行的最小单元。

1、thread库概述

<thread>提供管理线程,保护共享数据、线程间同步操作、原子操作的基本功能。

1.1 thread的成员函数

join()和detach()

join 会阻塞当前线程,当在一个线程中调用另一个线程对象的 join() 方法时,当前线程会暂停执行,等待被调用的线程完成其任务。

detach 会将线程对象与执行的线程分离,被分离的线程会在后台独立运行,不再受原线程对象的控制。调用 detach() 后,该线程对象不再关联任何线程。

C++ 复制代码
// 模拟一个耗时的任务
void longTask() {
    std::cout << "子线程开始执行耗时任务..." << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(5));
    std::cout << "子线程完成耗时任务。" << std::endl;
}

int main() {
    std::cout << "主线程开始。" << std::endl;

    // 使用 join 的情况
    std::cout << "\n使用 join 的情况:" << std::endl;
    std::thread joinThread(longTask);
    std::cout << "主线程等待 join 线程完成..." << std::endl;
    joinThread.join();
    std::cout << "join 线程已完成,主线程继续。" << std::endl;

    // 使用 detach 的情况
    std::cout << "\n使用 detach 的情况:" << std::endl;
    std::thread detachThread(longTask);
    std::cout << "主线程分离 detach 线程并继续执行..." << std::endl;
    detachThread.detach();
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "主线程不等待 detach 线程,继续执行其他任务。" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "主线程完成其他任务,准备退出。" << std::endl;

    return 0;
}

代码中如果修改detach段的第二个sleep_for的等待时间,则会得到两个不一样的结果。究其原因就是 detach分离线程导致的,如果子线程的耗时任务大于主线的退出时间,则子线程不能完成。

不论是 join 还是 detach ,运行后 joinable 固定返回 false

joinable()

joinable()用于检测线程是否可以joindetach,其本质是判断thread对象是否与活跃线程相关联。

1.2 thread的构造函数

c++11为thread类提供了无参构造 和含可变参数构造两种主要初始化构造方式。删除了拷贝构造(毕竟线程可以拷贝不合理),提供了移动构造来配合无参构造使用。

C++ 复制代码
#include<iostream>
#include<thread>
using namespace std;

void func(int n)
{
	int x = 0;
	for (int i = 0; i < n; i++)
	{
		++x;
	}
	cout << x << endl;
}

int main()
{
	thread td1(func,100);
	thread td2;
	td2 = thread(func, 200);

	td1.join();
	td2.join();
	return 0;
}

无参构造一般用于被数据结构创建并管理的情景:

C++ 复制代码
#include<iostream>
#include<vector>
#include<thread>
#include<mutex>
using namespace std;

int main()
{
	vector<thread> thd;
	thd.resize(5);
	mutex mtx;
	int x = 0;
	auto func = [&](int n) {
		mtx.lock();
		for (int i = 0; i < n; i++)
		{
			++x;
		}
		mtx.unlock();
		};
	for (auto& nthd : thd)
	{
		nthd = thread(func, 100);//移动构造
	}
	for (auto& nthd : thd)
	{
		nthd.join();
	}

	return 0;
}

2、线程锁

线程之间的锁的类型有很多,按功能和使用场景有如互斥锁、条件锁、自旋锁、读写锁、递归锁。其主要目的是用于控制多个线程对共享资源的访问,避免数据竞争和不一致的问题。

2.1 互斥锁(Mutex)

功能:提供独占式资源 访问,保证同一时间只有同一个线程访问资源。一个线程获取互斥锁后,其他线程获锁尝试将被阻塞。

使用:互斥锁用于共享资源保护 ,确保在多个线程 访问同一个共享资源时,保证线程安全。

C++ 复制代码
mutex mtx;
int i = 0;
void fun(int n) {
	while (i < n) {
		i++;
		cout << i << " ";
	}
}

int main() {
	thread t1(fun, 100);
	thread t2(fun, 120);

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

	return 1;
}

如果没有加锁,两个线程同时访问共享数据会引发混乱。

我们可以通过互斥锁的方式保证资源只被同一线程使用。

C++ 复制代码
void fun(int n) {
	mtx.lock();
	while (i < n) {
		i++;
		cout << i << " ";
	}
	mtx.unlock();
}

在线程访问数据时上锁,保证其他线程不会同时访问数据,直到循环完成,下一个线程才可以继续访问数据。

2.2 条件锁(Condition Variable)

功能:作用线程 而非共享资源,主要是用于线程间的同步 。某个线程因为条件变量 而阻塞,当其他线程改变条件使其满足时,发出信号量,该线程继续执行。

使用:线程等待条件成立。比如:生产------消费者模型。

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

C++ 复制代码
mutex mtx;
condition_variable cv;
queue<string> taskQuene;
const int MAX_SIZE = 20;

string product() {
    int num1 = rand() % 10;
    int num2 = rand() % 10;
    return to_string(num1) + "+" + to_string(num2) + "=";
}

void solve(string exp) {
     vector<int> v;

     for (char c : exp) {
         if (isdigit(c)) {
             string str(1, c);
             v.push_back(stoi(str));
         }
    }
     int num1 = v.back();
     v.pop_back();
     int num2 = v.back();
     int res = num1 + num2;
     cout << exp << res << endl;
}

void producer() {
    for (int i = 0; i < 10; i++) {
        unique_lock<mutex> lock(mtx);
        cv.wait(lock, [] {return taskQuene.size() < MAX_SIZE; });

        string expression = product();
        taskQuene.push(expression);
        cout << "Produced: " << expression << endl;

        lock.unlock();
        cv.notify_one();

        this_thread::sleep_for(chrono::milliseconds(200));

    }
}

void consumer() {
    while (true) {
        unique_lock<mutex> lock(mtx);
        cv.wait(lock, [] {return !taskQuene.empty(); });

        string task = taskQuene.front();
        taskQuene.pop();
        solve(task);

        lock.unlock();
        cv.notify_one();

        this_thread::sleep_for(chrono::milliseconds(200));
    }
}
int main()
{
    thread producerThread1(producer);
    thread consumerThread1(consumer);

    producerThread1.join();

    consumerThread1.detach();
}

2.3 递归锁(Recurise Mutex)

功能:特殊的互斥锁 ,能被同一线程多次获取。只有所有获取操作被释放,其他线程才能获得锁。

使用:在递归函数或者嵌套调用中,同一线程多次进入临界区。比如:状态机模型,嵌套调用的同步模型。

电梯状态机:

C++ 复制代码
enum class ElevatorState {
    IDLE,
    MOVING_UP,
    MOVING_DOWN
};

class ElevatorStateMachine {
private:
    ElevatorState currentState;
    recursive_mutex mtx;

    void transitionTo(ElevatorState newState) {
        lock_guard<recursive_mutex> lock(mtx);
        currentState = newState;
        switch (newState)
        {
        case ElevatorState::IDLE:
            cout << "状态为停止" << endl;
            break;
        case ElevatorState::MOVING_UP:
            cout << "状态为上升" << endl;
            break;
        case ElevatorState::MOVING_DOWN:
            cout << "状态为下降" << endl;
            break;
        default:
            break;
        }
    }

public:
    ElevatorStateMachine() : currentState(ElevatorState::IDLE) {}

    // 处理上升指令
    void goUp() {
        std::lock_guard<std::recursive_mutex> lock(mtx);
        if (currentState != ElevatorState::MOVING_UP) {
            transitionTo(ElevatorState::MOVING_UP);
        }
    }

    // 处理下降指令
    void goDown() {
        std::lock_guard<std::recursive_mutex> lock(mtx);
        if (currentState != ElevatorState::MOVING_DOWN) {
            transitionTo(ElevatorState::MOVING_DOWN);
        }
    }

    // 处理停止指令
    void stop() {
        std::lock_guard<std::recursive_mutex> lock(mtx);
        if (currentState != ElevatorState::IDLE) {
            transitionTo(ElevatorState::IDLE);
        }
    }
};

// 线程函数,模拟不同的操作
void operateElevator(ElevatorStateMachine& elevator, int operations) {
    for (int i = 0; i < operations; ++i) {
        int randomOp = rand() % 3;
        switch (randomOp) {
        case 0:
            elevator.goUp();
            break;
        case 1:
            elevator.goDown();
            break;
        case 2:
            elevator.stop();
            break;
        }
    }
}

int main()
{
    //srand(time(nullptr));
    ElevatorStateMachine elevator;

    vector<thread> threads;
    // 创建多个线程
    for (int i = 0; i < 3; ++i) {
        threads.emplace_back(operateElevator, std::ref(elevator), 5);
    }

    // 等待所有线程完成
    for (auto& thread : threads) {
        thread.join();
    }

    return 0;
}

2.4 自旋锁(Spin Lock)

功能:线程申请失败时,不会进入阻塞 ,状态线程会不断的申请 锁直到成功申请。类似于while循环。减少了线程上下文切换的开销

使用:性能要求极高,锁持有时间极短的场景。例如:实时系统的任务同步。

2.5 读写锁(Read - Write Lock)

功能:读者---写者模型,共享资源访问存在两种操作。允许多线程同时执行一种操作 ,进行另一操作 时则仅运行单线程

场景:读者---写者模型。

C++ 复制代码
std::mutex mtx;
std::condition_variable cvReaders, cvWriters;
int readers = 0;
int writers = 0;
bool writing = false;

// 读者函数
void reader(int id) {
    std::unique_lock<std::mutex> lock(mtx);
    // 等待没有写者正在写且没有写者在等待
    cvReaders.wait(lock, [] { return!writing && writers == 0; });
    readers++;
    lock.unlock();

    std::cout << "读者 " << id << " 在读. 共有读者: " << readers << std::endl;
    // 模拟读取操作
    std::this_thread::sleep_for(std::chrono::milliseconds(100));

    lock.lock();
    readers--;
    // 如果当前没有读者了,唤醒可能等待的写者
    if (readers == 0) {
        cvWriters.notify_one();
    }
    lock.unlock();
    std::cout << "读者 " << id << " 完成读入." << std::endl;
}

// 写者函数
void writer(int id) {
    std::unique_lock<std::mutex> lock(mtx);
    writers++;
    // 等待没有读者正在读且没有写者正在写
    cvWriters.wait(lock, [] { return readers == 0 && !writing; });
    writing = true;
    writers--;
    lock.unlock();

    std::cout << "写者 " << id << " 写入." << std::endl;
    // 模拟写操作
    std::this_thread::sleep_for(std::chrono::milliseconds(200));

    lock.lock();
    writing = false;
    // 如果有写者在等待,优先唤醒写者,否则唤醒所有读者
    if (writers > 0) {
        cvWriters.notify_one();
    }
    else {
        cvReaders.notify_all();
    }
    lock.unlock();
    std::cout << "写者 " << id << " 写完." << std::endl;
}

int main() {
    std::vector<std::thread> threads;

    // 创建多个读者和写者线程
    for (int i = 0; i < 3; ++i) {
        threads.emplace_back(reader, i);
    }
    for (int i = 0; i < 2; ++i) {
        threads.emplace_back(writer, i);
    }

    // 等待所有线程完成
    for (auto& thread : threads) {
        thread.join();
    }

    return 0;
}

3、锁守卫

在 C++ 多线程编程中,锁守卫(Lock Guard)是一种利用 RAII技术来管理锁的工具。对象构造时获取锁析构时释放锁

RAII(Resource Acquisition Is Initialization)即资源获取即初始化,是一种将资源的生命周期与对象生命周期绑定的技术。

3.1 lock_guard

.lock_guard在构造时锁定关联的互斥锁,在析构时解锁该互斥锁,不支持手动解锁和重新锁定操作。

C++ 复制代码
mutex mtx;
int val = 0;

void func() {
    lock_guard<mutex> lock(mtx);
    int res = val;
    while (val < res + 10000) {
        val++;
    }
    cout << val << endl;
}

int main()
{
    thread t1(func);
    thread t2(func);

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

    return 1;
}

取消锁守卫可以参看互斥锁的例子,lock_guard本质上就是一个自助管理锁的对象。

3.2 unique_lock

unique_lock提供比lock_guard的管理功能。支持延迟锁定、手动解锁、重新锁定以及与条件变量配合使用的操作。

4、原子操作

C++的 atomic 类是 C++11 标准引入的,支持原子操作,主要用于多线程编程中 。原子操作是一种不可分割最小且不可并行操作,在多线程环境中只存在完成和未完成两种形式。

原子操作的特点就是在执行过程中不可被中断的操作。

C++ 复制代码
std::atomic<int> counter(0);

// 线程函数,对计数器进行递增操作
void increment() {
    for (int i = 0; i < 100000; ++i) {
        ++counter;
    }
}

int main() {
    std::vector<std::thread> threads;

    // 创建多个线程
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment);
    }

    // 等待所有线程完成
    for (auto& t : threads) {
        t.join();
    }

    std::cout << "Final counter value: " << counter << std::endl;
    return 0;
}

从多线程编程的角度考虑,原子操作同样能使多个线程安全共享数据。

4.1 构造函数

atomic 类似于 STL 的容器,实例化对象时要指明实例化对象的类型。 atomic 支持实例化整型类型、bool、字符类型(某些编译可能不支持)、指针类型等类型,同时也支持满足特定条件的自定义类型。

C++ 复制代码
#include <atomic>

std::atomic<int> n;
std::atomic<char> ch;
std::atomic<bool> is;

4.2 成员函数

fetch_add() 和 fetch_sub()

这两个函数分别用于进行自增和自减操作,它能保证这个操作的过程是原子的。对于这两个函数的 sync 参数,它它决定了当前线程和其他线程之间对内存的访问顺序,默认缺省使用 std::memory_order_seq_cst提供最强的顺序一致性保证,确保所有线程的操作按顺序进行,类似于全局的同步操作。它保证所有线程中所有的原子操作遵循单一的顺序。 在绝大多数情况下,使用默认的 sync 即可。

实例:

C++ 复制代码
#include <iostream>
#include <atomic>
#include <thread>

std::atomic<int> for_add(0);
std::atomic<int> for_sub(0);

void increment() {
    for (int i = 0; i < 500; ++i) {
        for_add.fetch_add(1);  // 原子加1
    }
}

void decrement() {
    for (int i = 0; i < 500; ++i) {
        for_sub.fetch_sub(1);  // 原子减1
    }
}

int main() {
    std::thread a1(increment);
    std::thread a2(increment);

    std::thread s1(decrement);
    std::thread s2(decrement);


    a1.join();
    a2.join();
    s1.join();
    s2.join();

    std::cout << "for_add: " << for_add << std::endl;  // 输出 1000
    std::cout << "for_sub: " << for_sub << std::endl;  // 输出 -1000
    return 0;
}
store()

原子地将一个值存储到原子变量中。

C++ 复制代码
std::atomic<int> atomicVar(0);

void writer() {
    atomicVar.store(100);
}

4.3 自定义类型

atomic 类也可以用于自定义类型的变量,但自定义类型必须满足:

  1. 满足标准布局(Standard Layout) 要求。

    标准布局即没有虚拟继承、成员变量必须按顺序排列、没有特殊的构造函数和析构函数。

  2. 满足可复制可赋值的要求。

    需要有默认构造和拷贝构造和赋值。

  3. 必须支持合适的内存模型。

  4. 必须具有适当的对齐要求。

实际上, atomic 实例化自定义类型的要求就是自定义类型必须足够简单,在实际中很难把握,所以推荐自己加锁保证原子性和线程安全即可。

CSDN 文章作者同名

相关推荐
虾球xz15 分钟前
游戏引擎学习第195天
c++·学习·游戏引擎
熙曦Sakura37 分钟前
【C++】map
前端·c++
Stardep43 分钟前
算法学习11——滑动窗口——最大连续1的个数
数据结构·c++·学习·算法·leetcode·动态规划·牛客网
双叶8361 小时前
(C语言)学生信息表(学生管理系统)(基于通讯录改版)(正式版)(C语言项目)
c语言·开发语言·c++·算法·microsoft
wen__xvn1 小时前
每日一题洛谷P8716 [蓝桥杯 2020 省 AB2] 回文日期c++
c++·算法·蓝桥杯
rigidwill6662 小时前
LeetCode hot 100—二叉搜索树中第K小的元素
数据结构·c++·算法·leetcode·职场和发展
星途码客2 小时前
C++位运算精要:高效解题的利器
java·c++·算法
周Echo周3 小时前
5、vim编辑和shell编程【超详细】
java·linux·c++·后端·编辑器·vim
榆榆欸3 小时前
6.实现 Reactor 模式的 EventLoop 和 Server 类
linux·服务器·网络·c++·tcp/ip
虾球xz5 小时前
游戏引擎学习第193天
c++·学习·游戏引擎