太多的离别总是发生在六月,
太多的情绪,总是难以在这个带着伤感的季节里诉说~
目录:
1.线程概念
2.Linux线程VS进程
3.线程的实现
3.1线程控制
- 线程创建
- 线程终止
- 线程等待
- 线程分离
- 线程安全
4.线程间互斥的实现
5.线程间同步的实现
>>线程概念
线程是什么?
1.线程是一个执行流(运行代码,处理数据);
2.先看进程:传统操作系统中使用pcb来描述一个程序的运行--pcb就是进程;
3.Linux下通过pcb来模拟实现线程,pcb是一个轻量级进程;(这个轻量级进程因为共用大部分进程资源,相较于传统进程更加轻量化)同一个进程组中的pcb共用同一个虚拟地址空间,共享进程中大部分资源;
线程之间的独有与共享:
独有:栈,寄存器,信号屏蔽字,errno,线程ID,
共享:虚拟地址空间(数据段/代码段),文件描述符,信号处理方式,当前工作路径,用户id/组id,
>>线程VS进程
进程是资源分配的基本单位---因为程序运行时资源是分配给整个线程组(进程)的。
线程是CPU调度的基本单位---因为Linuxpcb是线程。
多任务的执行:既可以使用多线程也可以使用多进程,哪一个更好些呢?(分析优缺点/视场景而定)
多线程任务处理的优缺点分析:
多线程共用进程大部分资源;
1.线程间通信处理进程间的方式之外还有更简单的就是全局数据/传参-->线程间通信更加方便
2.共享虚拟地址空间:创建、销毁成本更低
3.线程间的调度相较于进程要更低
线程之间缺乏访问控制,有些系统调用、异常都会对整个进程造成影响;稳定性相较于进程更低。
场景:shell这种对主程序稳定安全性要求更高的程序就需要使用多线程,让子程序来处理任务。
>>线程控制
线程控制的接口都是库函数实现的:创建一个用户态线程让用户控制【使用库函数实现创建的线程称之为用户态线程,这个用户态线程在内核中使用一个轻量级进程实现调度】,但是程序的调度处理都只通过轻量级进程pcb实现。
Linux下的线程:用户态线程+轻量级进程
>>线程创建
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine)(void*), void *arg);
参数:
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:0 成功 !0 创建失败(errno)
线程id:
tid-线程地址空间首地址-方便用户操作线程
pcb->pid 轻量级进程id--LWP
pcb->tgid 线程组(进程)id,默认等于首线程的id
>>线程终止
return 不能在main函数中return(退出的进程--导致所有线程退出),
void pthread_exit(void *retval) 退出线程自身,谁调谁退出,
retval:线程的退出返回值
int phread_cancel(pthread_t thread) 取消其他线程,让其它线程退出
thread:要取消的线程id
线程退出后,默认不会自动释放资源,保存自己的退出结果在线程独有的地址空间中,因此会造成资源泄漏。主线程退出,其他线程还可以正常运行。
>>线程等待
等待指定线程退出,获取这个线程的退出返回值,并且回收这个线程的资源;
一个线程有一个默认属性:joinable;处于joinable属性的线程退出后为了保存返回值,因此不会自动释放资源;如果不进行等待则会造成资源泄露。一个线程处于joinable状态的时候必须被等待!
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:0 成功;失败返回错误码
int pthread_join(pthread_t thread,void **retval);功能:阻塞等待指定线程退出,通过retval获取返回值。
void test(int **a){*a=(void*)1;} test(*b=10)
>>线程分离
线程分离就是将线程joinable属性修改为detach属性。可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离。
pthread_detach(pthread_self());
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
void *thread_run( void * arg ){
pthread_detach(pthread_self());
printf("%s\n", (char*)arg);
return NULL;
}
int main( void ){
pthread_t tid;
if ( pthread_create(&tid, NULL, thread_run, "thread1 run...") != 0 ) {
printf("create thread error\n");
return 1;
}
int ret = 0;
//要让线程先分离,再等待
sleep(1);
if ( pthread_join(tid, NULL ) == 0 ) {
printf("pthread wait success\n");
ret = 0;
}
else
{
printf("pthread wait failed\n");
ret = 1;
}
return ret;
}
线程若处于detach属性,则线程退休后将自动回收资源;并且这个线程不需要被等待,等待是毫无意义的,因为线程退出返回值占用的空间已经被收回了。
pthread_detach(pthread_t tid),
线程分离的使用场景:对线程的返回值不关心,
线程分离可以在任意线程中实现。
>>线程安全
常见不可重入的情况:
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的;
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构;
- 不可重入函数内使用了静态的数据结构。
常见可重入的情况:
- 不使用全局变量或静态变量;
- 不使用malloc或者new开辟出的空间;
- 不调用不可重入函数;
- 不返回静态或全局数据,所有数据都有函数的调用者提供;
- 使用本地数据,或者通过全局数据的本地拷贝来保护全局数据。
多个线程同时对临界资源进行访问而不会造成数据二义。
如何实现线程安全:同步+互斥
同步:对临界资源访问的时序合理性,
互斥:对临界资源同一时间访问的唯一性。
- 可重入函数是线程安全函数的一种。
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果将对面临资源的访问加上锁,则这个函数是线程安全的,但如果这个可重入函数加锁未释放则会产生死锁,因此是不可重入的。
>>线程间互斥的实现
互斥锁
1.定义互斥锁变量 pthread_mutex_t
2.对互斥锁变量进行初始化 pthread_mutex_init(&mutex,&attr)
3对临界资源操作之前先加锁 pthread_mutex_lock(&mutex)
若可以加锁则直接修改计数,函数返回;负责挂起等待
pthread_mutex_trylock /pthread_mutex_timedlock
4.对临界资源操作完毕后进行解锁
pthread_mutex_unlock(&mutex);
5.销毁互斥锁
pthread_mutex_destroy(&mutex);
互斥量加锁和解锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
死锁:多个线程对资源进行竞争访问,但是因为推进顺序不当,导致相互等待,使程序无法往下进行
死锁产生的四个必要条件:
1.互斥条件 一个锁只有一个线程可以获取
2.不可剥夺条件 我加的锁别人不能解
3.请求与保持条件 拿着A锁,去请求B锁,但是获取不到B锁,也不释放A锁
4.环路等待条件 我拿着A锁请求B锁,对方拿着B锁请求A锁
死锁预防:破坏四个必要条件 【银行家算法,下一章详解】
>>线程间同步的实现
等待+唤醒:操作条件不满足则等待,别人促使条件满足后唤醒等待
条件变量:条件变量实现同步:线程在对临界资源访问之前,先判断是否能够操作;若可以操作则线程直接操作;否则若不能操作;则条件变量提供等待功能;让pcb等待在队列上;其他线程促使操作条件满足,然后唤醒条件等待队列上的线程【eg.去餐馆吃面的例子以便理解】
~~bye~~
继续下一节总结!