侧边栏壁纸
博主头像
Into The Abyss 博主等级

My Life is a Death Race

  • 累计撰写 34 篇文章
  • 累计创建 7 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

c++_study_thread

Administrator
2023-11-09 / 0 评论 / 0 点赞 / 177 阅读 / 0 字

并发

两个或者更多的任务(独立的活动)同时发生,宏观上是同时进行的,微观上是同一时刻只有一个任务在进行;一个程序同时执行多个独立的任务

单核cpu:某一时刻只能进行一个任务,由操作系统调度,每秒钟进行多次的任务切换,不知真正的同时进行,这种切换(上下文切换)是由时间开销的,操作系统需要保存切换时的各种状态,执行进度等,切换回来时需要恢复

多核cpu:多处理器计算机,能够实现真正的并行执行多个任务

进程

当一个可执行程序运行起来后,就叫创建了一个进程

进程就是运行起来的程序

线程

每个进程就是执行起来的可执行程序,每个进程都有一个主线程,这个主线程是唯一的,也就是一个进程中只能有一个主线程,轻量级进程

当你执行可执行程序,产生了一个进程后,这个主线程就随着这个进程默默启动。

线程实际是执行代码的,线程可以理解为一条代码的执行通路

除了主线程之外,我们可以通过代码创建其他线程,其他线程走的是别的道路

每创建一个新线程,就可以在同一时刻多干一个不同的事

多线程(并发)

线程并不是越多越好,每个线程都需要一个独立的堆栈空间(1M),线程之间的切换需要保存很多中间状态,切换会耗费本该属于程序运行的时间.

多线程可以提高运行效率,但是不是很容易评估,需要在实际项目中不断优化

实现并发的方式

1)通过多个进程实现并发

2)在单独的进程中,创建多个线程来实现并发,自己写代码来创建除了主线程之外的其他线程

多进程并发

同一电脑上进程间通信:管道,文件,消息队列,共享内存

不同电脑上进程间通信:socket通信技术

多线程并发

同一进程中的所有线程共享地址空间(共享内存)

全局变量,指针,引用都可以在线程之间传递,所以使用多线程开销远远小于多进程

共享内存带来的问题:数据一致性问题

多进程和多线程虽然可以混合使用 ,但优先考虑多线程

c++11新标准线程库

以往的不能跨平台,windows:CreateThread(),_beginthred(),_beginthredexe()创建线程

linux:pthread_create()创建线程

从c11新标准,c本身增加对多线程的支持,增加了可移植性

示例

主线程从main()开始执行,我们自己创建的线程,也需要从一个函数开始运行(初始函数),一旦这个函数运行完毕,就代表这个线程结束

整个函数执行完毕的标志是主线程是否执行完毕,如果主线程执行完毕,就代表整个线程执行完毕,一般情况下,如果还有其他子线程没有执行完毕,那么这些子线程会被操作系统强行终止。一般情况下,如果想保持子线程的运行状态,那么就要让主线程一直保持运行(有例外)

1)要包含#include<thread>

2)初始函数

#include<thread>
#include<iostream>
using namespace std;
void myprint()
{
    cout<<"myprint_start"<<endl;
    cout<<"myprint_end"<<endl;
}
int main(int argc, char const *argv[])
{
    /* code */
    thread mytobj(myprint); //创建了线程,线程起点为myprint(),并让myprint()开始执行
    mytobj.join(); //阻塞主线程,让主线程等待子线程执行完毕,然后主线程和子线程汇合,然后主线程继续往下执行
    cout<<"main"<<endl;
    return 0;
}
/*输出:      //join阻塞主线程,等待子线程执行完
  myprint_start
  myprint_end
  main
*/

linux使用g++编译标准线程库项目时,需要-lpthread参数,如上面的代码名为test.cpp,编译命令为g++ test7.cpp -o test7 -lpthread

如果主线程执行完毕,但子线程没执行完毕,这种程序是不稳定的

detach():传统的多线程程序需要等待子线程执行完毕,然后主线程退出;但是detach()可以让主线程与子线程分离,主线程可以先执行结束,不用等待子线程执行结束。一旦detach()之后,与主线程关联的thread对象就会失去与主线程的关联,此时子线程就会驻留在后台运行,子线程相当于被c++运行时库接管,当这个子线程执行完毕后,由运行时库负责清理该线程的相关资源(守护线程)

