C++之多线程

C++11开始提供对线程的内置支持,通过std::thread创建和管理线程,强调了跨平台和简洁性。线程不能被拷贝,但可以通过移动构造函数转移资源。线程资源通过join()或detach()回收,call_once()确保函数只执行一次。线程安全问题涉及到原子性、可见性和顺序性,解决方法包括使用互斥锁如mutex,以及lock_guard等RAII工具。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在C++11之前,C++没有对线程提供语言级别的支持,各种操作系统和编译器实现线程的方法不一样。

C++11增加了线程以及线程相关的类,统一编程风格、简单易用、跨平台。

创建线程

头文件:#include <thread>
线程类:std::thread

构造函数

1)thread() noexcept;
默认构造函,构造一个线程对象,不执行任何任务(不会创建/启动子线程)。
2)template< class Function, class... Args >
explicit thread(Function&& fx, Args&&... args );
创建线程对象,在线程中执行任务函数fx中的代码,args是要传递给任务函数fx的参数。
任务函数fx可以是 普通函数、类的非静态成员函数、类的静态成员函数、lambda函数、仿函数

下面附上部分thread的源码

删除了赋值函数和拷贝构造函数,但是可以把左值转换为右值就可以赋值成功。

3)thread(const thread& ) = delete;
删除拷贝构造函数,不允许线程对象之间的拷贝。
4)thread(thread&& other ) noexcept;
移动构造函数,将线程other的资源所有权转移给新创建的线程对象。

赋值函数

赋值函数:
thread& operator= (thread&& other) noexcept;
thread& operator= (const other&) = delete;
线程中的资源不能被复制,如果other是右值,会进行资源所有权的转移,如果other是左值,禁止拷贝。

但是这里注意,因为是把医院所有权进行了转移,所以相当于原来的那个对象线程已经丢了,所以不能进行join()操作。

下面给出示例

注意

先创建的子线程不一定跑得最快(程序运行的速度有很大的偶然性)。
线程的任务函数返回后,子线程将终止。
如果主程序(主线程)退出(不论是正常退出还是意外终止),全部的子线程将强行被终止。

线程资源的回收

虽然同一个进程的多个线程共享进程的栈空间,但是,每个子线程在这个栈中拥有自己私有的栈空间。所以,线程结束时需要回收资源。

回收子线程的资源有两种方法:

在这先附上一段源码,在这我们目前只需要简单的能看懂就行,不必深究

joinable()判断子线程的分离状态,返回值为布尔类型,

1)在主程序中,调用join()成员函数等待子线程退出,回收它的资源。如果子线程已退出,join()函数立即返回,否则会阻塞等待,直到子线程退出。
2)在主程序中,调用detach()成员函数分离子线程,子线程退出时,系统将自动回收资源。分离后的子线程不可join()。
用joinable()成员函数可以判断子线程的分离状态,函数返回布尔类型。

注意

已经detach()的对象不能在最后调用join(),否则运行会报错。

this_thread的全局函数

get_id():该函数用于获取线程ID,thread类也有同名的成员函数;
yield():该函数让线程主动让出自己已经抢到的CPU时间片;
sleep_for(参数):该函数让线程休眠一段时间。参数:windows下以毫秒为单位,linux下以秒为单位
sleep_until():该函数让线程休眠至指定时间点。(可实现定时任务)

其他函数

void swap(std::thread& other); // 交换两个线程对象。
static unsigned hardware_concurrency() noexcept; // 返回硬件线程上下文的数量。

示例:

void func()
{
    cout << "我是子线程:" << this_thread::get_id() << endl;
}
int main()
{
    thread thread_1(func);
    thread thread_2(func);
    cout << "子线程A的id=" << thread_1.get_id() << endl;
    cout << "子线程B的id=" << thread_2.get_id() << endl;
    thread_1.swap(thread_2);
    cout << "子线程A的id=" << thread_1.get_id() << endl;
    cout << "子线程B的id=" << thread_2.get_id() << endl;
    thread_1.detach();
    thread thread_3(move(thread_1));    //正确,将thread左值转换为右值
    thread_3.join();
    return 0;
}

call_once函数

多线程环境中,某些函数只能被调用一次,例如:初始化某个对象,而这个对象只能被初始化一次。

在线程的任务函数中,可以用std::call_once()来保证某个函数只被调用一次

头文件:#include <mutex>
template< class callable, class... Args >
void call_once( std::once_flag& flag, Function&& fx, Args&&... args );
第一个参数是std::once_flag,用于标记函数fx是否已经被执行过。
第二个参数是需要执行的函数fx。
后面的可变参数是传递给函数fx的参数。

