线程

什么是线程

LWP:light weight process 轻量级的进程 ,本质仍是进程(在Linux环境下)
进程 : 独立地址空间 拥有PCB
线程: 有独立的PCB ,但没有独立的地址空间 (共享)
区别: 在于是否共享地址空间 独居(进程) 合租(线程)
Linux下: 线程:最小的执行单位
进程:最小分配资源单位,可看成只有一个线程的进程

  • 从资源分配的角度来看,进程是操作系统进行资源分配的基本单位。
  • 从资源调度的角度来看,线程是资源调度的最小单位,是程序执行的最小单位

线程可看作寄存器和栈的集合

协程(coroutine)是一种程序运行的方式,即在单线程里多个函数并发地执行.
ps -Lf 进程id —-》 线程号 LWP —》cpu执行的最小单位
“ps aux” 可以查看系统中所有的进程;
“ps -le” 可以查看系统中所有的进程,而且还能看到进程的父进程的 PID 和进程优先级;
“ps -l” 只能看到当前 Shell 产生的进程;

LWP 与 进程的三级页表是相同的 三级映射
三级映射就是从PCB中的页表信息 到 MMU映射到物理磁盘内存的过程
三级映射 :进程PCB->页目录(可看成数组,首地址位于PCB中)-》页表 -》物理页表 -》内存单元

一个进程中的线程们的页表地址都相同 MMU映射到的磁盘内存一致。

线程共享资源

  1. 文件描述符表
  2. 每种信号的处理方式 /////一般线程与信号不同时用
  3. 当前工作目录
  4. 用户ID和组ID
  5. 内存地址空间(.text/.data/.bss/heap/共享库)

    线程非共享资源

  6. 线程id

  7. 处理器现场和栈指针 (内核栈)
  8. 独立的栈空间(用户空间栈)
  9. error变量
  10. 信号屏蔽字//可以不用的线程对信号不同的屏蔽及处理
  11. 调度优先级

    线程优缺点

    优点:

  12. 提高程序并发性

  13. 开销小
  14. 数据通信 共享数据方便

缺点:

  1. 库函数 不稳定
  2. 调试 编写困难 不支持gdb
  3. 对信号支持不好

线程控制原语

pthread_self函数

获取线程ID 其作用对应进程中getpid()函数
pthread_t pthread_self(void)
线程ID prthread_t 在linux下为无符号长 2 整型
线程ID是进程内部 识别标志 (两个进程间 线程ID允许相同)
返回值:成功:线程ID

pthread_create函数

创建一个新线程 其作用 相当于进程中fork函数

  1. int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_rountn)(void *), void *arg);
  2. 创建子线程。
  3. 1:传出参数,表新创建的子线程 id
  4. 2:线程属性。传 NULL 表使用默认属性。
  5. 3:子线程回调函数。创建成功,ptherad_create 函数返回时,该函数会被自动调用。 4:参 3 的参数。没有的话,传 NULL
  6. 返回值:成功:0
  7. 失败:errno


案例

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <unistd.h>
  5. #include <error.h>
  6. #include <pthread.h>
  7. void sys_err(const char *str)
  8. {
  9. perror(str);
  10. exit(1);
  11. }
  12. void *tfn(void *arg)
  13. {
  14. return NULL; //子线程
  15. }
  16. int mian(int argc,char *argv[])
  17. {
  18. pthread_t tid;
  19. //主线程
  20. printf("main = %d , tid = %lu",getpid(),pthread_self());
  21. int ret = pthread_create(&tid,NULL,tfn,NULL);
  22. if(ret != 0)
  23. {
  24. perror("pthread_create erroe");
  25. }
  26. return 0;
  27. }

image.png

线程全局变量共享

独享 栈空间(内核栈 用户栈)
共享 ./text/data./rodataa./bsss heap —->共享【全局变量】

void pthread_exit(void * retval)

  • 入口函数的return返回,线程就退出了
  • 线程调用pthread_exit(NULL),谁调用谁退出

include void pthread_exit(void *retval);
参数:retval是返回信息,”临终遗言“,可以给可以不给
该变量不能使用临时变量。
可使用:全局变量、堆上开辟的空间、字符串常量。

pthread_cancel

  • 其它线程调用了pthread_cancel函数取消了该线程

int pthread_cancel(pthread_t thread);
thread:线程标识符
调用该函数的执行流可以取消其它线程,但是需要知道其它线程的线程标识符,也可以执行流自己取消自己,传入自己的线程标识符。
如果线程组中的任何一个线程调用了exit函数, 或者主线程在main函数中执行了return语句, 那么整个线程组内的所有线程都会终止。
需要使用pthread_join进行回收

pthread_join