joinable():判断是否可以使用join()和detach()

其他创建线程的手法

使用类创建

class TA
{
public:
    TA(){
        cout<<"TA()"<<endl;
    }
    TA(const TA &ta){
        cout<<"TA(const TA &ta)"<<endl;
    }
    ~TA(){
        cout<<"~TA()"<<endl;
    }
    void operator()(){
        cout<<"TA_start"<<endl;
        cout<<"TA_end"<<endl;
    }
};

TA ta;  //TA()
thread myobj(ta);  //TA(const TA &ta),TA_start,TA_end,~TA()  析构拷贝构造函数创建的TA对象
myobj.join();
cout<<"main"<<endl;  //main
//~TA()  析构ta

如果使用detach(),主线程执行完毕了,那么ta这个对象还在吗?

这个对象实际是被复制到线程中,执行完主线程,ta被销毁,但是所复制的TA对象依旧存在;所以只要TA类中没有引用,指针,就不会产生问题

使用lambda表达式创建

auto mylamthread = []{
    cout<<"mylamthread_start"<<endl;
    cout<<"mylamthread_end"<<endl;
}
thread myobj(mylamthread); 
myobj.join();
cout<<"main"<<endl;

线程传参

传递临时对象作为线程参数

#include<thread>
#include<iostream>
using namespace std;
void myprint(const int &i,char *buf) //如果使用detach,不推荐使用引用,不能使用指针
{
    cout<<i<<endl;  //分析认为,i并不是myarg的引用,实际是值传递,即使主线程detach()那么子线程中也是安全的
    cout<<buf<<endl;  //指针如果使用detach()则是不安全的,主线程执行完毕会被回收
}

void myprint2(const int i,const string &buf)
{        
    cout<<i<<endl;  
    cout<<buf<<endl; 
}

int main(int argc, char const *argv[])
{
    int myarg=10;
    char mybuf[] = "this is a test";
    // thread t1(myprint,myarg,mybuf); //不安全
    // thread t1(myprint2,myarg,mybuf); //有隐患,事实上存在,mybuf被回收了,系统才用mybuf转string的可能性
    thread t1(myprint2,myarg,string(mybuf)); //稳定
    t1.join();
    cout<<"main"<<endl;
    return 0;
}

总结

1)若传递int这种简单类型参数,建议都是值传递,不要用引用

2)如果传递类对象,避免隐式类型转换,全部都在创建线程这一行就构建出临时对象,然后再函数参数中使用引用来接收;否则系统还会创建一个临时对象

建议不使用detach(),只是用join(),就不会出现临时对象失效导致线程对内存的非法引用的问题

线程id

每个线程,不管是主线程还是子线程,实际上都对应一个数字,而且每个线程对应的这个数字都不相同,也就是说不同线程他们的线程id不同,可以使用标准库std::this_thread()::get_id()获取

传递类对象、智能指针作为线程参数

class TA
{
public:
    // mutable int m_i; //在子线程中修改m_i的值,主线程仍然不改变
    int m_i;
    TA(int tmp):m_i(tmp){
        cout<<"TA()"<<endl;
    }
    TA(const TA &ta):m_i(ta.m_i){
        cout<<"TA(const TA &ta)"<<endl;
    }
    ~TA(){
        cout<<"~TA()"<<endl;
    }
};

void myprint(TA &ta) //不使用std::ref()时,必须加const
{        
    ta.m_i = 199; // 可以正常修改
    cout<<”thtreadid=“<<this_thread()::get_id()<<endl; 
}

void myprint2(unique_ptr<int> pzn) 
{        
    cout<<”thtreadid=“<<this_thread()::get_id()<<endl; 
}

unique_ptr<int> myp(new int(100));
//thread myobjp(myprint2,myp);  //不能直接传递
thread myobjp(myprint2,move(myp)); //不能配detach()
myobjp.join();

TA ta(10);  
thread myobj(myprint,ref(ta));  
myobj.join();
cout<<"main"<<endl; 

std::ref():传递的参数不会再拷贝一份,是真正的引用

创建和等待多个线程

//线程入口函数
void myprint(const int i)
{
    cout<<"myprint_start"<<i<<endl;
    cout<<"myprint_end"<<i<<endl;
}

