c++中的多线程:概念、基本用法、锁以及条件变量

1.基本概念

首先要对并发,进程,线程有基本的概念。

1.1什么是并发

意思就是两个任务同时执行。

对於单核CPU:在不考虑Intel超线程技术的情况下,由于只有一个CPU,某一时刻只能执行一个任务,因此只能软件并发,多任务并发情景下需要进行任务切换,因此这个并不是帧并发,而是假并发。

对于多核CPU:因为有多个CPU,所以可以同时执行多个任务;因此可以硬件并发,真正同时执行多个任务。

1.2什么是进程

进程是系统资源分配的最小单位,是应用程序运行的环境。打开Windows的进程管理器,你会发现Chrome浏览器、Word等应用都是以进程的形式被管理的。

1.3什么是线程

线程是任务执行的最小单位,一般是执行某个function。

1.4进程与线程的关系

线程属于进程,一个进程可以拥有多个线程。每个进程都有一个主线程。

一个进程中的所有线程,是共享资源的,比如全局的变量、对象、指针、引用等等。

举个例子,在word这个应用进程中,有负责UI的线程,有负责字数统计的线程,所有的线程的功能加起来,就是word这个应用进程的全部功能。当某个线程执行时需要资源时,就从word进程的资源池里取。

其实单线程也可以跑word,只不过多线程可以最大化的发挥出多核CPU的性能优势。

1.5多进程并发和多线程并发的区别

一个进程中创建了多个线程,一个进程中的线程共享地址空间,全局变量、指针、引用,都可以在线程间传递,使用多线程的开销远远小于多进程。

多进程并发,这些进程之间没有共享地址空间,无法直接传递变量、指针、引用等资源,因此数据同步带来的开发难度与效率会弱于多线程。

 

2.C++中多线程的创建、启动与应用

c++ 11 之后有了标准的线程库:std::thread

基本作用:开启另一个线程并行执行函数/任务。

 

2.0最基本用法,什么都不用(join和detach都不用)

效果是起一个线程,执行线程中的函数,直到结束,函数结束时,子线程会自动被销毁。

线程创建之后就开始执行了,只不过在一般的示例中,main函数中起了线程之后,如果不用join或者detach会报错,这是因为main已经结束了,而子线程还没结束,但是资源已经被释放掉了,就会报错。

而一般实际项目中,main会一直跑,比如某个service跑在循环里,这个时候子线程什么都不用,也可以一直跑着,并不会报错。

2.1join等待thread执行完毕:

#include<iostream>
#include<thread>
using namespace std;
 
void func_thread() {
    // sleep 3s
    sleep(3000);
    cout << "in func_thread, sleeped 3s.." << endl;
}
 
int main() {
    cout << "main begin ....." << endl;
    std::thread t1(func_thread);
    // 阻塞当前main主线程,等待子线程执行完毕后,恢复主线程执行
    t1.join();
    cout << "main continue ....." << endl;
 }

常用参数:

get_id()    取得目前的线程 id, 回传一个 std::thread::id  类型

joinable()    检查是否可 join

join()   // 阻塞当前线程,等待子线程执行完毕

detach()  // 与该线程分离,一旦该线程执行完后它所分配的资源就会被释放

native_handle()    取得平台原生的 native handle.

sleep_for()    // 停止目前线程一段指定的时间

yield()   // 暂时放弃CPU一段时间,让给其他线程

 

2.2不等待thread执行结束,主线程与子线程分离:detach

void thread_func() {
    while(1){
    cout << " thread_func begin...." << endl;
        sleep(1);
    cout << " thread_func end...." << endl;
    }
}
 
int main() {
    std::thread t1(thread_func);
    cout << "created thread t1....." << endl;;
    t1.detach();
    cout << "detach thread t1....."<< endl;
    while(1){
    cout << "main running" << endl;
    Sleep(2);
    }
    return 0;
}

参数传入引用:

void myFunc(int n) {
    std::cout << "myFunc n = " << n << endl;
    n += 10;
}
 
void myFunc_ref(int& n) {
    std::cout << "myFunc reference n = " << n << endl;
    n += 10;
}
 
int main() {
 
    int n1 = 5;
    thread t1(myFunc, n1);
    t1.join();
    cout << "main n1 = " << n1 << "\n";
 
    int n2 = 10;
    thread t2(myFunc_ref, std::ref(n2));
    t2.join();
    cout << "main n2 = " << n2 << "\n";
 
    cout << "main thread run over" << endl;
    return 0;
}

