Linux 多线程(二)

简介: Linux 多线程

线程库


在程序员或者用户的角度,只认识线程,在Linux中并没有创建线程的接口,只有轻量级进程;线程库的存在能够解决这个问题

45ae2fe5212158e8ef7bf2461f90e68a_e31897bfa4984e40bfd3414ed2ef1187.png


首先,线程库会被不同的用户使用,其中肯定存在着许多的线程,这时便需要进程管理:先描述,再组织;在库中创建线程控制块存储线程的必要属性;线程控制块调用创建轻量级进程的接口,在操作系统中创建对应的轻量级进程,从而以轻量级进程来代替模拟线程


e8af6bbc18c49c6ffa0c74436faad0c8_de2a47ad307d49a18827400d82142106.png


在虚拟地址空间中,存在着 mmp区域其中包含动态库,线程库也在其中;上面介绍到:线程库中存在着线程控制块,也就是结构体,结构体的起始地址就是线程id,线程栈也解释了为什么线程都有自己的私有栈;可通过添加__thread将内置类型数据设置为局部存储


模拟实现创建线程


class Thread;
class Context
{
public:
    Thread* _this;
    void* _args;
public:
    Context()
    :_this(nullptr)
    ,_args(nullptr)
    {}
    ~Context()
    {}
};
class Thread
{
public:
    typedef function<void*(void*)> func_t;
    const int num=1024;
public:
    Thread(func_t func,void*args=nullptr,int number=0)
    :_func(func)
    ,_args(args)
    {
        char buffer[num];
        snprintf(buffer,sizeof(buffer),"thread:%d",number);
        _name=buffer;
        Context* ctx=new Context();
        ctx->_this=this;
        ctx->_args=_args;
        int n=pthread_create(&_tid,nullptr,start_routine,ctx);
        assert(n==0);
        (void)n;
    }
    static void* start_routine(void*args)
    {
        Context* ctx=static_cast<Context*>(args);
        void* ret=ctx->_this->run(ctx->_args);
        delete ctx;
        return ret;
    }
    void join()
    {
        int n=pthread_join(_tid,nullptr);
        assert(n==0);
        (void)n;
    }
    void* run(void* args)
    {
        return _func(args);
    }
    ~Thread()
    {}
private:
    string _name;
    func_t _func;
    void* _args;
    pthread_t _tid;
};


线程互斥


线程间的互斥相关概念


临界资源:多线程执行流共享的资源称作临界资源

临界区:每个线程内部,访问临界资源的代码

互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,对临界资源起到保护作用

原子性:不会被任何调度机制打断的操作,该操作只有两态:要么完成,要么未完成

在没有互斥的情景下,模拟抢票过程


int tickets=1000;
void* getticket(void* args)
{
    string username=static_cast<const char*>(args);
    while(true)
    {   
        //满足条件才能抢票
        if(tickets>0)
        {
            usleep(1234);
            cout<<username<<"正在抢票..."<<tickets<<endl;
            tickets--;
        }
        else{
            break;
        }
    }
}
int main()
{
    pthread_t tid1,tid2,tid3,tid4;
    pthread_create(&tid1,nullptr,getticket,(void*)"user1");
    pthread_create(&tid2,nullptr,getticket,(void*)"user2");
    pthread_create(&tid3,nullptr,getticket,(void*)"user3");
    pthread_create(&tid4,nullptr,getticket,(void*)"user4");
    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);
    pthread_join(tid3,nullptr);
    pthread_join(tid4,nullptr);
    return 0;
}


0aabbb4d10fec2f8e8fd4910be45d03a_cc9dbedac0dc45bfaf84c250dc31d1f1.png


火车票是共享资源,这四个线程交叉执行,频繁地发生调度与切换;当线程从内核态返回用户态时,线程需要对调度状态进行检测,如果可以,直接发现线程切换;由结果来看,对一个全局变量进行多线程更改并不是安全的


7d3ca1a91cfdc302531cef2d657cd9fc_9317a2270dcf459395c99c70c7a3b4ef.png