vector<thread> mythreads;
for(int i = 0;i<10;i++)
{
    mythreads.push_back(thread(myprint,i));
}
for(auto iter = mythreads.begin();iter != mythreads.end();iter++)
{
    iter->join();
}
cout<<"all finish"<<endl;

多个线程的执行顺序是乱的,跟操作系统内部的调度机制有关;主线程等待所有子线程运行结束,最后主线程结束,推荐使用join(),程序更加稳定

数据共享问题

只读的数据

只读的数据是安全稳定的,不需要特别的处理手段,直接读就可以

有读有写的数据

多个线程写,多个线程读,如果代码没有特殊处理,程序肯定崩溃

最简单的处理方式:读的时候不能写,写的时候不能读,多个线程不能同时写,多个线程不能同时读

其他案例

火车订票

共享数据保护示例

class A
{
public:
    //收到的数据加入到消息队列的线程
    void inmsgRecvQueue()
    {
        for(int i = 0; i<100000;i++)
        {
            cout<<"inmsgRecvQueue()执行,插入一个数据:"<<i<<endl;
            msgRecvQueue.push_back(i);
        }
    }
    //把数据从消息队列中取出的线程
    void outmsgRecvQueue()
    {
        for(int i = 0; i<100000;i++)
        {
            if(!msgRecvQueue.empty())
            {
                cout<<"outmsgRecvQueue()执行,取出一个数据:"<<msgRecvQueue.front()<<endl;
                msgRecvQueue.pop_front();
            }
            else
            {
                cout<<"outmsgRecvQueue()执行,消息队列为空"<<i<<endl;
            }
        }
    }
private:
    list<int> msgRecvQueue;
};

A  myobj;
thread mymsginobj(&A::inmsgRecvQueue,&myobj);  //第二个参数必须为引用,这样才能保证线程中用的是同一个对象
thread mymsgoutobj(&A::outmsgRecvQueue,&myobj);
mymsginobj.join();
mymsgoutobj.join();
// 有异常,又读有写,同时操作出现异常

互斥量

保护共享数据操作时,用代码将共享数据锁住,操作数据,解锁,其他想操作共享数据的线程必须等待共享线程解锁

互斥量是个类对象,可以理解为一把锁,多个线程尝试用lock()成员函数加锁,只有一个线程可以锁定成功,成功的标志是lock()函数返回,如果没有锁成功,那么流程会阻塞在lock()这里直到成功

使用互斥量mutex,需要#include<mutex>,使用时先lock(),操作共享数据,再unlock()。lock()和unlock()必须成对使用,非对称数量的调用会导致代码不稳定

class A
{
public:
    //收到的数据加入到消息队列的线程
    void inmsgRecvQueue()
    {
        for(int i = 0; i<100000;i++)
        {
            my_mutex.lock();
            cout<<"inmsgRecvQueue()执行,插入一个数据:"<<i<<endl;
            msgRecvQueue.push_back(i);
            my_mutex.unlock();
        }
    }
    //把数据从消息队列中取出的线程
    void outmsgRecvQueue()
    {
        for(int i = 0; i<100000;i++)
        {
            my_mutex.lock();
            if(!msgRecvQueue.empty())
            {
                cout<<"outmsgRecvQueue()执行,取出一个数据:"<<msgRecvQueue.front()<<endl;
                msgRecvQueue.pop_front();
                my_mutex.unlock();
            }
            else
            {
                cout<<"outmsgRecvQueue()执行,消息队列为空"<<i<<endl;
                my_mutex.unlock();
            }
        }
    }
private:
    list<int> msgRecvQueue;
    mutex my_mutex; //创建一个互斥量
};

为了防止忘记unlock(),引入了std::lock_guard()的类模板,可以自动unlock(),类似于智能指针自动释放内存;std::lock_guard()直接取代lock()和unlock(),用了std::lock_guard(),就不能再使用lock()和unlock()了