2.3 reference to non-static member function must be called

代表std::thread t1(func,...)传入的func是对象的非静态成员函数。有可能是因为不确定线程还在工作时,该对象是否会被free,因此报错。

2个解决办法:

  • 方法一:把该函数改为static函数,这样该函数属于类,而不是对象,所以就没有上述担忧了
  • 方法二:参考上述class 类成员函数创建线程,如下所示

class 类成员函数创建线程

C++ std::thread 的构建可以传入class类别中的成员函数,如下范例所示:AA::start 分别建立t1, t2 两个线程,而 t1传入 AA::a1 类别函数。

notice :

第一个参数:AA::a1 前面需要添加 &

第二个参数:代表的是那个类对象

后面参数: 按照要求传入即可

class AA
{
public:
    void a1()
    {
        cout << "a1\n";
    }
 
    void a2(int n) {
        cout << "a2 : " << n << "\n";
    }
 
    void start() {
        thread t1(&AA::a1, this);
        thread t2(&AA::a2, this,10);
 
        t1.join();
        t2.join();
    }
 
private:
 
};

 

3.C++创建多个线程以及数据共享

3.1多线程创建

创建多个线程的步骤与创建第一个线程时一样,需要注意的时线程被创建后,立刻就开始运行了,因此在不使用join的情况下,很难去规定不同线程的执行顺序。

使用join()会让整个流程更稳定。

3.2数据共享

只读数据在进行线程间的操作时是安全稳定的,不需要特别的处理手段,直接读就可以;
有的线程写有的线程读,不进行特殊处理,那么程序肯定会崩溃;

因此解决办法是: 对于读和写操作,在对某个数据进行读写操作时,先对数据进行上锁,其他线程要等待该操作完成后对数据解锁后才可以使用

保护共享数据

3.2.1用互斥量(mutex)解决共享数据的保护问题

互斥量(mutex)是个类对象,理解成一把锁,当多个线程尝试使用lock()成员函数来加锁时,只有一个线程可以锁成功(成功标志是lock()返回);如果没锁成功,那么线程会卡在lock()这里不断进行尝试去加锁。

在执行多个线程之间的共享数据的读写操作时,在每一个读写操作之前进行上锁lock(),然后在操作完之后进行解锁unlock(),这样可以保证同一时刻只有一个线程对数据进行处理。

下面是一个用mutex的例子:

// C++stu_03.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//本例程用于学习创建多线程以及数据共享问题

#include <iostream>
#include <windows.h>
#include <thread>
#include <vector>
#include <list>
#include <mutex>

using namespace std;

vector<int>g_vaul = { 1,2,3,4 };//共享数据,只读

class A {
public:
    //把玩家命令输入到list中
    void inMsgRecvQueue()
    {
        for (int i=0;i<1000;++i)
        {
            my_mutex.lock();
            cout << "inMsgRecvQueue执行插入一个元素" << i << endl;
            msgRecvQueue.push_back(i);
            my_mutex.unlock();
        }
    }

    //从list中取出玩家命令
    void outMsgRecvQueue()
    {
        for (int i = 0; i < 1000; ++i)
        {
            my_mutex.lock();
            if (!msgRecvQueue.empty())
            {
                //消息不为空
                int command = msgRecvQueue.front();//返回第一个元素
                msgRecvQueue.pop_front();//移除第一个元素但不返回
                cout << "从消息队列中取出一个数据"<< command << endl;
                my_mutex.unlock();
            }
            else
            {
                my_mutex.unlock();//进行判断时要注意每种情况下都要有对应的unlock()
                cout << "outMsgRecvQueue执行,但是消息队列为空" << i << endl;
            }
        
        }
    }

private:
    list<int>msgRecvQueue;
    mutex my_mutex;
};

int main()
{
    std::cout << "Hello World!\n";
    A myobja;
    thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);//要注意第二个参数是引用才能保证线程中使用的是同一个对象
    thread myInnMsgObj(&A::inMsgRecvQueue, &myobja);    
    myOutMsgObj.join();
    myInnMsgObj.join();



    cout << "I love Arsenal!" << endl;
    return 0;
}

4. 死锁以及解决方案

4.1死锁的概念

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

解释起来就是:
两个线程两把锁,其中一个线程先lock mutex1,再lock mutex2,另外一个线程先lock mutex2,再lock mutex1

