Linux操作系统笔记整理之多线程总结(概念,对比进程,线程优缺点,线程安全,同步与互斥)

    xiaoxiao2023-11-21  174

    多线程

    多线程概念

    想了解线程,前提是要了解进程 进程概念: 1. 从用户角度,进程就是一个正在执行的程序,就想你正在使用的csdn,属于一个进程 2. 从操作系统角度,对进程的描述就是一个pcb,这个描述是一个结构体(task_struct) task_struct{ //进程标识pid,内存指针,进程状态,上下文数据,程序计数器,记账状态,文件状态信息,优先级 }; 3. 操作系统对进程的管理:先描述再组织,操作系统在硬盘上,进程在内存中,cpu要处理程序, 因此要将程序先加载到内存中。因此进程就是pcb,cpu调度就是pcb 4. 当我们运行一个程序,这个程序被加载到内存中运行,那么这个时候操作系统创建一个进程, 实际就是创建了一个 PCB(task_struct)并且加入到PCB表(双向链表)来进行管理。

    关于线程的更多知识总结,在之前的博客中都有总结过

    如果了解了进程,那么就很容易了解多线程了

    多线程概念: 前提:Linux下没有真正的线程,因为Linux的线程是以进程的pcb模拟的,所以Linux下的线程也叫轻量级进程 简单的说,进程是由至少一个的线程的线程组组成的

    线程是能够运行和cpu调度的最小单位,所以线程就是pcb是进程实际运作的单位是进程中单一顺序的执行流(例如,一个进程中的main函数,是由线程执行起来的)一个进程可以并发执行多个线程因为外部看到的pid实际上是task_struct中的tgid,而tgid是线程组id


    线程间的共享与独有

    一个进程下回有至少一个进程在运行,所以进程间会有它们之间的共享资源和独有资源

    例如学校里的某一个正在上课班级,至少会有一个学生,这些学生共享教室和老师等物品,但是他们会有独有物品 独有资源: 文件描述符表:也称io标识符表,线程可以根据文件描述符表找到对应的文件信号的处理方式:SIG_DFL,SIG_IGN或者自定义的信号处理方式用户id,组id和当前工作目录

    优点:由于资源共享,所以进程间通信变得极为简单

    资源独有 栈优先级:线程优先级,针对轮询运行时的比较上下文数据:寄存器,存储文件中的数据线程标识符信号屏蔽字:每个线程都可以屏蔽想要屏蔽的信息error

    优点:由于资源独有,多线程可以同时运行,但是不会运行时造成调用栈混乱


    多线程的优缺点分析

    优点: 1. 由于部分资源共享,多线程通信更便捷 2. cpu调度成本更低 3. 创建与销毁成本更低

    缺点: 1. 缺乏访问控制,编码复杂度高,exit退出整个进行 2. 稳定性较差,某个线程出了错误就会导致整个进程的退出


    多线程的应用场景 1. 多用于io密集型程序和cpu密集型程序中起到重要作用:分担压力,提高效率 io密集型程序:是大量的io磁盘操作程序 cpu密集型程序:大量的数据运算操作程序
    线程控制: 1.线程创建: int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); 注:使用时需要调用库函数 #include<pthread.h> #include <stdio.h> #include <errno.h> #include <unistd.h> #include <stdlib.h> #include <pthread.h> void *thr_start(void *arg) { //pthread_t pthread_self(void); //返回调用线程的线程id pthread_t tid = pthread_selr(); while(1){ printf("child thread------%d ----- tid:%p\n",arg,tid); } int main(int argc,char *argv[]) { //int pthread_create(pthread_t *thread, const pthread_attr_t *attr, //void *(*start_routine) (void *), void *arg); //thread:用于保存返回的线程id //attr: 线程属性,通常置NULL //start_routine: 线程入口函数 //arg: 给线程传入的参数 //返回值:0 ,失败:errno //例如这样 int ret = pthread_create(&tid,NULL,thr_start,NULL); }
    线程终止:有三种退出方式 1. pthread_exit 2. pthread_cancel 3. return 返回退出

    线程属性:

    为什么需要线程等待? 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。 创建新的线程不会复⽤刚才退出线程的地址空间 线程等待:等待指定线程退出,获取指定线程返回值,回收线程资源 一个线程被创建,默认有一个属性是joinable;处于这个属性的线程必须被等待,因为线程退出后,不会自动回收资源,造成内存泄露; int pthread_join(pthread_t tid,void **retval); tid:线程ID value_ptr:它指向一个指针,后者指向线程的返回值 返回值:成功返回0;失败返回错误码线程分离:线程退出后,不会保存返回值,直接回收资源,将属性从joinable改为detach int pthread_detach(pthread_t tid); tid:线程ID
    线程安全:因为多个执行流之间对数据的竞争操作有可能会造成数据二义性的问题 解决线程安全问题:

    同步与互斥 同步: 对临界资源的的公平性保证,使得每一个线程访问的时序可控 互斥: 对临界资源的唯一性保证,使得同一时间只有唯一访问权

    实现同步: pthread_cond_wait

    实现互斥:使用互斥锁和互斥量

    互斥锁: 实际就是一个计数器,但是这个计数器的值只有0/1 互斥锁也是一个临界资源,如何保证安全,意味着互斥锁的操作必须是原子操作 pthread_mutex_t mutex

    互斥锁操作步骤: a.定义互斥锁变量 pthread_mutex_t b.初始化互斥锁变量 pthread_mutex_init c.头文件:#include <pthread.h> 互斥锁变量: int pthread_mutex_destroy(pthread_mutex_t *mutex);互斥锁初始化: int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr); mutex:要初始化的互斥锁 attr:互斥锁属性,通常置NULL临界操作加锁 pthread_mutex_lock(阻塞加锁) pthread_mutex_unlock(解锁) pthread_mutex_trylock(非阻塞加锁) pthread_mutex_timelock(有时间限制的加锁) #include <pthread.h> //加锁 int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_trylock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); int pthread_mutex_timelock(pthread_mutex_t *mutex); printf("加锁之后要在所有可能return退出的地方解锁"); // d.临界操作完毕解锁 pthread_mutex_unlock pthread_mutex_unlock(pthread_mutex_t *mutex); // e.销毁互斥锁 pthread_mutex_destory pthread_mutex_destory(pthread_mutex_t *mutex);

    死锁:

    产生的四个必要条件: a.互斥条件 b.不可剥夺条件(一个线程加的锁其他线程不可以释放) c.请求与保持条件(线程1已经获取锁1,但并没有释放锁1,保持锁1,就去请求锁2,) d.环路等待条件(在c的条件下,有另一个线程已经获取锁2,没有释放锁2,就去请求锁1,这时,线程1与线程2互相等待锁的释放,便会形成环路) 预防死锁:破坏其中的一个条件,预防c和d 注意加锁解锁的顺序必须保持一致 避免死锁:银行家算法,死锁检测算法
    最新回复(0)