C++多线程初窥

    科技2025-01-07  16

    多线程的使用

    线程创建

    一开始我用的是pthread.h,创建了五个线程输出helloworld,玩的还挺开心,不过后来猛地在网上发现了C++11中的thread类,顿时惊为天人,这玩意太方便了,反过来看pthread.h的缺点真是不少,首先创建线程就很麻烦(命令长,参数多)。

    pthread_t tid;//创建一个线程id int ret = pthread_create(tid, NULL, helloWorld, NULL);

    thread就很牛逼了,短短一行完事。

    thread t(helloWorld);

    线程方法函数

    pthread最难受的一点可能就在这里了,它只接受void* fun(void*)类型的函数,如果你有特殊需求还得给函数包装一下让线程接受 而thread就可以直接接受函数名,也可以接受函数指针,而且函数参数传入也很方便,给几个例子

    void helloWorld(int a,int b); thread t1(helloWorld,a,b);//调用了helloWrold(a,b) class TEST{//调用对象的成员函数 public: fun0(int a,int b); }; TEST t; thread t2(fun0,t,a,b);//调用t.fun0(a,b)

    pthread.h的缺点就说到这,下面只讨论thread。

    线程等待

    thread t(helloWorld); t.join();

    调用join()方法的线程(主线程也不例外)会在t中线程退出前阻塞,直到t中线程结束退出。

    线程安全

    之前多少听说过多个线程访问同一个变量会引起线程安全问题,不过我在做大作业的时候并没有遇到过,大概是运气好线程少加上程序简单,不过如果要进入公司工作,接触的多线程项目是不能对线程安全问题视而不见的

    为什么会产生线程安全问题

    举个例子,这里有两个线程t1,t2访问同一个初值为0的整型a,两个线程都要对a进行+=10的操作 按理说我们应当得到一个a = 20的结果,但多线程同时执行的时候可能并不尽如人意 首先我们要搞清a += 10这行代码发生了什么? a是保存在内存中的,而计算发生在cpu,cpu要完成这一过程,大体上分为三步

    首先从内存中取到a的值,放在寄存器中然后cpu把寄存器中的值+10程序将寄存器中的计算结果放回内存中

    如果是多线程执行,线程之间的切换可能发生在任何一个时刻,这就产生了计算结果与预期不符的隐患 比如下面这个一步一切换的情况

    步骤t1寄存器t2寄存器内存中的at1取值0?0t2取值000t1计算1000t2计算10100t1存值101010t2存值101010

    两个线程结束后,内存中a的值竟然是10,t1的计算结果被t2完全覆盖,所幸这只是发生在测试程序中,如果某个程序员在银行系统中遗留了这种线程安全问题,那可能就会导致大量账目异常。这种问题在多线程编程中一定要极力避免

    如何解决线程安全问题

    volatile关键字?并不能

    我先尝试了volatile关键字,这个关键字可以防止编译器对值的结果预测,也就是取值的时候一定会从内存取值获取真实值,取值正确,那么结果就会正确吗?如果你看了我上一节的例子,应该能理解这并不能干净利落的解决问题,产生问题的根源在于取值和放回步骤的交错,和是否预测值无关。

    int a = 0; volatile int b = 0; void helloWorld(int count){ while(count--){ a++; b++; cout<<a<<" "<<b<<endl; } return; }

    实验中我让五个线程调用helloWorld(2),虽然最终结果正确了(不知是因为编译器优化还是纯粹运气原因),但整体输出属实难看

    互斥锁mutex

    互斥锁我之前就听说过,但还没使用过(接触的多线程开发太少了),这玩意使用还挺方便的

    mutex m; int a = 0; volatile int b = 0; void helloWorld(int count){ m.lock(); while(count--){ a++; b++; cout<<a<<" "<<b<<endl; } m.unlock(); return; }

    我用了同一个锁把a和b锁住,编译运行后输出结果表现如下: 非常漂亮的输出!完全符合预期 互斥锁是如何工作的呢?线程可以对m进行lock和unlock,lock前系统会检查mutex是否已经被lock,如果已经lock则这个线程lock失败,必须等待mutex的释放(unlock),如此,我们就确保了同一时刻只能有一个线程访问a和b(因为他们都被m锁住了),这也确保了多个线程取值和存值步骤的互不交叉,也就不会产生上述问题。

    atomic原子变量

    atomic变量重载了++在内的多个运算符,确保了变量操作的原子性 具体实现细节的我还没研究 总之结果是不错的

    std::atomic_int a{0};//atomic_int的初始化 volatile int b = 0; void helloWorld(int count){ while(count--){ a++; b++; cout<<a<<" "<<b<<endl; } return; }

    输出结果是这样的 左列和右列都很乱,但仔细研究我们能发现还是不同的,左列是1-10的数字,没有重复,右列则出现了不少数字重复。 输出顺序杂乱是因为我们只实现了自加操作的原子性,但没有把cout操作囊括进去,这可能导致明明t1先完成自加,t2线程却强插一步抢先输出。总之,结果显示原子变量也成功解决了问题。

    Processed: 0.010, SQL: 8