线程重要概念

线程重要概念

如何谈进程?

进程是代码的一次动态执行,系统中肯定不止一个进程,所以OS为了管理起这些进程创建了PCB这种结构用来描述关于进程的所有信息。在系统内核用链表把PCB组织起来,进行调度。

进程本质可以说是代码+数据,OS为每个进程分配一块虚拟地址空间mm_struct,里面放着PCB,堆栈区,共享内存映射区,代码区,静态区。虚拟地址空间通过MMU中页表项的对应关系映射到物理内存,为多进程共存提供了基础条件。

线程是什么?

线程是进程内部的一个控制序列。当创建一个进程时,里面至少有一个执行线程,称为主线程。

在linux系统中,没有真正的线程,用轻量级进程模拟线程。也就是系统中只有描述进程的PCB结构,而模拟线程的PCB可以执行进程的部分代码。

因为线程在进程的地址空间内执行,所以数据段,代码段共享,比如全局变量,函数 ,文件描述符在每个线程里都可以访问到。线程也有自己的上下文数据,和私有栈结构,保证进程是可以被独立调度的最小单位。

在创建进程(主线程)的时候操作系统分配了地址空间,之后线程就共享这块空间,说明进程是分配资源的最小单位。

线程本身可以执行进程的部分代码,而且有自己的上下文数据,所以是调度执行的最小单位。

为什么需要线程?

线程共享进程的代码和数据,因此创建简单。

进程间切换需要切换页表内容,线程则不需要。

处理IO密集型任务可以分配多个线程等待不同的IO操作,计算密集型分解到多个处理器上运行。

缺点:如果计算密集型线程数量比可用处理器多,切换线程的开销可能比本身计算量还大。健壮性降低,一个线程崩了,整个进程都崩了。

进程ID和线程ID

没有线程之前,一个进程对应内核的一个进程描述符PID,引入线程以后,每个线程都有自己的进程描述符,进程和内核描述符的关系变成 1:N,此时如何getpid 返回那个进程ID呢?

多线程的进程也称线程组,调用getpid 返回的是线程组中的 tgid。而一个线程的ID是就是PCB中的pid。

ps -eLf |head -1 && ps -eLf |grep a.out 大L选项可以查看线程ID

线程ID的本质

线程既然是用户态的,操作系统并不知道该如何调度维护线程,所以线程的维护是由pthread 库来维护的,因此库用进程地址空间上的一个地址去标识一个线程ID(因为地址具有唯一性)。而操作系统用lwp标识一个唯一的轻量级进程。

线程的互斥和同步

想要做到线程协同合作完成一项任务,经过的阶段依次是 互斥-》 同步 -》信号量。

互斥是让多个线程同时只有一个能访问到临界区,如果不加以互斥限制,访问临界资源就有大概率造成混乱的问题。加入互斥限制后,就以及能保证对临界资源的访问是正确的,哪怕后面的步骤没有做。

同步是在互斥的基础上保证线程以可靠的顺序访问临界资源,虽然互斥机制能保证程序的正确性,但是没有一个良好的调度规则会产生饥饿现象,好比一个生产者以非常慢的速度生产苹果,消费者以快速不断的轮询争夺临界资源但并不能拿到苹果,这时应该使用条件变量让消费者进行等待。

条件变量

条件变量和互斥锁搭配使用,实现对临界资源访问的有序性,本质是通过告知其他线程一个状态信息做到的。

例如消费者在发现可消费资源为空时,不应该让消费者再继续争夺临界资源,正确的方法是将生产者生产一个苹果这个动作作为一个条件,然后让消费者线程挂起去等待这个条件,同时消费者需要释放当前拿到的互斥锁,毕竟不能拿着锁去挂起。

此处挂起的本质:线程PCB放到某个条件的等待队列上,唤醒的时候把取出等待队列上的PCB放入就绪队列。

线程的挂起和释放锁的操作用pthread_cond_wait() 函数实现,该函数比较特殊,在调用的时候保证是在临界区里,唤醒的时候也需要操作系统保证拿到互斥锁才能在调用处继续向下执行。

典型的应用:生产者消费者模型
https://blog.csdn.net/hanzheng6602/article/details/80753925

读写锁

编写多线程的另一种常见场景,对临界资源写操作频率低,读操作频率高,并且读操作耗时长。

读者写者问题就是其典型,假如写者想要申请临界区的访问,然而读者占有资源时间非常长,这时如果让写者轮询查看是否可以申请到资源就太浪费了,更合理的是采用挂起等待的方式。其中前者也称为自旋方式,因此读写锁的本质是自旋锁。

同时注意和生产者消费者模型的区别是,读者不会像消费者一样修改临界资源,因此读者可以同时访问临界资源。

猜你喜欢

转载自blog.csdn.net/hanzheng6602/article/details/80767310