一文搞懂多线程中各个难点
一口Linux1.什么是线程?
linux内核中是没有线程这个概念的,而是轻量级进程的概念:LWP。一般我们所说的线程概念是C库当中的概念。
1.1线程是怎样描述的?
线程实际上也是一个task_struct,工作线程拷贝主线程的task_struct,然后共用主线程的mm_struct。线程ID是在用task_struct中pid描述的,而task_struct中tgid是线程组ID,表示线程属于该线程组,对于主线程而言,其pid和tgid是相同的,我们一般看到的进程ID就是tgid。
即:
![](/uploads/202208/03/wx_article__6154a515946444a92a97b9871d7d9fef3023.jpg)
获取线程ID和主线程ID的值:
![](/uploads/202208/03/wx_article__d29b5c050c5addfa5b34b8526d59af7a3023.jpg)
但是获取该gettid系统调用接口并没有被封装起来,如果确实需要获取线程ID,可使用:
#include
则对线程组而言,所有的tgid一定是一样的,所有的pid一定是不一样的。主线程pid和tgid一样,工作线程pid和tgid一定不一样。
1.2如何查看一个线程的ID
命令:ps -eLf
![](/uploads/202208/03/wx_article__7df4bee62260dc3571324e211af6070e3023.jpg)
上述polkitd进程是多线程的,进程ID为731,进程内有6个线程,线程ID为731,764,765,768,781,791。
![](/uploads/202208/03/wx_article__6acb497e77ad9bf017cf8a1b971df4313023.jpg)
1.3多线程如何避免调用栈混乱的问题?
工作线程和主线程共用一个mm_struct,如果都向栈中压栈,必然会导致调用栈出错。
实际上工作线程压栈是压了共享区,该共享区包含了许多线程独有的资源。如图:
![](/uploads/202208/03/wx_article__346f4c3f864a6c5c6bb850e8e2d785693024.jpg)
每一个线程,默认在共享区中占有的空间为8M,可以使用ulimit -s修改。
进程是资源分配的基本单位,线程是调度的基本单位。
1.3.1线程独有资源
线程ID
一组寄存器
errno
信号屏蔽字
调度优先级
1.3.2线程共享资源和环境
文件描述符表
信号的处理方式
当前工作目录
用户id和组id
1.4为什么要有多线程?
举个生活中的例子, 这就好比去银行办理业务。到达银行后, 首先取一个号码, 然后坐下来安心等待。这时候你一定希望, 办理业务的窗口越多越好。如果把整个营业大厅当成一个进程的话, 那么每一个窗口就是一个工作线程。
1.4.1线程带来的优势
1、线程会共享内存地址空间。
2、创建线程花费的时间要少于创建进程花费的时间。
3、终止线程花费的时间要少于终止进程花费的时间。
4、线程之间上下文切换的开销, 要小于进程之间的上下文切换。
5、线程之间数据的共享比进程之间的共享要简单。
6、充分利用多处理器的可并行数量。(线程会提高运行效率,但当线程多到一定程度后,可能会导致效率下降,因为会有线程调度切换。)
1.4.2线程带来的缺点
健壮性降低:多个线程之中, 只要有一个线程不够健壮存在bug(如访问了非法地址引发的段错误) , 就会导致进程内的所有线程一起完蛋。
线程模型作为一种并发的编程模型, 效率并没有想象的那么高, 会出现复杂度高、 易出错、 难以测试和定位的问题。
1.5注意
1、并不是只有主线程才能创建线程, 被创建出来的线程同样可以创建线程。
2、不存在类似于fork函数那样的父子关系, 大家都归属于同一个线程组, 进程ID都相等, group_leader都指向主线程, 而且各有各的线程ID。
通过group_leader指针, 每个线程都能找到主线程。主线程存在一个链表头,后面创建的每一个线程都会链入到该双向链表中。
3、并非只有主线程才能调用pthread_join连接其他线程, 同一线程组内的任意线程都可以对某线程执行pthread_join函数。
4、并非只有主线程才能调用pthread_detach函数, 其实任意线程都可以对同一线程组内的线程执行分离操作。
线程的对等关系:
![](/uploads/202208/03/wx_article__6f9f9fd0e20f6ff15de037405becfee83024.jpg)
2.线程创建
接口:int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
参数解释
1、thread:线程标识符,是一个出参
2、attr:线程属性
3、star_routine:函数指针,保存线程入口函数的地址
4、arg:给线程入口函数传参
返回值:成功返回0,失败返回error number
详解:
第一个参数是pthread_t类型的指针, 线程创建成功的话,会将分配的线程ID填入该指针指向的地址。线程的后续操作将使用该值作为线程的唯一标识。
第二个参数是pthread_attr_t类型, 通过该参数可以定制线程的属性, 比如可以指定新建线程栈的大小、 调度策略等。如果创建线程无特殊的要求, 该值也可以是NULL, 表示采用默认属性。
第三个参数是线程需要执行的函数。创建线程, 是为了让线程执行一定的任务。线程创建成功之后, 该线程就会执行start_routine函数, 该函数之于线程, 就如同main函数之于主线程。
第四个参数是新建线程执行的start_routine函数的入参。
pthread_create错误码及描述:
![](/uploads/202208/03/wx_article__04c285f5d307daef967b31a6beb443b23024.jpg)
2.1传入参数arg的选择
![](/uploads/202208/03/wx_article__4ee829418717b42d06cf9eb86e4e36a63024.jpg)
不要使用临时变量传参,使用堆上开辟的变量可以。
例:
#include
线程获取自身的ID:
#include
判断两个线程ID是否对应着同一个线程:
#include
返回为0时,则表示两个线程为同一个线程,非0时,表示不是同一个线程。
用户调用pthread_create函数时, 首先要为线程分配线程栈, 而线程栈的位置就落在共享区。调用mmap函数为线程分配栈空间。pthread_create函数分配的pthread_t类型的线程ID, 不过是分配出来的空间里的一个地址, 更确切地说是一个结构体的指针。
![](/uploads/202208/03/wx_article__14f088d05c795dc3bb0f0a636ac6358c3024.jpg)
即:
![](/uploads/202208/03/wx_article__c7c523bf5e0cf96e66c518e791d87b153024.jpg)
1 2 3 4 下一页>