目录
一、线程概念
1.1线程和进程的关系(工厂和流水线的关系)
- 结论1 :线程是依附于进程才能存在的,如果没有 进程,则线程不会单独存在
- 结论2 :多线程的是为了提高整个程序的运行效率的
- 线程也被称之为执行流,因为在执行用户写的代码(程序员创建的线程被称之为“工作线程”
1.2pid本质上是轻量级进程id,换句话说,就是线程ID
- 在task_ struct当中
- pid_ t pid; //轻量级进程id, 也被称之为线程id
- 不同的线程拥有不同的pid,表示是不同的线程
- pid_ t tgid; //轻量级进程组id, 也被称之为进程id
- 一个进程当中的不同线程拥有相同的tgid,表示是同一个进程
- 为什么进程概念的时候,说pid就是进程 id?
- 线程因为主线程的pid和tgid相等,而我们当时进程中只有一个主线程。所以我们的pid就等于tgid。所以将pid成为进程id也就是现在的tgid。
、
1.3 linux内核是如何创建一个线程的呢?
- 其本质就是再在当前进程组中创建一个task_struct结构体,它拥有着和主线程不同的pid,指向同一块虚拟进程地址空间
进程是操作系统分配资源的最小单位,
线程是操作系统调度的基本单位
1.4线程的共享与独有
独有
- 在进程虚拟地址空间的共享区当中,调用栈,寄存器, 线程ID, errno, 信号屏蔽字, 调度优先级独有
1、调用栈
如果主线程和工作线程共用一个栈,就会出现调用栈混乱的问题,例如:
f1函数调用完毕了,但是销毁不了,因为工作线程的func2函数没有工作完
2、寄存器
当操作系统调度进程的时候一定是以task struct结构体调度而task struc结构体是以双向列表存储,而操作系统调度时是从就绪队列中调度已经就绪的进程,在这里也就是轻量级进程-线程,当调度时一定会有其他线程被切出,而寄存器就是用来保存当线程进行切换的时候,它独有的内容
3.errno:
当线程在执行出错时会返回一个errno,这个errno属于当前自己的线程错误。
4.信号屏蔽字:阻塞位图
5.调度优先级:每个进程在执行时被调度的优先顺序。
共享
- 文件描述符表,用户id, 用户组id, 信号处理方式(操作系统定义的信号处理方式), 当前进程的工作目录
1.5线程的优缺点:(重中之重)
- 优点:
- 1.多线程的程序,拥有多个执行流,合理使用(要保证结果运行结构正确,例如多个进程并发执行就可能会出现同时更改一块内存,从而出现运行结果错误), 可以提高程序的运行效率
- 2.多线程程序的线程切换比多进程程序快,付出的代价小 (因为这些线程指向同一个进程虚拟地址空间,有些可以共享的数据(全局变量)就能在线程切换的时候,不进行切换)
- 可以充分发挥多核CPU并行(并行就是有多个每个CPU执行一个线程,各自执行各自的)的优势。
- 3.计算密集型的程序,可以进行拆分,让不同的线程执行计算不一 样的事情(比如我们要从1加到1亿我们可以让多个进程来各自计算其中一段加法,可以更快的得出结果。)
- 4. I/ 0密集型的程序,可以进行拆分, 让不同的线程执行不同的I/ 0操作,可以不用串型运行, 提高程序运行效率。
在之前的概念里,scanf()没有完成的时候,就不能printf(),但现在让两个不同的线程分别进行这两个操作,那么效率就提高了不少
- 缺点:
- 1.编写代码的难度更加高(当多个线程访问同一个程序的时候我们需要控制线程访问的先后顺序,要不然就可能出现问题)
- 2.代码的(稳定性)鲁棒性要求更加高,一个线程崩溃,会导致整个进程退出
- 3.线程数量并不是越多越好,线程的切换是需要时间的
- 3.缺乏访问控制,多个线程同时访问一个空间,如果不加以控制,可能会导致程序产生二义性结果。
二、线程控制
2.1线程创建
- int pthread_ create(pthread _t *thread, const pthread_ attr _t *attr ,void *(*start_ routine) (void *),void *arg) ;
- 参数:
- 1.thread :获取线程标识符(地址),本质 上就是线程独有空间的首地址
- 2.attr :线程的属性信息, 一 般填写NULL,采用默认的线程属性
- 属性信息当中比较关心的:
- 调用栈的大小
- 分离属性
- 调度策略:先来先服务,
- 分时策略,时间片轮转
- 调度优先级等等
- 3.start_ routine :函数指针, 线程执行的入口函数 (线程执行起来的时候,从该函数开始运行, 切记: 不是从main函数开始运行),当前线程执行时从这个函数开始执行。
- 4.arg:给线程入口函数传递参数;也就是说start_routine的参数就是它给传递的。
- 返回值:
- 成功==0
- 失败< 0
代码演示:
注意这时候的makefile要加上-lpthread
我们运行一下:
可以看到主线程和工作线程都在进行着
我们这时候可以再来看看他的调用堆栈
我们来看下有意思的部分:
看下下面的代码:
我们这个代码想法很美好,按”道理“就是打印 thread0 thread1 thread2 thread3
我们来运行一下:
我们运行了之后发现结果都是4,这是为什么呢?
这时候,我们再来加上一个sleep,有意思的就来了
我们来分析一波:
我们每次打印的都是同一个变量的四字节空间,这肯定就会和我们的预期发生冲突的
这样作有风险吗?
有,表现是,多个线程现在访问的i的空间是非法访问,因为i是临时变量,出了for循环的作用域之后就被销毁了
那么有解决办法吗?
肯定是有的
1.各个线程直接打印自己的线程标识符 使用 p_threadself函数
2.传递不同的堆上的空间
我们这时候再来运行一下:
可以看到,这时候打印出来就很完美了
这个方法有个结论:1、线程入口函数传递参数的时候,传递堆区空间。 2、释放堆区空间的时候,让线程自己释放
2.2线程终止
1.void pthread_ exit(void *retval) ;
- 1.1参数:
- retval :线程退出时, 传递给等待线程的退出信息。
- 1.2作用:
- 谁调用谁退出,主线程调用主线程终止,工作线程调用工作线程终止
2.int pthread_cancel(pthread_ t thread) ;
- 2.1作用:
- 退出某个线程
- 2.2参数:
- thread:被终止的线程的标识符
3.pthread_t pthread_self(void);(返回调用此函数的线程id)
2.3线程等待
- 1.线程被创建出来的默认属性是joinable属性,退出的时候,依赖其他线程来回收资源(主要是退出线程使用到的共享区当中的空间)
- 2.接口:
- int pthread_ join(pthread_ t thread, void **retval);
- 参数:
- thread:线程的标识符(就是要等待的线程的线程标识符)
- retval:退出线程的退出信息
- 第一种:线程入口函数代码执行完毕, 线程退出的,就是 入口函数的返回值
- 第二种: pthread_ exit退出的,就是pthread_exit的参数
- 第三种: pthread_ cancel退出的,就是 一个宏: PTHREAD_ CANCELED
注意:这个函数在调用的时候是阻塞调用的
我们可以来验证一下这个阻塞属性
-
2.4线程分离
- 设置线程的分离属性,一旦线程设置 了分离属性,则线程退出的时候,不需要任何人回收资源。 操作系统可以进行回收。
- 接口:
- int pthread_ detach (pthread_t thread) ;
- thread:设置线程分离的线程的标识符