int pthread_join(pthread_t thread, void **retval); 阻塞 回收线程。
thread: 待回收的线程 id
retval:传出参数。 回收的那个线程的退出值。 线程异常借助,值为 -1。
返回值:成功:0 失败:error
调用该函数,该执行流在等待线程退出的时候,该执行流是阻塞在pthread_joind当中的。

代码中如果没有pthread_join主线程会很快结束从而使整个进程结束,从而使创建的线程没有机会开始执行就结束了。加入pthread_join后,主线程会一直等待直到等待的线程结束自己才结束,使创建的线程有机会执行。
所有线程都有一个线程号,也就是Thread ID。其类型为pthread_t。通过调用pthread_self()函数可以获得自身的线程号。
如果你的主线程,也就是main函数执行的那个线程,在你其他线程退出之前就已经退出,那么带来的bug则不可估量。通过pthread_join函数会让主线程阻塞,直到所有线程都已经退出。

线程等待和进程等待的不同

  1. 第一点不同之处是进程之间的等待只能是父进程等待子进程, 而线程则不然。线程组内的成员是对等的关系, 只要是在一个线程组内, 就可以对另外一个线程执行连接(join) 操作。
  2. 第二点不同之处是进程可以等待任一子进程的退出 , 但是线程的连接操作没有类似的接口, 即不能连接线程组内的任一线程, 必须明确指明要连接的线程的线程ID

pthread_join()错误码:

返回值 说明
ESRCH 传入的线程ID不存在,查无此线程
EINVAL 线程不是一个joinable线程
EINVAL 已有其它线程捷足先登,链接目标线程
EDEADLK 死锁,如自己链接自己

如果不连接已经退出的线程, 会导致资源无法释放。 所谓资源指的又是什么呢?

  1. 已经退出的线程, 其空间没有被释放, 仍然在进程的地址空间之内。
  2. 新创建的线程, 没有复用刚才退出的线程的地址空间。

如果不执行链接操作, 线程的资源就不能被释放, 也不能被复用, 这就造成了资源的泄漏。
纵然调用了pthread_join, 也并没有立即调用munmap来释放掉退出线程的栈, 它们是被后建的线程复用了.释放线程资源的时候, 若进程可能再次创建线程, 而频繁地munmap和mmap会影响性能, 所以将该栈缓存起来, 放到一个链表之中, 如果有新的创建线程的请求, 会首先在栈缓存链表中寻找空间合适的栈, 有的话,直接将该栈分配给新创建的线程。

pthread_detach()

int pthread_detach(pthread_t thread);
设置线程分离
thread: 待分离的线程 id
返回值:成功:0
失败:errno

是因为线程分离后,系统会自动回收资源,用 pthread_join 去回收已经被系统
回收的线程,那个线程号就是无效参数

ret = pthread_detach(tid); // 设置线程分离` 线程终止,会自动清理 pcb,无需回收
if (ret != 0) {
perror(“pthread_detach error”);
}
分离之后 在进程结束时候会自动回收pcb
不需要再去pthread_join

线程属性初始化

注意 应先初始化线程属性,再pthread_create创建线程
初始化线程属性
int pthread_attr_init(pthread_attr_t attr)
成功 0 失败 错误号
销毁线程属性所占用的资源
int pthread_attr_destroy(pthread_attr_t
attr)
成功 0 失败 错误号

线程的分离状态:
线程的分离状态决定一个线程以什么样的方式来终止自己
非分离状态 线程的默认属性是非分离状态 这种情况下 原有的线程等待创建的线程结束 ,只有pthread_join函数返回时 创建的线程才算终止 才能释放自己占用的系统资源、
分离状态 分离线程没有被其他的线程所等待 自己运行结束了 线程也就终止了 马上释放系统资源 。应该根据自己的需要 来选择适当的分离状态
设置线程属性
int pthread_attr_setdetach(pthread_attr_t attr,int detachstate)
获取线程属性
int pthread_attr_getdetach(pthread_attr_t
attr,int *detachstate)

线程同步

线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。 在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。 但是多个线 程同 时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。 同步就是协同步调,按预定的先后次序进行运行。 如:你说完,我再说。 其实不是,“同”字应是指协同、协助、互相配合。
image.png

数据混乱的原因:
1. 资源共享(独享资源则不会)
2. 调度随机(意味着数据访问会出现竞争)
3. 线程间缺乏必要同步机制

互斥量mutex

linux中提供一把互斥锁mutex (也称为互斥量)
每个线程在对资源操作都会尝试先加锁,成功加锁才能操作,操作结束解锁
资源还是共享 线程之间还是竞争的
但通过“锁”就将资源的访问变成互斥操作,而后与时间有关的错误也不会再产生了