下面给出示例

#include <iostream>
#include <thread>        
#include <mutex>        // std::once_flag和std::call_once()函数需要包含这个头文件。
using namespace std;

//once_flag onceflag;       // once_flag全局变量。本质是取值为0和1的锁。
// 在线程中,打算只调用一次的函数。
void once_func(const int bh, const string& str) {
    cout << "once_func() bh= " << bh << ", str=" << str << endl;
}

// 普通函数。
void func(int bh) {
    once_func(1, "我只能被调用一次哦");
    //call_once(onceflag, once_func, 0, "我只能调用一次哦");
    for (int ii = 1; ii <= 3; ii++)
    {
        cout << "第" << ii << "秒" << endl;
        this_thread::sleep_for(chrono::seconds(1));    // 休眠1秒。
    }
}

int main()
{
    // 用普通函数创建线程。
    thread t1(func, 3);
    thread t2(func, 8);

    t1.join();         // 回收线程t1的资源。
    t2.join();         // 回收线程t2的资源。
}

运行结果我们发现once_func()被调用了两次,每次创建线程都会调用,那如果我们只想让他被调用一次,怎么办?我们可以用函数call_once();

代码更改为下面

#include <iostream>
#include <thread>        
#include <mutex>        // std::once_flag和std::call_once()函数需要包含这个头文件。
using namespace std;

once_flag onceflag;       // once_flag全局变量。本质是取值为0和1的锁。
// 在线程中,打算只调用一次的函数。
void once_func(const int bh, const string& str) {
    cout << "once_func() bh= " << bh << ", str=" << str << endl;
}

// 普通函数。
void func(int bh) {
    //once_func(1, "我只能被调用一次哦");
    call_once(onceflag, once_func, 0, "我只能调用一次哦");
    for (int ii = 1; ii <= 3; ii++)
    {
        cout << "第" << ii << "秒" << endl;
        this_thread::sleep_for(chrono::seconds(1));    // 休眠1秒。
    }
}

int main()
{
    // 用普通函数创建线程。
    thread t1(func, 3);
    thread t2(func, 8);

    t1.join();         // 回收线程t1的资源。
    t2.join();         // 回收线程t2的资源。
}

这是发现只被调用了一次。

到这里创建线程的操作基本就完了,但是有没有发现我们测试debug时打印的内容很乱,这是为什么呢??

我们先

谈一下线程安全问题

线程安全

#include <iostream>
#include <thread>        // 线程类头文件。
using namespace std;
int aa = 0;
void func() 
{
    //把a加十万次
    for (int i = 1; i <= 100000; i++)
        aa++;
}
int main()
{
    thread t1(func);
    thread t2(func);
    t1.join();
    t2.join();
    cout << "aa=" << aa << endl;   // 显示全局变量aa的值。
}

我们发现结果都不一样,这是为什么呢??

引入一点线程安全的知识

同一个进程中的多个线程共享该进程中的全部系统资源
多个线程访问同一共享资源的时候会产生冲突
顺序性、可见性、原子性

原子性

原子性的操作是不可被中断的一个或一系列操作。个人理解和MySQL中的事务那块的知识有点类似。
严格定义:严格的原子性的操作,其他线程获取操作的变量时,只能获取操作前的变量值和操作后的变量值,不能获取到操作过程中的中间值,在操作过程中其他操作需要获取变量值,需要进入阻塞状态等待操作结束。

顺序性

为了优化程序性能,编译器、处理器和运行时会对代码指令进行重排,重排过程中会遵循as-if-serial语义,即不影响单线程的运行结果。

举个例子,当编译器看到下面代码时会进行怎么处理

int a=10;
a++;
a-=10;
a=20;

此时代码会被优化为int a=20;

CPU为了提高程序整体执行效率,可能会对到吗顺序进行优化,虽然不保证完全按照代码顺序执行,但是可以保证结果和按代码顺序执行时一致。

可见性

线程操作共享变量时,会将该变量从内存加载到CPU缓存中,修改该变量后,CPU会立即更新缓存,但不一定会立即将它写回内存。这时候如果其它线程访问该变量,从内存中读到的是旧数据,而非第一个线程操作后的数据, 多个线程并发访问共享变量时,一个线程对共享变量的修改,其他线程立刻可以看到

所以上面的问题就可以知道出在哪里了,a++操作不是原子操作,两个线程可能执行时获得的时旧数据.

如何保证线程安全?

volatile关键字

作用
1. 保证内存变量的可见性
2. 进制代码优化(重排序)

