俩小demo
利用多线程完成图片复制,主线程复制前一半,子线程复制后一半;
多线程翻转字符串与输出字符串
仨小demo(互斥锁,条件变量,信号量)
一个线程读文件,另一个线程将读取的内容输出到终端
三个线程打印ABC,每个线程打一个字符,且顺序不变
三个线程打印ABC,每个线程打一个字符,且顺序不变
六十二、线程
62.1 线程的概念
62.1.1 什么是线程
- 线程是一个进程并发执行多种任务的机制。
- 串行,并发,并行。
- 串行:多个任务之间,有先后顺序,有序运行。一个任务执行完毕后,才能执行另一个任务。
- 并发:多个任务之间,在单核CPU上,根据时间片轮询机制运行,同一个时间只能运行一个任务,cpu根据时间片调度任务。
- 并行:多个任务之间,在多核CPU上,同时运行。
- 进程的上下文切换:
- 上下文:运行一个进程所需要的所有内容。
- 上下文切换:访问A进程时候的所有资源,切换到访问B进程的所有资源。这是一个耗时操作。
为了提高系统性能,引入了一个轻量级进程的概念,称之为线程。
- 线程是属于进程的,运行在进程空间内。每一个进程至少有一个线程作为指令执行体。
- 多线程:一个进程中可以运行多个线程,即一个进程中可以同时运行多个任务。CPU根据时间片轮询机制在各个线程,进程间切换。
- 线程是任务运行的最小单位!!
62.1.2 线程是任务运行的最小单位(重点)
- 在同一个进程下的线程,共享其附属进程的所有资源。
- 共享:
- 静态存储区
- 堆区
- 栈区
- 内核空间
- 共一个进程下的线程,若要访问该进程下其他线程的资源,必须要知道该资源的地址。
62.1.3 进程和线程的区别(重点!!!!!)
- 进程是资源分配的最小单位,线程是任务运行的最小单位
- 进程与进程之间的用户空间相互独立,内核空间所有进程共享。若进程之间要做数据通信,需要使用到进程间通信机制。
- 线程运行在进程空间内,附属于同一个进程下的线程共享该进程的所有资源。所有线程之间通信不需要通信机制,但是要注意线程间的同步互斥。
- 多线程的效率比多进程高。
- 多进程的稳定性和资源量比多线程高。
62.2 线程相关的函数
注意:
Compile and link with -pthread.
编译的时候需要手动连接 -pthread库
gcc 1.c -pthread
62.2.1 pthread_create
功能:
创建一个线程;
原型:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
参数:
pthread_t *thread:用于存储创建成功的线程的id号,tid号;
pthread_attr_t *attr:线程的属性,一般填NULL;
可以通过 pthread_attr_init函数进行设置attr属性。可以设置为分离属性。
void *(*start_routine) (void *):函数指针,回调函数。该指针可以指向返回值是void*类型,参数列表是(void*)类型的函数。
该指针指向的函数,是新创建的线程的执行体.(线程要执行什么任务,就写在这个函数里面);
例如可以指向 void* func(void* arg){ };
void *arg:给回调函数传递的参数;
返回值:
成功,返回0;
失败,返回错误码,注意不是errno;
指针类型 *指针名;
int *pa;
void *(*start_routine) (void *):
int (*p)[4];
注意:
1. 我们将从main函数进来的线程称之为主线程,新创建的线程称之为分支线程,或者子线程
2. 主线程和分支线程之间根据时间片轮询机制调度。主线程和分支线程谁先运行不确定。
3. 主线程退出后,会导致进程退出,从而在该进程下的其他分支线程会强制结束;
4. 分支线程退出,不会影响进程运行,所以不会导致其他线程退出。
思考:
- 任务1:思考若定义全局变量,分支线程和主线程能否访问到该全局变量
可以,且访问到的全局变量是同一个全局变量,因为线程共享其附属进程的所有资源。且全局变量的作用域为整个进程。 - 任务2:若定义局部变量,分支线程主线程能否直接访问对方的局部变量
不可以,因为局部变量的作用域被限制在,定义它的那个函数内部。 - 任务3:思考如何将主线程的数据传递给分支线程。
可以将主线程中变量的地址通过pthread_create函数的最后一个参数传递给分支线程。如下(62.2.2)中的第1点 - 任务4:如何将分支线程的值传递给主线程。
可以将主线程中变量的地址通过pthread_create函数的最后一个参数传递给分支线程。在分支线程内做赋值。如下(62.2.2)中的第2点
62.2.2 主线程与分支线程的传参
- 主线程---->分支线程
- 分支线程---->主线程
- 下列代码只能将分支线程中a的值传递给主线程。
- 下列代码可以将分支线程中a的地址传递给主线程
- 下列代码只能将分支线程中a的值传递给主线程。
62.2.3 pthread_exit
功能:
退出线程;
原型:
#include <pthread.h>
void pthread_exit(void *retval);
参数:
void *retval:该参数可以传递线程退出状态值,传递的参数会被pthread_join函数接收;
若不想传递,则参数填NULL;
线程退出后,会有类似僵尸线程的状态,需要使用pthread_join函数回收.
62.2.4 pthread_join
功能:
阻塞函数,阻塞等待指定的分支线程退出。
接收指定分支线程的退出状态值。
回收指定分支线程的资源。
原型:
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
参数:
pthread_t thread:指定要回收哪个分支线程,填对应的tid号;
void **retval:若该参数不为空,则pthread_join函数会将目标线程传递的退出状态值拷贝到该二级指针指向的一级指针空间内。
若不想接收,填NULL;
返回值:
成功,返回0;
失败,返回错误码(error number),注意不是errno,所以不能用perror函数
如:
pthread_join(tid, NULL);
62.2.5 pthread_exit和pthread_join传递状态值
62.2.6 pthread_detach
功能:
分离线程,线程退出后资源自动被回收; 此时调用pthread_join函数操作同一个tid线程,pthread_join函数不阻塞
原型:
#include <pthread.h>
int pthread_detach(pthread_t thread);
参数:
pthread_t thread:指定要分离的线程的tid号;
返回值:
成功,返回0;
失败,返回错误码,注意不是errno;
62.2.7 pthread_cancel
功能:
请求指定线程退出; 请求成功,对方线程不一定退出
原型:
#include <pthread.h>
int pthread_cancel(pthread_t thread);
参数:
pthread_t thread:指定要请求哪个线程退出;
返回值:
成功,返回0;
失败,返回错误编号,即非0,没说更新errno,所以不能用perror打印;
- pthread_cancel会给目标线程打上一个退出标识,cpu切换到目标线程后,运行到退出标识,才会让目标线程退出。
- 但是while for 等循环结构,以及算数运算等等位置,无法打上退出标识。所以当目标线程只有上述结构的时候,无法用pthread_cancel退出线程
- printf ,sleep等等函数结构可以打上退出标识
- 请求成功,不代表目标线程一定会退出。
62.3 线程同步互斥机制
62.3.1 工作原理
- 对于要访问临界资源的线程,在访问之前,先申请互斥锁。
- 如果申请上锁成功,则进入临界区执行临界区代码,直到退出临界区,解开互斥锁。
- 如果申请上锁失败,则说明互斥锁被别的线程占用,则当前线程进入休眠,等待互斥锁解开。
休眠—>主动放弃剩余的时间片
- 互斥锁无法限制访问者的访问顺序,但是可以保证临界区的完整性。
62.3.2 pthread_mutex_init
功能:
创建并从初始化一个互斥锁;
原型:
#include <pthread.h>
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
参数:
pthread_mutex_t *mutex:存储申请后的互斥锁;
pthread_mutexattr_t
*mutexattr:设置锁的属性,一般填NULL,代表用于线程;
该参数可以设置互斥锁是用于线程还是用于进程。
返回值:
永远返回0;
62.3.3 pthread_mutex_lock
功能:
对互斥锁进行上锁,若有其他线程占用互斥锁,该函数会阻塞;
原型:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数:
pthread_mutex_t *mutex;
返回值:
成功,返回0;
失败,返回非0,没有说更新errno,所以不要用perror打印错误。
62.3.4 pthread_mutex_unlock
功能:
解开互斥锁;
原型:
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:
pthread_mutex_t *mutex;
返回值:
成功,返回0;
失败,返回非0,没有说更新errno,所以不要用perror打印错误。
62.3.5 pthread_mutex_destroy
功能:
销毁互斥锁
原型:
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
pthread_mutex_t *mutex;
返回值:
成功,返回0;
失败,返回非0,没有说更新errno,所以不要用perror打印错误。
62.3.6 死锁
- 拥有锁资源的任务没有释放锁。
- 持有互斥锁的线程异常退出,没有释放锁资源。
- 同一线程对一把互斥锁重复上锁。
- 互斥锁交叉嵌套。
- 退出临界区的时候要注意检查互斥锁是否解开。上锁的时候要监测是由有锁资源。
62.4 条件变量
62.4.1 工作原理
- 将不访问共享资源的线程直接休眠,并设置一个唤醒条件,该唤醒条件就称为条件变量。
- 当需要访问的时候,其他线程通过指定条件变量唤醒该线程。
- 条件变量与互斥锁相比,竞争次数大大减少了,
- 条件变量能在保证临界区完整的同时,保证访问者的有序执行。
62.4.2 pthread_cond_init
功能:
创建并初始化条件变量;
会在内核中创建一个cond_wait队列,mutex_wait队列,
原型:
#include <pthread.h>
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
参数:
pthread_cond_t *cond:存储申请后的条件变量;
pthread_condattr_t
*condattr:设置条件变量的属性,一般填NULL,代表用于线程;
该参数可以设置条件变量是用于线程还是用于进程。
返回值:
成功,返回0;
失败,返回非0,没有说更新errno,所以不要用perror打印错误。
62.4.3 pthread_cond_wait
功能:
让线程进入休眠阶段,并设置一个条件变量,等待被唤醒.
当线程睡在cond上的时候,会将线程放入cond_wait队列中
在执行pthread_cond_wait函数之前,当前线程必须拥有锁资源(man手册原话)
原型:
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
参数:
pthread_cond_t *cond:指定唤醒条件,我们一般成该线程睡在该条件变量上;
pthread_mutex_t *mutex:指定要解开的互斥锁,若不解开,该锁会变成死锁.
返回值:
成功,返回0;
失败,返回非0,没有说更新errno,所以不要用perror打印错误。
函数步骤:
- 解开互斥锁,同时让当前线程进入休眠阶段等待被唤醒。(原子操作:原子从操作是指不会被调度机制打断的操作)
- 等待被指定条件变量唤醒
- 当其他线程pthread_cond_signal的时候,线程会从cond_wait队列移动到mutex_wait队列中,且signal计数器+1,线程尝试上锁
- 若上锁成功,则线程完全被唤醒,此时线程会从当前位置继续往后执行,且signal计数器-1;
- 当没有互斥锁资源,且signal计数器为0时,则上锁失败,线程会重新回到cond_wait队列上继续休眠,等待下一次唤醒(signal)。
ps:如果上述不能理解,则简化版:pthread_cond_signal肯定会随机唤醒一个睡在cond上的线程。
62.4.4 pthread_cond_signal
功能:
通过指定的条件变量,唤醒指定的线程;
原型:
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
参数:
pthread_cond_t *cond:指定要唤醒哪个条件变量上的线程;
返回值:
成功,返回0;
失败,返回非0,没有说更新errno,所以不要用perror打印错误。
62.4.5 pthread_cond_destroy
功能:
销毁条件变量;
原型:
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
参数:
pthread_cond_t *cond:指定要销毁哪个条件变量上的线程;
返回值:
成功,返回0;
失败,返回非0,没有说更新errno,所以不要用perror打印错误。
62.5 示例代码
62.5.1 工作原理
- 在访问临界区之前,都先执行申请信号量的操作
- 若信号量的值大于0,则申请信号量成功,此时信号量的值会-1,且能够成功进入临界区,执行临界区代码。
- 若信号量的值等于0,则申请信号量失败,当前线程进入休眠阶段,等待信号量的值大于0.
- 信号量是允许同时多个线程进入临界区的,主要看信号量的初始值。
- 若信号量的初始值为1,则最多允许1个线程进入临界区
- 若信号量的初始值为2,则最多允许2个线程进入临界区
- ……
- 互斥锁又称之为二值信号量,等价于信号量的值只有0,和1两种情况。即此时最多只有一个线程可以进入临界区。
- PV操作:实现进程线程同步互斥的有效方式
- P操作:申请信号量,减操作
- V操作:释放信号量,加操作
62.5.2 sem_init
功能:
创建并初始化信号量;
原型:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem_t *sem:存储创建并初始化后的信号量;
int pshared:共享标识;
0,用于同一个进程下的线程.
非0,用于进程之间;
unsigned int value:指定信号量中的初始值;
返回值:
成功,返回0;
失败,返回-1,更新errno;
62.5.3 sem_wait(P操作)
功能:
申请信号量。
若信号量的值大于0,则申请信号量成功,此时信号量的值会-1,且能够成功进入临界区,执行临界区代码。
若信号量的值等于0,则申请信号量失败,当前线程进入休眠阶段,等待信号量的值大于0.
原型:
#include <semaphore.h>
int sem_wait(sem_t *sem);
参数:
sem_t *sem:init创建的信号量
返回值:
成功返回0,信号量减少1
失败返回-1,更新errno,信号量的值不变
62.5.4 sem_post(V操作)
功能:
释放信号量; 不会阻塞
原型:
#include <semaphore.h>
int sem_post(sem_t *sem);
参数:
sem_t *sem:init创建的信号量
返回值:
成功返回0,信号量加1
失败返回-1,更新errno,信号量值不变
62.5.5 sem_destroy
功能:
销毁信号量
原型:
#include <semaphore.h>
int sem_destroy(sem_t *sem);
参数:
sem_t *sem:需要销毁的信号量
返回值:
成功返回0,信号量加1
失败返回-1,更新errno