Linux之线程
- 线程之形
- 线程接口
- 线程安全
- 互斥锁
- 条件变量&信号量
- 生产者与消费者模型
- 线程池
线程之形
进程是资源分配的基本单位,而线程是进程内部的一个执行单元,也是 CPU 调度的基本单位。
线程之间共享进程地址空间、文件描述符与信号处理,但也有独立资源:寄存器、栈、线程ID和调度优先级等。
线程切换由于地址空间相同,上下文切换时寄存器切换和内存地址缓存诸如页表缓存TLB(快表)及硬件缓存切换成本比进程切换低得多。
Linux下的线程并非标准的、独立于进程实现的线程(参见windows实现线程),而是通过pthread库封装了轻量级进程LWP(light weight process)来模拟线程。具体的说法,详见下文。
说Linux的线程是用LWP模拟的意思是,Linux利用了进程的概念和机制来实现线程的功能,通过让不同的线程共享某些资源来达到所谓的“轻量级”效果。这种方式使得线程既能够享受到与进程相似的隔离性和保护性,又能够在同一程序内部高效地进行数据交换和通信。另,ps -aL查看LWP即线程信息。之前的ps -axj只能查看进程级别信息。
线程接口
- 线程创建:
int pthread_create(pthread_t *restrict thread,const pthread_attr_t *restrict attr,void *(*start_routine)(void *),void *restrict arg);
,thread输出型参数,存储用户层线程ID;attr线程属性,给NULL即可;start_routine线程执行函数;arg传给前者的参数。
pthread_create是glibc库提供的接口,它实际上,调用mmap()在堆区分配struct pthread(线程控制块 TCB,库描述与管理线程的结构体,存储内核级tid,线程栈指针与大小,TLS指针和线程状态、返回值等)和固定大小的线程栈以及线程局部存储(TLS,存储私有变量如errno),再调用int clone(int (*fn)(void *), void *child_stack, int flags, void *arg, ... /* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
fn即线程执行函数,arg其参数,child_stack传mmap出来的线程栈栈底地址,flags选项较多自行了解。然后,内核再创建一个task_struct描述新线程,其mm_struct指向同一个地址空间,pid实为内核级tid(t即thread),tgid线程组id为进程id(主线程tid和pid和进程pid是同一个)。而mmap区的struct pthread的起始虚拟地址即为用户层线程ID,作为上面函数第一个参数带出。
线程局部存储,在全局内置类型变量前加__thread修饰,使之成为各个线程局部存储区变量而非全局区变量。另外,pthread_setname_np(pthread_t,const char*)可将变量放至局部存储,实现取名字;pthread_getname_np(pthread_t,char*,size_t)得之。
- 线程终止:
void pthread_exit(void *retval);
终止当前线程,retval即线程返回值。注意,exit()终止进程。 - 等待线程结束:
int pthread_join(pthread_t thread, void **retval);
阻塞等待指定ID线程,retval带出上面的返回值。而线程返回值在终止后会被存放在struct pthread里,等join时拿出来。 - 线程分离:
int pthread_detach(pthread_t thread);
将指定线程置于分离状态,系统会在该线程结束后自动回收其资源而无需等待。可搭配pthread_t pthread_self(void);分离自己。
线程终止还包括在执行函数return和线程取消int pthread_cancel(pthread_t thread);
,后者给join的retval为-1。如果调用join则会释放其struct pthread和线程栈以及task_struct,如果detach则结束自动销毁它们。注意,动态开辟的堆区内存需要手动释放,否则内存泄漏。
线程安全
线程安全指多个线程并发访问共享资源时不出因为线程切换等引发的问题。通常需要互斥锁、条件变量等方案来保证。
互斥锁
临界资源是一次仅允许一个执行流使用的共享资源,临界区是访问临界资源的那段代码,互斥是多线程并发竞争式访问临界资源时、同一时间只有一个线程能进入临界区的情况。
互斥锁实际上就是同一时刻只让拿到锁的线程进入临界区,让并行变成串行访问。
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
初始化锁,后参为属性通常NULL,全局锁也可用PTHREAD_MUTEX_INITIALIZER初始化;pthread_mutex_lock(pthread_mutex_t *mutex);
阻塞式获取锁,非阻塞与定时自行了解;pthread_mutex_unlock(pthread_mutex_t *mutex);
释放锁;pthread_mutex_destroy(pthread_mutex_t *mutex);
销毁锁。
锁本身作为临界资源被访问,多线程并发去lock时是怎么保证互斥的?
伪代码如下:
lock:movb $0, %al//原子性将al写入0xchgb %al, mutex//原子性交换锁和al的内容,锁初始为1if(al寄存器的内容 > 0)return 0;//抢到锁了else挂起等待;goto lock;
unlock:movb $1, mutex//原子唤醒等待Mutex的线程;return 0;
原子性指一个操作只有01两态,即有无两态,无中间态。原子操作就是遵守原子性的操作。上面锁的代码中写入movb和交换xchgb为原子操作,如果一个线程抢到了锁,则mutex里为0,al里为1,其他线程永远拿不到这个1,因为mutex里为0,除非该线程unlock,再把1放回mutex。
关于RAII风格的lockguard,即构造lock mutex,析构unlock之。
条件变量&信号量
同步值在互斥前提下,多线程按序访问临界资源。
条件变量Condition即允许多线程排队等待某个条件变量就绪,等候通知从而按序访问临界资源。
pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
初始化条件变量,也可PTHREAD_COND_INITIALIZER;pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
等待条件,后参为互斥锁,该函数会1先解锁,既防止死锁(因为其他线程无法拿到锁并修改条件、发出通知)也提高性能,允许其他线程访问临界资源,2阻塞等待,进入条件变量的等待队列中,3一旦被唤醒成功,重新获取并上原来的锁。该函数应在while(检测条件)内部使用,因为存在伪唤醒(比如因为系统中断或异常处理、内核优化等导致调度变化)或等待失败、函数返回的情况;pthread_cond_signal(pthread_cond_t *cond);
唤醒一个等待该条件的线程;pthread_cond_broadcast(pthread_cond_t *cond);
唤醒所有;pthread_cond_destroy(pthread_cond_t *cond);
销毁之。
信号量Semaphore(此处是POSIX标准下的)是一种预定机制,想象去电影院前先买票再排队入场,信号量就是票,所有人互斥(通过原子操作保证)抢票来预定座位,然后排队进场看电影。
int sem_init(sem_t *sem, int pshared, unsigned int value);
初始化信号量,二参0为线程间共享、非0进程间共享,三参为初始值;sem_wait(sem_t *sem); // 如果信号量值大于0,则减1;否则阻塞
P操作,申请信号量即信号量减1,类比买票;sem_post(sem_t *sem);
V操作,释放信号量即信号量加1,类比退票或放票。
生产者与消费者模型
多个生产者并行生产商品,串行投入商品至超市,多个消费者串行拿取商品出超市,并行消费商品。
3种关系,生产者之间互斥,消费者之间互斥,生消之间互斥且同步;2种角色,生消;1个交易场所,以特定结构构成的内存空间。
可以通过阻塞队列存放任务+锁+条件变量同步等待(生产者生产任务了通知等待的消费者、满了等待消费者通知,消费者消费任务了通知等待的生产者、完了等待生产者通知)实现,也可以通过环形队列存放任务+锁+信号量(生产者对空位量P操作、任务量V操作,消费者对空位量V操作、任务量P操作)实现。
线程池
提前创造一批线程,等待任务到来,来一个召唤一个线程去处理一个。可采用锁+条件变量的方式实现。
最后两个模块的代码具体实现,还没考虑好是否要呈现以及如果要呈现的话要怎么呈现,所以只提供了思路。
线程over,终于进入网络部分了,理论系统学习生涯结束指日可待啊!