当第一个线程把mutex1给locked住时,第二个线程locked了mutex2,此时两个线程都要继续上锁,但是第一个线程无法上lock mutex2,第二个线程无法上lock mutex1,那么就会卡在这里,这时就产生了死锁。

或者说多个线程进行环路等待,就会出现死锁:

发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

4.2死锁的解决办法

std::lock

功能:

  • 一次锁住两个或两个以上的mutex(至少两个,多了不限,1个不行)
  • 使用该函数模板不存在因多个线程上锁顺序问题而导致的死锁情况的发生。
  • 如果mutex中有一个没锁住,那么就会一直等待mutex都锁住才会继续向下执行。

特点:

要么多个mutex都锁住,要么多个mutex都没锁住。如果只锁了一个,另外一个没有锁成功,那么它会立即把已经锁住的解锁,避免死锁的发生。
解锁时要按常规方式挨个解锁


例子:

// C++stu_03.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//本例程用于学习创建多线程以及数据共享问题

#include <iostream>
#include <windows.h>
#include <thread>
#include <vector>
#include <list>
#include <mutex>

using namespace std;

vector<int>g_vaul = { 1,2,3,4 };//共享数据,只读

class A {
public:
    //把玩家命令输入到list中
    void inMsgRecvQueue()
    {
        for (int i=0;i<1000;++i)
        {
            //my_mutex1.lock();//先锁1,再锁2
            //my_mutex2.lock();
            std::lock(my_mutex1, my_mutex2);//用来代替上面两句
            cout << "inMsgRecvQueue执行插入一个元素" << i << endl;
            msgRecvQueue.push_back(i);
            my_mutex1.unlock();
            my_mutex2.unlock();
        }
    }

    //从list中取出玩家命令
    void outMsgRecvQueue()
    {
        for (int i = 0; i < 1000; ++i)
        {
            //my_mutex1.lock();//先锁1,再锁2
            //my_mutex2.lock();
            std:lock(my_mutex2, my_mutex1);//用来代替上面两句
            if (!msgRecvQueue.empty())
            {
                //消息不为空
                int command = msgRecvQueue.front();//返回第一个元素
                msgRecvQueue.pop_front();//移除第一个元素但不返回
                cout << "从消息队列中取出一个数据"<< command << endl;
                my_mutex1.unlock();
                my_mutex2.unlock();
            }
            else
            {
                my_mutex1.unlock();
                my_mutex2.unlock();
                cout << "outMsgRecvQueue执行,但是消息队列为空" << i << endl;
            }
        
        }
    }

private:
    list<int>msgRecvQueue;
    mutex my_mutex1;
    mutex my_mutex2;
};

int main()
{
    std::cout << "Hello World!\n";
    A myobja;
    thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);//要注意第二个参数是引用才能保证线程中使用的是同一个对象
    thread myInnMsgObj(&A::inMsgRecvQueue, &myobja);    
    myOutMsgObj.join();
    myInnMsgObj.join();

    cout << "I love Arsenal!" << endl;
    return 0;
}

 

5.unique_lock

5.1什么是unique_lock

lock_guard时用来管理mutex的上锁解锁的模板类,unique_lock也是用来管理mutex上锁解锁的类模板

lock_guard<>可以看出它是一个模板类,它在自身作用域(生命周期)中具有构造时加锁,析构时解锁的功能。

unique_lock是一个类模板,比lock_guard更加灵活,效率上低一点,内存占用大一点。

unique_lock可以直接替换lock_guard,调用unique_lock也不需要手动解锁,在当前作用域结束时会自动解锁。

5.2std::defer_lock

作用是初始化一个没有加锁的mutex(在使用其他参数时会在创建mutex时直接尝试加锁),这样可以自己控制枷锁的粒度

5.3unique_lock的成员函数

5.3.1成员函数lock()

对于没有加锁的unique_lock对象,可以通过成员函数lock()进行上锁。

5.3.2成员函数unlock()

对于上锁的unique_lock对象,可以通过成员函数unlock()进行解锁。这样提前解锁来运行一些不需要共享数据的代码,这使得我们的代码设计更加灵活

粒度一般用粗细来描述

锁住的代码越少,这个锁的粒度就细,执行效率就越高
锁住的代码越多,这个锁的粒度就粗,执行效率就越低

例子:

std::unique_lock<std::mutex>myunique_lock(my_mutex1,std::defer_lock);//创建一个没有加锁的myunique_lock
myunique_lock.lock();//对myunique_lock进行加锁操作
//处理一些共享数据代码
myunique_lock.unlock();
//继续处理一些非共享代码
//。。。。。。
//处理完之后可以再次上锁
myunique_lock.lock();//对myunique_lock进行加锁操作