同一时刻 只能有一个线程持有该锁。
当A线程对某个全局变量加锁访问,B再访问前尝试加锁,拿不到锁,B阻塞,C线程不去加锁 而是直接去 访问该全局变量 依然能够访问 但会出现数据混乱
锁本身不具备强制性

所以 互斥锁实质上是操作系统提供的一把 建议锁 又称协同锁 建议程序中有多线程访问共享资源
主要应用函数:
pthread_mutex_init
函数
pthread_mutex_destory
函数
pthread_mutex_lock
函数
pthread_mutex_trylock
函数
pthread_mutex_unlock
函数
以上 5 个函数的返回值都是:成功返回 0,失败返回错误号
pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整
数看待
pthread_mutex_t mutex;变量 mutex 只有两种取值:0,1

主要应用函数:
pthread_mutex_init
函数
pthread_mutex_destory
函数
pthread_mutex_lock
函数
pthread_mutex_trylock
函数
pthread_mutex_unlock
函数
以上 5 个函数的返回值都是:成功返回 0,失败返回错误号
pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整
数看待
pthread_mutex_t mutex;变量 mutex 只有两种取值:0,1
image.png

使用 mutex(互斥量、互斥锁)一般步骤:
pthread_mutex_t 类型。
1. pthread_mutex_t lock; 创建锁
2 pthread_mutex_init; 初始化
13. pthread_mutex_lock;加锁 1— —> 0
4. 访问共享数据(stdout)
5. pthrad_mutext_unlock();解锁 0++ —> 1
6. pthead_mutex_destroy;销毁锁

int pthread_mutex_init(pthread_mutex_t restrict mutex,
const pthread_mutexattr_t
restrict attr)
这里的 restrict 关键字,表示指针指向的内容只能通过这个指针进行修改
restrict 关键字:
用来限定指针变量。被该关键字限定的指针变量所指向的内存操作,必须由本指针完成。


初始化互斥量
pthread_mutex_t mutex;
1. pthread_mutex_init(&mutex, NULL);
动态初始化。
2. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
静态初始化

读写锁操作函数

读写锁特性:

  1. 读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞
  2. 读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功,如果线程以写模式加锁会阻塞。
  3. 读写锁时“读模式加锁”时,既有试图以写模式加锁的线程,也有试图以读模式加锁的线程,那么读写锁会阻塞随后的读模式锁请求,有限满足写模式锁,读锁、写锁并行阻塞,写锁优先级高。

读写锁也叫共享 独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占 读共享。
读写锁非常适合于数据结构读的次数远大于写的情况
写锁:
锁只有一把。以读方式给数据加锁——读锁。以写方式给数据加锁——写锁。
读共享,写独占。
多个线程同时进行读锁 可以同时得到读锁
已有读锁 写锁阻塞
写锁优先级高。
相较于互斥量而言,当读线程多的时候,提高访问效率
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
pthread_rwlock_rdlock(&rwlock);
try
pthread_rwlock_wrlock(&rwlock);
try
pthread_rwlock_unlock(&rwlock);
pthread_rwlock_destroy(&rwlock);
以上函数都是成功返回 0,失败返回错误号。
pthread_rwlock_t 类型
用于定义一个读写锁变量
pthread_rwlock_t rwlock

两种死锁

死锁时使用锁不恰当导致的现象

  1. 对一个锁反复lock
  2. 两个线程各自持有一把锁 请求另一把。

image.png然后互等

静态初始化条件变量和互斥量

条件变量
条件变量是线程的另外一种同步机制,这些同步对象为线程提供了会合的场所,理解起来就是两个(或者多个)线程需要碰头(或者说进行交互-一个线程给另外的一个或者多个线程发送消息),我们指定在条件变量这个地方发生,一个线程用于修改这个变量使其满足其它线程继续往下执行的条件,其它线程则接收条件已经发生改变的信号。
条件变量同锁一起使用使得线程可以以一种无竞争的方式等待任意条件的发生。所谓无竞争就是,条件改变这个信号会发送到所有等待这个信号的线程。而不是说一个线程接受到这个消息而其它线程就接收不到了。
pthread_cond_t cond;
初始化条件变量:

  1. pthread_cond_init(&cond, NULL); 动态初始化。
  2. pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 静态初始化

条件变量和相关函数 wait

