什么是线程:线程是进程中的一条执行流,linux下线程是以进程的PCB模拟的,linux下的线程就是轻量级进程,因此linux下的线程是CPU调度的基本单位(线程是实际上处理事件的)。linux下的进程是线程组,进程id==线程组id,所以进程是资源分配的基本单位,线程是进程的一个执行单位,一个进程可以有一个或多个线程,它们共用该进程的所有资源。线程没有独立的内存空间。 linux下的线程共享虚拟地址空间(代码段和数据段)。
线程是程序中完成一个独立任务的、一个可调度的实体。根据运行环境和调度者可以分为内核线程和用户线程,内核线程运行在内核空间,由内核调度;用户线程运行在用户空间,由线程库来调度。
进程之间相互独立,是系统分配资源的最小单位;同一个进程中的线程共享资源。
总结下来就是:
(1)线程是进程的一部分
(2)CPU调度的是线程
(3)系统为进程分配资源,不对线程分配资源
线程和进程:线程是cpu的调度基本单位,进程是资源分配基本单位。
线程优点:线程的创建/销毁成本更低----线程的调度切换成本也会更低。
共享虚拟地址空间,所以线程间的通信更加方便,线程的执行力度更加细致。
能充分利用多处理器提高可并行数量
线程缺点:缺乏访问控制---线程安全需要考虑更多,有些系统调用和程序异常是针对整个进程产生影响。
多个线程对临界资源(公共资源)进行操作会造成数据混乱,健壮性降低。
而进程:因为独立性,所以健壮性比较强,但是通信麻烦,资源成本高 。
线程共享: 文件状态表 用户id组id 虚拟地址空间 信号处理方式
线程独有:(相对独有---因为独有的数据还是在虚拟地址空间中) 栈区、上下文数据、线程id、errno、信号屏蔽字。
线程控制
1.创建线程
创建一个线程后,Do具体去做其对应的事情,tid的地址中存放线程的ID,ret用来接收返回值。
2.线程终止
想要终止一个线程有以下几种方式:
- 在线程中用return返回,就结束了该线程,在主进程中return返回后,是退出了整个进程。
- 在线程中调用pthread_exit( ),可以结束当前线程。
- 一个线程可以调用pthread_cancel来结束同一个进程中的线程。
3.线程等待
线程等待的作用是已经退出的线程,其空间没有被释放,仍然在进程的地址空间内,创建新的线程不会复用之前的线程地址。所以线程退出后,需要对其进行pthread_join( )等待,否则无法释放资源,造成资源泄露。
4.线程分离
如果不关心线程的返回值,就不需要使用pthread_join了,可以使用pthread_detach,当线程退出后,系统自动释放其资源。在一个线程中使用方法:
线程安全
当多个线程访问共享资源时,如果不考虑线程的访问方式和执行顺序,那么该资源是不安全的。由于线程共享进程虚拟地址空间,使得线程之间的通信变得十分方便,但是有可能多个线程会对同一数据进行操作,而该数据的安全访问就变得十分重要。因为多个线程对临界资源的操作会导致逻辑的混乱/数据二义性,因此就有了线程安全。保证数据的安全访问机制----同步/互斥。
三种用于线程同步的机制:(有demo)
1.POSIX信号量:POSIX的无名信号量用于解决线程间的同步问题。以下是POSIX信号量的相关操作:
sem_init:用来创建一个POSIX信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
创建一个匿名的信号量,pshared值为0时,代表当前信号量是进程内的局部信号量(线程之间共享),否则就代表进程之间(多个进程)共享。
value参数用于指定信号量的初始值。
sem_destroy:用来销毁一个信号量,释放其占用的内核资源
int sem_destroy(sem_t *sem);
切忌:不可释放一个正在被其他线程等待的信号量
sem_wait:以原子操作来对信号量减一
int sem_wait(sem_t *sem);
若信号量的值小于等于0,表示没有资源可以用,此时sem_wait被阻塞,直到信号量大于零
sem_post:以原子操作对信号量加一
int sem_post(sem_t *sem);
当信号量的值大于1时,阻塞等待的线程被唤醒
以上函数成功返回0,失败返回-1
与进程间通信中的System V版本的信号量不太一样,posix信号量指的是单个计数信号量,而IPC中的System V,主要是计数信号量集
2.互斥量(互斥锁):用于保护关键代码段,以确保其独有的访问权限。当进入关键代码段时,我们要对其加锁;离开时,需要解锁,以唤醒其他等待该互斥锁的线程。例如,这是一个最开始的售票系统,当在没有加锁的时候四个线程可以并发的对ticket进行操作,出现问题,因为多线程可以并发的访问ticket,所以会出现ticket变为负数。解决方法就是加上锁,这样就相当于四个线程对于ticket的访问是同步(按访问顺序进行操作)的。
创建锁:int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
//一般将第二个参数设为NULL
销毁锁:int pthread_mutex_destroy(pthread_mutex_t *mutex);
加锁:int pthread_mutex_lock(pthread_mutex_t *mutex);
解锁:int pthread_mutex_unlock(pthread_mutex_t *mutex);
其中第一个参数都是pthread_mutex_t类型的。
3.条件变量:互斥锁是用来对共享数据访问的同步,那么条件变量就是用于在线程之间同步共享数据的值。条件变量提供了一种线程之间的通知机制,当某个共享数据满足一定条件时,唤醒正在等待这个共享数据的线程。着重了解一下条件变量的应用:消费者--生产者模型,生产或消费完都需要唤醒正在等待的一方做事情了。
初始化条件变量:pthread_cond_init
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
attr:用来设置条件变量的属性,设置为NULL,是默认属性
销毁条件变量:pthread_cond_destroy
int pthread_cond_destroy(pthread_cond_t *cond);
销毁条件变量,释放占用的内核资源
注意销毁一个正在被等待的条件变量是不被允许的,失败并返回EBUSY
唤醒等待的线程:pthread_cond_broadcast
int pthread_cond_broadcast(pthread_cond_t *cond);
以广播的方式唤醒所有等待目标条件变量的线程
int pthread_cond_signal(pthread_cond_t *cond);
只唤醒一个等待目标条件变量的线程,取决于优先级和调度策略
等待目标条件变量:pthread_cond_wait
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
mutex:是用于保护条件变量的互斥锁,以确保该操作的原子性。
上面的函数第一个参数都指的是要操作的条件变量,类型是pthread_cond_t的结构体
成功返回0,失败返回错误码
一:线程互斥:保证数据同一时间的唯一访问性。用到了互斥锁:某个线程想要访问某个共享资源时,对其加锁,此时只有该线程可以对其访问,其他线程不能更改锁的状态;当操作完成后,解锁供其他线程使用。注意成对出现,别造成死锁。
死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个执行流执行
- 不可剥夺条件:对于已获取的资源,未使用完之前,别的执行流不能强行获取
- 请求与保持:拿到一个资源,保持着不释放,还去请求别的资源
- 环路等待:若干条执行流形成首尾成环的循环等待资源的关系,后面的一直等前面的。
解决死锁(预防)注意:
- 打破死锁的四个必要条件
- 注意加锁、解锁的顺序一致
- 别忘记释放锁
- 资源一次性分配:一次性分配所有资源,这样就不会再请求了(打破请求条件)
二: 线程同步:保证访问数据的时序性。用到了条件变量:
可重入函数:如果一个函数能被多个线程同时调用而不发生竞态条件,是线程安全的,则称该函数为可重入函数。