正常来说,单线程对全局变量进行修改,分为三个步骤:将变量读取到CPU中的寄存器;进行运算;将运算结果写回内存中


但是,多线程对全局变量进行修改却不是如此;由于更改数据并不是原子性的,会存在某一个线程正在执行时,突然被切换的情况

例如:当 tickets==1,线程1将数据读取到寄存器中,发生线程切换;线程1只能将自身的上下文进行保存;待线程2执行完毕,此时tickets==0,将线程1切回,但是由于线程1所保存的上下文中tickets==1,再次读取数据,进行运算,写回,由此车票的数目就变成了tickets==-1;为了解决这个问题,引入了互斥概念


d7c0eb087bd1739075b5751e0429a9db_a7b6cd52728944c2aafe42c8b323d49c.png


互斥量mutex


大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内;这种情景,变量属于单个线程,其他线程无法获得该变量

有时,许多变量需要在线程内共享,此变量称作共享变量,可以通过数据共享,完成线程之间的交互

多线程并发操作的共享变量,会引发一些问题

互斥量就是锁,使用互斥量可以让线程串行执行,由此了解决上面的问题


锁的使用分为两种:

全局


pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;


局部,需要先初始化后销毁


int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);


接下来采取全局定义锁的方式对线程进行上锁,在加锁和解锁之间的代码称为临界区


//锁的定义
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int tickets=1000;
void* getticket(void* args)
{
    string username=static_cast<const char*>(args);
    //加锁
    pthread_mutex_lock(&lock);
    while(true)
    {
        if(tickets>0)
        {
            usleep(1234);
            cout<<username<<"正在抢票..."<<tickets<<endl;
            tickets--;
            //解锁
            pthread_mutex_unlock(&lock);
        }
        else{
            //解锁
            pthread_mutex_unlock(&lock);
            break;
        }
        //抢票成功之后的工作
        usleep(1000);
    }
}
int main()
{
    pthread_t tid1,tid2,tid3,tid4;
    pthread_create(&tid1,nullptr,getticket,(void*)"user1");
    pthread_create(&tid2,nullptr,getticket,(void*)"user2");
    pthread_create(&tid3,nullptr,getticket,(void*)"user3");
    pthread_create(&tid4,nullptr,getticket,(void*)"user4");
    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);
    pthread_join(tid3,nullptr);
    pthread_join(tid4,nullptr);
    return 0;
}


beb74c57a8aea9433027b852925b27c3_318ce7197ec841cba25f950d9fd2c76c.png


上面的问题完美解决


该如何看待锁呢?


首先,加锁的前提是先让线程看到锁,类似全局变量,锁的本质也是共享资源;全局变量由锁来保护,锁由操作系统来保护

锁如果申请成功,线程向后执行;如果暂时没有成功,线程会发生阻塞

线程持有锁,才能进入临界区

当持有锁的线程被切换时,锁也被切换,其他线程是无法申请成功的,直到当前线程的锁被释放

加锁解锁的本质也是原子性的


常见锁的概念


死锁


概念

在多把锁的情况下,持有自己的锁,还要索要对方的锁;对方亦是如此便会造成死锁问题

死锁存在的原因:多线程中大部分资源是共享的,多线程访问可能会出现数据不一致的问题,为保证线程安全需要使用锁,由此便出现死锁的问题


死锁的必要条件


互斥:一个资源每次只能被一个执行流使用

请求与保持:一个执行流因请求资源而阻塞时,对已获得的资源持有不放

不剥夺:一个执行流已获得的资源,在未使用完之前,不能强行剥夺

循环等待:若干执行流之间形成一种头尾相接的循环等待资源的关系

避免死锁


破坏死锁的四个必要条件

加锁顺序一致

避免锁未释放的场景

资源一次性分配


Linux线程同步


条件变量


当一个线程互斥地访问某个变量时,他可能发现在其他线程改变状态之前,自己什么也做不了


举个栗子

在企业招聘时,每个应聘者都被告知自己的号码。当叫号到自己时,就到对应的屋子里去面试,这里的号码就是条件变量,当没有叫到自己时,只能排队等待,只有叫到自己才能去面试