class A
{
public:
    //收到的数据加入到消息队列的线程
    void inmsgRecvQueue()
    {
        for(int i = 0; i<100000;i++)
        {
            my_mutex.lock();
            cout<<"inmsgRecvQueue()执行,插入一个数据:"<<i<<endl;
            msgRecvQueue.push_back(i);
            my_mutex.unlock();
        }
    }
    //把数据从消息队列中取出的线程
    void outmsgRecvQueue()
    {
        for(int i = 0; i<100000;i++)
        {
            std::lock_guard<std::mutex> sbguard(my_mutex);
            if(!msgRecvQueue.empty())
            {
                cout<<"outmsgRecvQueue()执行,取出一个数据:"<<msgRecvQueue.front()<<endl;
                msgRecvQueue.pop_front();
            }
            else
            {
                cout<<"outmsgRecvQueue()执行,消息队列为空"<<i<<endl;
            }
        }
    }
private:
    list<int> msgRecvQueue;
    mutex my_mutex; //创建一个互斥量
};

死锁:要产生死锁,必须至少含有两个互斥量。

有两个锁lock1和lock2,两个线程都需要两个互斥量加锁,线程A执行的时候,先加锁lock1,再加锁lock2;线程B相反。线程A加锁lock1之后,线程切换,线程B再加锁lock2,这样就形成了死锁

保证两个互斥量的上锁顺序一致就不会产生死锁

std::lock()函数模板:可以一次加锁两个及两个以上的互斥量,可以有效解决因为锁的顺序问题导致的死锁风险。用了std::lock(),还是得使用unlock()来解锁;也可以使用下面的方法进行处理

std::lock(my_mutex1,my_mutex2);
std::lock_guard<std::mutex> sbguard1(my_mutex1,std::adopt_lock);
std::lock_guard<std::mutex> sbguard2(my_mutex2,std::adopt_lock);
//这样就不需要unlock()了

std::adopt_lock:是个结构体对象,起一个标志作用,作用是表示这个互斥量已经Lock了,不需要在std::lock_guardstd::mutex构造函数中再对mutex对象加锁

unique_lock

unique_lock是一个类模板,工作中,一般使用lock_guard()(推荐使用)

unique_lock比lock_guard更加灵活,但是效率上差一点,内存占用多一点

正常情况下,unique_lock可以直接替换lock_guard使用

unique_lock的第二个参数

std::adopt_lock:表示这个互斥量已经Lock了,不需要在std::lock_guardstd::mutex构造函数中再对mutex对象加锁,使用之前需要先加锁;lock_guard也可以带这个参数,含义相同

std::try_to_lock:尝试使用mutex的lock()来锁定这个mutex,如果没有成功,会立即返回,并不会阻塞;前面不能lock()

class A
{
public:
    //收到的数据加入到消息队列的线程
    void inmsgRecvQueue()
    {
        for(int i = 0; i<100000;i++)
        {
            std::unique_lock<mutex> sbguard(my_mutex, std::try_to_lock);
            if(sbguard.owns_lock())
            {
                cout<<"inmsgRecvQueue()执行,插入一个数据:"<<i<<endl;
                msgRecvQueue.push_back(i);
            }else{
                cout<<"inmsgRecvQueue()执行,没有拿到锁"<<i<<endl;
            }
        }
    }
    //把数据从消息队列中取出的线程
    void outmsgRecvQueue()
    {
        for(int i = 0; i<100000;i++)
        {
            std::unique_lock<std::mutex> sbguard(my_mutex);
            std::chrono::milliseconds dura(100);
            std::this_thread::sleep_for(dura);
            if(!msgRecvQueue.empty())
            {
                cout<<"outmsgRecvQueue()执行,取出一个数据:"<<msgRecvQueue.front()<<endl;
                msgRecvQueue.pop_front();
            }
            else
            {
                cout<<"outmsgRecvQueue()执行,消息队列为空"<<i<<endl;
            }
        }
    }
private:
    list<int> msgRecvQueue;
    mutex my_mutex; //创建一个互斥量
};

std::defer_lock:使用defer_lock()时,不能先lock();并没有给mutex加锁,即初始化一个未加锁的mutex,就可以调用unique_lock的成员函数

lock():unique_lock的加锁函数,不需要unlock()

unlock():unique_lock的解锁函数,可以随时解锁

try_lock():尝试给互斥量加锁,如果返回true,表示拿到锁;返回false,表示未拿到锁。这个函数是非阻塞的

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

有人把锁头锁住的代码多少称为锁的粒度,锁住的代码少,则粒度细,执行效率高;锁住的代码多,粒度粗,执行效率低。所以要尽量选择合适的粒度进行保护

0
c++

评论区