阻塞等待条件:
pthread_cond_wait(&cond, &mutex);
作用:
1) 阻塞等待条件变量cond满足
2) 解锁已经加锁成功的信号量 (相当于 pthread_mutex_unlock(&mutex)),12 两步为
一个原子操作
3) 当条件满足,函数返回时,解除阻塞并重新申请获取互斥锁。重新加锁信号量 (相
当于, pthread_mutex_lock(&mutex);

image.png

提供了一个pcb等待队列以及使线程阻塞和唤醒线程的两个接口

1.定义条件变量
pthread_cond_t cond;
2.初始化条件变量
int pthread_cons_init(pthread_cond_t cond,pthread_condattr_t attr)//属性通常置NULL
3.使线程阻塞
int pthread_cond_wait(pthread_cond_t cond,pthread_mutex_t mutex);
这个接口涉及三个操作:解锁,阻塞,被唤醒后加锁

并且解锁和陷入阻塞是原子操作,一步完成不会被打断

int pthread_cond_timedwait()限制时长的阻塞等待
pthread_cond_wait使线程阻塞.修改线程状态,将线程pcb加入到cond的pcb队列里

4.唤醒阻塞的线程
int pthread_cond_signal(pthread_cond_t cond);将cond的pcb队列中的线程至少唤醒一个
int pthread_cond_broadcast(pthread_cond_t
cond);将cond的pcb队列中的线程全部唤醒
5.释放销毁
int pthread_cond_destroy(pthread_cond_t *cond);
信号只提供了使线程阻塞和唤醒线程接口,至于什么时候阻塞,全部由用户控制

注意:

1.是否满足条件的判断应该使用循环操作

2.多种角色线程等待应该分开来等待,分开唤醒防止唤醒角色错误,需要定义多个多个条件变量

  1. #include<stdio.h>
  2. #include<pthread.h>
  3. #include<stdlib.h>
  4. #include<unistd.h>
  5. int pots=0;//盆 1表示有饭,0表示没饭
  6. pthread_mutex_t mutex;
  7. pthread_cond_t cond_stu;
  8. pthread_cond_t cond_aut;
  9. void *student(void *arg)
  10. {
  11. while(1)
  12. {
  13. pthread_mutex_lock(&mutex);
  14. while(pots==0)
  15. {
  16. pthread_cond_wait(&cond_stu,&mutex);
  17. }
  18. printf("nice!真好吃~~~!再来一碗-%d\n",pots);
  19. pots=0;
  20. pthread_mutex_unlock(&mutex);//解锁
  21. pthread_cond_signal(&cond_aut);//唤醒阿姨
  22. }
  23. return NULL;
  24. }
  25. void *aunt(void *arg)
  26. {
  27. while(1)
  28. {
  29. //加锁
  30. pthread_mutex_lock(&mutex);
  31. while(pots==1)
  32. {
  33. //阻塞
  34. pthread_cond_wait(&cond_aut,&mutex);
  35. }
  36. printf("ljx你的饭好了~~~!\n");
  37. pots=1;
  38. //解锁
  39. pthread_mutex_unlock(&mutex);
  40. pthread_cond_signal(&cond_stu);
  41. //唤醒学生
  42. }
  43. return NULL;
  44. }
  45. int main()
  46. {
  47. pthread_t stu_id,aunt_id;
  48. int ret;
  49. pthread_mutex_init(&mutex,NULL);
  50. pthread_cond_init(&cond_aut,NULL);
  51. pthread_cond_init(&cond_stu,NULL);
  52. for(int i=0;i<4;++i)
  53. {
  54. ret=pthread_create(&stu_id,NULL,student,NULL);
  55. if(ret!=0)
  56. {
  57. printf("create thread error\n");
  58. return -1;
  59. }
  60. ret=pthread_create(&aunt_id,NULL,aunt,NULL);
  61. if(ret!=0)
  62. {
  63. printf("create thread error\n");
  64. return -1;
  65. }
  66. }
  67. pthread_join(stu_id,NULL);
  68. pthread_join(aunt_id,NULL);
  69. pthread_mutex_destroy(&mutex);
  70. pthread_cond_destroy(&cond_aut);
  71. pthread_cond_destroy(&cond_stu);
  72. return 0;
  73. }

生产者消费者模型

image.pngpthread_cond_signal(): 唤醒阻塞在条件变量上的 (至少)一个线程。
pthread_cond_broadcast(): 唤醒阻塞在条件变量上的 所有线程

信号量

信号量:
应用于线程、进程间同步。
相当于 初始化值为 N 的互斥量。 N 值,表示可以同时访问共享数据区的线程数。
函数:
sem_t sem; 定义类型。
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem: 信号量
pshared:
0: 用于线程间同步
1: 用于进程间同步
value:N 值。(指定同时访问的线程数)

sem_destroy();
sem_wait();
一次调用,做一次— 操作,
当信号量的值为 0 时,再次 — 就会阻塞。
(对比 pthread_mutex_lock)

sem_post();
一次调用,做一次++ 操作. 当信号量的值为 N 时, 再次 ++ 就会阻塞。
(对比 pthread_mutex_unlock)

image.png
####################