当条件不满足时,线程必须去某些定义好的条件变量进行等待


条件变量函数


全局定义


pthread_cond_t cond = PTHREAD_COND_INITIALIZER;


局部定义


int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);


等待条件满足


int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);


唤醒等待线程


唤醒一个
int pthread_cond_signal(pthread_cond_t *cond);
唤醒一批
int pthread_cond_broadcast(pthread_cond_t *cond);


销毁


int pthread_cond_destroy(pthread_cond_t *cond);


在抢票线程中加入条件变量,先让所有线程等待,间隔几秒后全部唤醒


//票数
int tickets=100;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
void*start_routine(void*args)
{
    string name=static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        //等待条件满足
        pthread_cond_wait(&cond,&mutex);
        //省略判断
        cout<<name<<tickets<<endl;
        tickets--;
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
    pthread_t t[5];
    for(int i=0;i<5;i++)
    {
        char name[64];
        snprintf(name,sizeof(name),"thread: %d",i+1);
        pthread_create(t+i,nullptr,start_routine,name);
    }
    while(true)
    {
        sleep(2);
        pthread_cond_broadcast(&cond);
        cout<<"mian thread wakeup ..."<<endl;
    }
    for(int i=0;i<5;i++)
    {
        pthread_join(t[i],nullptr);
    }
    return 0;
}


c190abf45f86eed3b67a75f59eb1e047_dd5e3b4c3d484c4c9931ffb6cfdf8ff4.png


为上面代码中pthread_cond_wait中之所以会存在互斥量

是因为当该函数调用时,如果线程将锁抱走等待,就会导致其他线程只能进行等待,所以会以原子性的方式将锁释放,然后将自己挂起;当等待被唤醒时,会重新获取锁以保证后续共享资源的安全


条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,会一直等下去;因此必须有一个线程通过某些操作改变共享资源,使之前不满足的条件变得满足,然后通知等待在条件变量上的线程;条件不会无缘无故地变得满足,必然会牵扯到共享资源,使用互斥量,以保证其安全


相关实践学习
CentOS 7迁移Anolis OS 7
龙蜥操作系统Anolis OS的体验。Anolis OS 7生态上和依赖管理上保持跟CentOS 7.x兼容,一键式迁移脚本centos2anolis.py。本文为您介绍如何通过AOMS迁移工具实现CentOS 7.x到Anolis OS 7的迁移。
目录
相关文章
|
5月前
|
消息中间件 存储 缓存
【嵌入式软件工程师面经】Linux系统编程(线程进程)
【嵌入式软件工程师面经】Linux系统编程(线程进程)
125 1
|
3月前
|
算法 Unix Linux
linux线程调度策略
linux线程调度策略
77 0
|
1月前
|
资源调度 Linux 调度
Linux C/C++之线程基础
这篇文章详细介绍了Linux下C/C++线程的基本概念、创建和管理线程的方法,以及线程同步的各种机制,并通过实例代码展示了线程同步技术的应用。
29 0
Linux C/C++之线程基础
|
1月前
|
安全 Linux
Linux线程(十一)线程互斥锁-条件变量详解
Linux线程(十一)线程互斥锁-条件变量详解
|
3月前
|
存储 设计模式 NoSQL
Linux线程详解
Linux线程详解
|
3月前
|
缓存 Linux C语言
Linux线程是如何创建的
【8月更文挑战第5天】线程不是一个完全由内核实现的机制,它是由内核态和用户态合作完成的。
|
3月前
|
负载均衡 Linux 调度
在Linux中,进程和线程有何作用?
在Linux中,进程和线程有何作用?
|
3月前
|
缓存 Linux C语言
Linux中线程是如何创建的
【8月更文挑战第15天】线程并非纯内核机制,由内核态与用户态共同实现。
|
5月前
|
API
linux---线程互斥锁总结及代码实现
linux---线程互斥锁总结及代码实现
|
5月前
|
Linux API
Linux线程总结---线程的创建、退出、取消、回收、分离属性
Linux线程总结---线程的创建、退出、取消、回收、分离属性