但这里将a用volatile修饰是不解决问题的,因为他只解决了可见性问题,所以还需要进行线程同步

线程同步

加锁和解锁,确保同一时间只有一个线程访问共享资源
访问共享资源之前加锁,访问完成后释放锁
如果某线程持有锁,其它的线程形成等待队列

C++11提供了四种互斥锁(避免多个线程同时访问共享资源。避免数据竞争,并提供线程间的同步支持)

mutex类

mutex类的成员函数

1)加锁lock()
互斥锁有锁定和未锁定两种状态。
如果互斥锁是未锁定状态,调用lock()成员函数的线程会得到互斥锁的所有权,并将其上锁。
如果互斥锁是锁定状态,调用lock()成员函数的线程就会阻塞等待,直到互斥锁变成未锁定状态。
2)解锁unlock()
只有持有锁的线程才能解锁。
3)尝试加锁try_lock()
如果互斥锁是未锁定状态,则加锁成功,函数返回true。
如果互斥锁是锁定状态,则加锁失败,函数立即返回false。(线程不会阻塞等待)

这时我们只需要进行如下改动就可以输出正确结果

#include <iostream>
#include <thread>        // 线程类头文件。
#include<mutex>    //互斥锁头文件
using namespace std;
int aa = 0;
mutex mtx;    //创建互斥锁
void func() 
{
    //把a加十万次
    for (int i = 1; i <= 100000; i++)
    {
        mtx.lock();    //申请加锁
        aa++;
        mtx.unlock();    //解锁
    }
        
}
int main()
{
    thread t1(func);
    thread t2(func);
    t1.join();
    t2.join();
    cout << "aa=" << aa << endl;   // 显示全局变量aa的值。
}

再举一个例子,现在计数,三个线程,统计1-100

#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
volatile int Count = 0;
mutex mtx;
void Add(string pthread_name)
{
    while (Count < 100)
    {
        mtx.lock();
        if (Count < 100)
        {
            ++Count;
            cout << pthread_name << "执行了Add():" << Count << endl;
        }
        mtx.unlock();
        this_thread::sleep_for(chrono::milliseconds(100));
    }
}
int main()
{
    thread t1(Add, "线程1");
    thread t2(Add, "线程2");
    thread t3(Add, "线程3");
    t1.join();
    t2.join();
    t3.join();
    return 0;
}

timed_mutex类

成员函数

1)加锁lock()
2)解锁unlock()
3)尝试加锁try_lock()
4) bool try_lock_for(时间长度);
5) bool try_lock_until(时间点);

recursive_mutex类

递归互斥锁允许同一线程多次获得互斥锁,可以解决同一线程多次加锁造成的死锁问题。

死锁问题

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。
当多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进,这种情况就是死锁, 如果没有外力的作用,那么死锁涉及到的各个进程都将永远处于封锁状态。

看案例

#include<iostream>
#include<mutex>
using namespace std;
class Object
{
    mutex m_mutex;
public:
    void func1()
    {
        m_mutex.lock();
        cout << "func1()" << endl;
        m_mutex.unlock();
    }
    void func2()
    {
        m_mutex.lock();
        cout << "func2()" << endl;
        func1();
        m_mutex.unlock();
    }
};
int main()
{
    Object obj;
    //obj.func1();    //没问题
    obj.func2();    //出现错误,
    return 0;
}

这里出现错误,原因是在func2中调用了func1,此时func2持有锁,而func1申请加锁,但是func1肯定申请不到,所以出现了死锁,这里只需要将类中的成员变量属性改为递归锁:recursive_mutex就行了.

lock_guard类

lock_guard是模板类,可以简化互斥锁的使用,也更安全。
lock_guard的定义如下:
template<class Mutex>
class lock_guard
{
explicit lock_guard(Mutex& mtx); //这里的Mutex是上面四种锁的一个
}
lock_guard在构造函数中加锁,在析构函数中解锁。
lock_guard采用了 RAII思想(在类构造函数中分配资源,在析构函数中释放资源,保证资源在离开作用域时自动释放)。

RALL思想是什么?

RAII全称为(Resource Acquisition Is Initialization),即“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。
C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。利用这个特性就可以很好的管理创建好的资源。比如 锁、信号、指针这类成套操作的资源,即有创建有销毁。
RALL机制或者RALL思想,简单的说, 把需要创建的资源封装到类里面,在类的构造函数完成对资源的初始化,在类的析构函数中完成对资源的释放。

后续会更新一篇文章细说RALL思想.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小谢%同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值