线程库
在程序员或者用户的角度,只认识线程,在Linux中并没有创建线程的接口,只有轻量级进程;线程库的存在能够解决这个问题
首先,线程库会被不同的用户使用,其中肯定存在着许多的线程,这时便需要进程管理:先描述,再组织;在库中创建线程控制块存储线程的必要属性;线程控制块调用创建轻量级进程的接口,在操作系统中创建对应的轻量级进程,从而以轻量级进程来代替模拟线程
在虚拟地址空间中,存在着 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; }
火车票是共享资源,这四个线程交叉执行,频繁地发生调度与切换;当线程从内核态返回用户态时,线程需要对调度状态进行检测,如果可以,直接发现线程切换;由结果来看,对一个全局变量进行多线程更改并不是安全的
正常来说,单线程对全局变量进行修改,分为三个步骤:将变量读取到CPU中的寄存器;进行运算;将运算结果写回内存中
但是,多线程对全局变量进行修改却不是如此;由于更改数据并不是原子性的,会存在某一个线程正在执行时,突然被切换的情况
例如:当 tickets==1,线程1将数据读取到寄存器中,发生线程切换;线程1只能将自身的上下文进行保存;待线程2执行完毕,此时tickets==0,将线程1切回,但是由于线程1所保存的上下文中tickets==1,再次读取数据,进行运算,写回,由此车票的数目就变成了tickets==-1;为了解决这个问题,引入了互斥概念
互斥量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; }
上面的问题完美解决
该如何看待锁呢?
首先,加锁的前提是先让线程看到锁,类似全局变量,锁的本质也是共享资源;全局变量由锁来保护,锁由操作系统来保护
锁如果申请成功,线程向后执行;如果暂时没有成功,线程会发生阻塞
线程持有锁,才能进入临界区
当持有锁的线程被切换时,锁也被切换,其他线程是无法申请成功的,直到当前线程的锁被释放
加锁解锁的本质也是原子性的
常见锁的概念
死锁
概念
在多把锁的情况下,持有自己的锁,还要索要对方的锁;对方亦是如此便会造成死锁问题
死锁存在的原因:多线程中大部分资源是共享的,多线程访问可能会出现数据不一致的问题,为保证线程安全需要使用锁,由此便出现死锁的问题
死锁的必要条件
互斥:一个资源每次只能被一个执行流使用
请求与保持:一个执行流因请求资源而阻塞时,对已获得的资源持有不放
不剥夺:一个执行流已获得的资源,在未使用完之前,不能强行剥夺
循环等待:若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配
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; }
为上面代码中pthread_cond_wait中之所以会存在互斥量
是因为当该函数调用时,如果线程将锁抱走等待,就会导致其他线程只能进行等待,所以会以原子性的方式将锁释放,然后将自己挂起;当等待被唤醒时,会重新获取锁以保证后续共享资源的安全
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,会一直等下去;因此必须有一个线程通过某些操作改变共享资源,使之前不满足的条件变得满足,然后通知等待在条件变量上的线程;条件不会无缘无故地变得满足,必然会牵扯到共享资源,使用互斥量,以保证其安全