目录
2.1创建线程 ----------- pthread_create
2.2获取线程ID ------------ pthread_self
2.3等待线程结束 ----------- pthread_join / 线程的分离 ----------- pthread_detach
2.5线程退出处理函数 ----------------- pthread_cleanup_push/pthread_cleanup_pop
2.6.1初始化属性结构和销毁属性结构 ----------- pthread_attr_init / pthread_attr_destroy
2.6.2属性设置和获取函数 pthread_attr_setdetachstate / pthread_attr_getdetachstate
1.基础知识
进程我前面已经讲解过了(链接),其实进程就是一个的程序文件加载到了内存,然后在内存中创建--运行--消亡的过程
而线程是进程里面的,在C/C++中main函数可以理解为是一个进程的线程(主线程)
多线程就是一个进程可以由多个线程完成代码的并行处理
多线程属于轻量级的代码并行,之所以是轻量级, 是因为线程不需要额外创建过多的内存空间(因为线程可以共享进程的进程空间,无需像多进程那样重新又复制一遍进程空间),而它只需要一个线程栈,可以理解为一个线程其实就是一个函数,这每个函数可以同时处理,所以一般并行处理优先考虑多线程,而不是多进程
多线程之间既相互独立(拥有自己的线程栈),又相互影响(共享进程空间). 而因为这种影响导致了对于同一资源不同的线程分别同时进行读和写造成的数据混乱问题, 因此在多线程之中大量用到互斥和同步技术(下面有详细的讲解)
每个进程都有一个主线程,就是main函数,当主线程结束,进程也会结束, 进程空间释放了,其它线程也一起结束
线程的线程函数是可以一样(代码一样), 即可以是共用一个函数代码, 但是其实在运行的时候它们的线程栈是独立的
Linux中,创建多个线程或者进程后,它们的执行顺序是不确定的 (可以用同步机制, 使得顺序确定 )
没有“父子线程”的说法
如何编译:
Linux本身没有实现线程,在Linux中使用线程需要借助第三方线程库,在Linux使用多线程常使用pthread库
使用pthread库时包含头文件 pthread.h,编译时要链接pthread库(-pthread),pthread库中的所有函数都以pthread_开头
例如
gcc main.c -o main -pthread
2. 相关函数
2.1创建线程 ----------- pthread_create
创建的线程和原线程一起运行,谁先谁后不确定
参数:
thread - 传出参数,传出创建线程的ID
attr - 线程属性(给NULL表示使用默认属性)
start_rountine - 线程函数(线程执行的代码)
arg - 线程函数的参数
成功返回0,失败返回错误码
2.2获取线程ID ------------ pthread_self
2.3等待线程结束 ----------- pthread_join / 线程的分离 ----------- pthread_detach
参数:
thread - 要等待的线程ID
retval - (类型为void **), 是传出参数,传出结束线程的返回值
成功返回0,失败返回-1
注⚠️:线程结束后应该由主线程回收资源,如果不回收,会变成僵尸线程,消耗系统资源,线程的资源回收既可以使用pthread_join让主线程等待线程结束然后再回收,也可以将线程设置为分离状态(detach)这样主线程就不用去堵塞等待
分离状态的线程结束(退出)后自动回收资源,不需要主线程通过pthread_join等待回收
将线程设置为分离状态 ----------- pthread_detach (也可以通过2.6.2方法进行属性配置)
传入要设置的线程ID
2.4线程退出
2.4.1 正常退出 pthread_exit
线程的正常退出有以下两种方式:
1、在线程函数中执行return语句(不会执行退出处理函数)
2、调用了pthread_exit函数退出(可以执行退出处理函数, 这个函数可以由程序猿自己定义,在2.5中有讲解)
参数就是线程函数的返回值
2.4.2 非正常退出 pthread_cancel
线程的非正常退出有以下两种方式:
1、线程执行时遇到错误(不会执行退出处理函数)
2、被其他线程终止/取消(pthread_cancel)(可以执行退出处理函数, 这个函数可以由程序猿自己定义,在2.5中有讲解)
参数就是要取消线程的ID,此外, 线程可以设置是否允许被取消(在2.6.2线程属性中讲解)
2.5线程退出处理函数 ----------------- pthread_cleanup_push/pthread_cleanup_pop
其中push函数用于往线程退出处理函数栈中 加入 退出处理函数,pop函数用于往线程退出处理函数栈中 取出 退出处理函数
push 和 pop 必须成对出现,否则编译报错
栈是先进后出
push:
routine - 退出处理函数
arg - 传递给退出处理函数的参数
pop:
execute - 非0:取出执行
0:取出不执行
退出处理函数在以下三种情况下会自动执行 :
1、线程被其他线程取消(pthread_cancel),调用所有加入了的退出处理函数
2、线程调用pthread_exit自己退出,调用所有加入了的退出处理函数
3、执行pthread_cleanup_pop、同时传入非0参数时, 在线程退出处理函数栈中遵循先进后出原则取出一条退出处理函数,并执行它
2.6线程的属性
线程的属性在创建线程可以给定,线程运行期间某些属性可以修改
2.6.1初始化属性结构和销毁属性结构 ----------- pthread_attr_init / pthread_attr_destroy
使用属性前先调用init函数初始化,不再使用调用destroy函数销毁
2.6.2属性设置和获取函数 pthread_attr_setdetachstate / pthread_attr_getdetachstate
设置线程的分离/join属性:
参数:
attr - 属性
deatchstate - 状态
PTHREAD_CREATE_DETACHED - 分离状态
PTHREAD_CREATE_JOINABLE - join状态(默认)
设置线程是否允许被其他线程取消:
state(状态):
PTHREAD_CANCEL_ENABLE - 允许取消(默认)
PTHREAD_CANCEL_DISABLE - 禁止取消
2.7多进程和多线程的接口比较
多线程和多进程编程有很多类似的地方, 这里我们将它们的相关函数作个对比:
3. 线程的互斥和同步
3.1概念
我们知道,线程是共享同一个进程的进程空间,因此对于同一资源,不同线程同时去访问,会造成数据的混乱
为了避免这种情况, 所以我们要用线程的互斥和同步
互斥和同步的概念和进程中的互斥和同步是一个意思, 线程用的互斥和同步比进程用的更多, 由于线程共享的内容太多
多线程之间共享所处进程的资源,因此很容易出现共享数据冲突。解决方案就是将线程对共享资源的访问由并行改为串行,实现这种串行访问的技术就是我们所谓线程的互斥和同步技术
多线程中实现互斥和同步的技术包括:互斥锁 信号量 条件变量
其中互斥锁和信号量常用于互斥,条件变量用于同步
3.2互斥锁
互斥锁也叫互斥量,常用于互斥
属于pthread线程库里面的
加锁只能加1次
1)声明初始化互斥锁
pthread_mutex_t lock;
pthread_mutex_init(&lock,NULL);
or
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
2)加锁/上锁 --------- 访问共享资源前
pthread_mutex_lock(&lock); ---------- 阻塞加锁(等待直到加锁成功)
pthread_mutex_trylock(&lock);------- 非阻塞加锁(不管是否加锁成功都立即返回,成功返回0,失败返回非0)
3)执行访问共享资源的代码
4)解锁 ------------- 访问完共享资源
pthread_mutex_unlock(&lock);
5)不再使用释放锁
pthread_mutex_destroy(&lock);
3.3信号量
信号量常用于互斥
信号量是一个计数器,用来控制访问共享资源的最大并行 线程/进程 数,工作原理和进程中的信号量集中的信号量是一样的, 但是它们是不一样的两个东西
信号量不属于线程库的范围,只是一个辅助工具,使用信号量需要加头文件 semaphore.h
信号量分为无名信号量和有名信号量,最常用是无名信号量
3.3.1无名信号量的编程方法:
1)声明初始化信号量
sem_t sem;
sem_init(&sem,0,信号量初始值);
第一参数是信号量的地址
第二参数表示使用场景,0表示用于线程间,非0表示用进程间(无效)
第三个参数就是信号量的初始值
2)P操作(-1)
sem_wait(&sem);
3)执行访问共享资源代码
4)V操作(+1)
sem_post(&sem);
5)不再使用销毁信号量
sem_destroy(&sem);
3.3.2有名信号量的编程方法:
1)创建/打开有名信号量 ------- sem_open
参数:
name - 有效的路径
oflag - 打开标志(同open的标志)
mode - 创建时有效,代表权限
value - 创建时有效,代表初始值
成功返回信号量的地址,失败返回SEM_FAILED
2)P操作(-1)
sem_wait(&sem);
3)执行访问共享资源代码
4)V操作(+1)
sem_post(&sem);
5)关闭信号量
传入信号量地址
6)不再使用删除信号量
传入的是有名信号量的路径
3.4条件变量
1)概念
常用于线程的同步
条件变量的数据类型为 pthread_cond_t
在多线程程序运行中,某个线程需要等待某个条件成立后再继续运行,这种等待可以在其他线程中唤醒
条件变量的使用需要互斥锁的支持,用于防止条件竞争,这里被竞争的是条件变量
2)条件变量编程步骤
(1)分配初始化
pthread_cont_t cond;
pthread_cond_init(&cond,NULL);
(2)等待条件成立
代码如何写,格式如下(其中互斥锁的作用是防止条件竞争):
//先加锁
pthread_cond_wait(&cond,&lock);
//执行使用条件的代码
//解锁
(3)唤醒等待条件的线程
pthread_cond_signal(&cond); ------------ 唤醒一个等待条件的线程
pthread_cond_broadcast(&cond); --------- 唤醒所有等待的线程(惊群)
(4)不再使用可以删除
pthread_cond_destroy(&cond);