5.3.3成员函数try_lock()

在不阻塞的情况下进行lock,如果加锁成功,那么返回true,如果没有加锁成功,那么返回false。

5.3.4成员函数release()

通过release()会返回它所管理的mutex对象指针,并释放所有权(也就是说,这个unique_lock和mutex不再有关系)

release()之后要负责把上锁的mutex解锁,否则会报错。

std::unique_lock<std::mutex>myunique_lock(my_mutex1,std::defer_lock);//创建一个没有加锁的myunique_lock
myunique_lock.lock();//对myunique_lock进行加锁操作
//处理一些共享数据代码
std::mutex *ptr = myunique_lock.release();//释放myunique_lock并返回my_mutex1的指针
//处理共享数据
ptr->unlock();//手动解锁,如果不解锁会卡住导致程序崩溃

6.条件变量condition_variable、wait、notify_one、notify_all

6.1什么是条件变量

条件变量是允许多个线程相互交流的同步原语。它允许一定量的线程等待(可以定时)另一线程的提醒,然后再继续。条件变量始终关联到一个互斥。

std::condition_variable 类是同步原语,能用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量(条件)并通知 condition_variable 。

也就是当条件不满足时,相关线程被一直阻塞,直到某种条件出现,这些线程才会被唤醒。

然后通过使用my_cond的成员函数wait、notify_one、notify_all来进行条件相关的操作。

6.2如何使用条件变量

6.2.1使用条件

如果一个线程想要修改变量,必须满足以下条件:

  • 获得 std::mutex (典型地通过 std::lock_guard )
  • 在保有锁时进行修改
  • 在 std::condition_variable 上执行 notify_one 或 notify_all (不需要为通知保有锁)

即使共享变量是原子性的,它也必须在mutex的保护下被修改,这是为了能够将改动正确发布到正在等待的线程。

任意要等待std::condition_variable的线程,必须满足以下条件:

  • 获取std::unique_lock<std::mutex>,这个mutex正是用来保护共享变量(即“条件”)的
  • 执行wait, wait_for或者wait_until. 这些等待动作原子性地释放mutex,并使得线程的执行暂停
  • 当获得条件变量的通知,或者超时,或者一个虚假的唤醒,那么线程就会被唤醒,并且获得mutex. 然后线程应该检查条件是否成立,如果是虚假唤醒,就继续等待。

note:  所谓虚假唤醒,就是因为某种未知的罕见的原因,线程被从等待状态唤醒了,但其实共享变量(即条件)并未变为true。因此此时应继续等待

std::condition_variable 只可与 std::unique_lock<std::mutex> 一同使用;此限制在一些平台上允许最大效率。 std::condition_variable_any 提供可与任何基础可锁 (BasicLockable) 对象,例如 std::shared_lock 一同使用的条件变量。

6.2.2 成员函数wait

condition_variable 容许 wait 、 wait_for 、 wait_until 、 notify_one 及 notify_all 成员函数的同时调用。
示例:

#include <iostream>
#include <condition_variable>

using namespace std;

mutex wait_mutex;
condition_variable wait_condition_variable;

// 等待线程函数
void wait_thread_func()
{
    unique_lock<mutex> lock(wait_mutex);
    cout << "等待线程(" << this_thread::get_id() << "): 开始等待通知..." << endl;
    wait_condition_variable.wait(lock);
    cout << "等待线程(" << this_thread::get_id() << "): 继续执行代码..." << endl;
}

int main()
{
    thread wait_thread(wait_thread_func);

    this_thread::sleep_for(1s); // 等待1秒后进行通知
    cout << "通知线程(" << this_thread::get_id() << "): 开始通知等待线程..." << endl;
    wait_condition_variable.notify_one();
    wait_thread.join();
    cout << "--- main结束 ---" << endl;
}

6.2.3成员函数notify_one和notify_all

notify_one只会唤醒一个线程的wait(),而如果有多个线程需要等待唤醒,那么需要使用notify_all()来唤醒所有线程中的wait()。

 

 

参考链接:

https://www.runoob.com/w3cnote/cpp-std-thread.html

https://blog.csdn.net/u013620306/article/details/128565614

https://blog.csdn.net/milkhoko/article/details/118282922

https://www.cnblogs.com/xiaohaigegede/p/14008121.html

https://blog.csdn.net/qq_39277419/article/details/99544724

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章