1.基础知识与线程的创建、退出和等待
-
进程的特点:进程是资源分配的基本单位,拥有自己独立的资源,拥有自己独立的数据段、代码段、堆栈段,所以cpu切换进程的时候,需要保存较多上下文。为了解决cpu保存进程上下文所付出的较大开销,所以就引入了线程。
-
进程间通信方式:管道、共享内存、信号量、消息队列、信号
-
线程:是cpu调度的基本单位,一个进程里面至少拥有一个线程。如果在一个进程里面创建多个线程,那么这些线程共享该地址空间。线程只拥有少量的栈空间。
-
线程是cpu调度的最小单位、与同进程中线程共享进程的地址空间、只拥有少量的占空间、执行速 度快、同进程内切换速度快。不利于资源的管理和保护。
-
线程的缺点:不利于资源的管理和保护,而进程更利于资源的管理和保护的。
-
线程的种类
-
用户级线程:用户自己创建的线程
-
核心级线程:cpu实际运行的线程
-
-
进程:创建fork() 退出exit() 等待wait()
-
线程:创建pthread_create() 退出pthread_exit() 等待pthread_join()
-
线程库NPTL,查看方式man 7 pthreads
-
不要使用进程的退出方式退出线程。
-
线程创建
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); //成功返回0,失败返回错误码 //参数1:线程id //参数2:线程的属性 //参数3:线程的处理函数 //参数4:线程处理函数的参数
-
两个线程中打印三句话的原因(两句 i am child thread): 由于usleep时间太短了,导致子线程刚把数据输出到屏幕上,还没有来得及清空缓冲区,主线程就从usleep苏醒过来,执行return,发现缓冲区没有被清空,就刷新缓冲。
#include <head.h> void* threadFunc(void* p) { printf("i am child thread\n"); return NULL; } int main(int argc, char **argv) { pthread_t thid; int ret = 0; ret = pthread_create(&thid, NULL, threadFunc, NULL); THREAD_ERROR_CHECK(ret, "pthread_create"); printf("i an mian thread\n"); usleep(1); return 0; }
-
编程中尽量不要把自己申请的资源交给别人使用,会导致资源泄露,更严重的会导致死锁。
-
一个进程当中只有一个主线程,其它都是子线程,主线线程可以等待子线程并且回收子线程的资源,其它子线程级别相同,可以互相等待,但是它们都不能等待主线程。
-
#include <head.h> void* threadFunc(void* p) { printf("i am child thread, val = %ld\n", (long)p); //return NULL; pthread_exit(NULL); } int main(int argc, char **argv) { pthread_t thid, thid1; int ret = 0; long val = 2; ret = pthread_create(&thid, NULL, threadFunc, (void*)val); THREAD_ERROR_CHECK(ret, "pthread_create"); val += 2; ret = pthread_create(&thid1, NULL, threadFunc, (void*)val); THREAD_ERROR_CHECK(ret, "pthread_create"); printf("i an mian thread\n"); /* usleep(1); */ sleep(1); return 0; } //这种方法采用的值传递,pthread_create第四个参数需要的8个字节,所以我们只需要传递8个字节就可以了。
-
return 是C语言阵营的退出方式,void pthread_exit(void *retval)它是NPTL线程的线程退出方式。
-
线程的等待函数 int pthread_join(pthread_t thread, void **retval)
-
成功返回0, 失败返回错误码
-
参数1:等待退出的线程id
-
参数2:接收子线程的退出
-
可以接收指针类型的退出状态
#include <head.h> void* threadFunc(void* p) { printf("i am child thread\n"); char *p1 = (char*)malloc(20); strcpy(p1, "success\n"); //子线程的退出,并传递出一块堆空间 pthread_exit(p1); } int main(int argc, char **argv) { pthread_t thid; int ret = 0; ret = pthread_create(&thid, NULL, threadFunc, NULL); THREAD_ERROR_CHECK(ret, "pthread_create"); printf("i an mian thread\n"); char *retVal = NULL; //等待子线程的退出,并回收子线程的资源 ret = pthread_join(thid, (void**)&retVal); THREAD_ERROR_CHECK(ret, "pthread_join"); printf("retVal = %s\n", retVal); free(retVal); retVal = NULL; return 0; }
-
还可以接收long类型的变量
#include <head.h> void* threadFunc(void* p) { printf("i am child thread\n"); long ret = 2; pthread_exit((void*)ret); } int main(int argc, char **argv) { pthread_t thid; int ret = 0; ret = pthread_create(&thid, NULL, threadFunc, NULL); THREAD_ERROR_CHECK(ret, "pthread_create"); printf("i an mian thread\n"); long retVal = 0; //等待子线程的退出,并回收子线程的资源 ret = pthread_join(thid, (void**)&retVal); THREAD_ERROR_CHECK(ret, "pthread_join"); printf("retVal = %ld\n", retVal); return 0; }
-
-
线程的退出 void pthread_exit(void *retval)
-
查看线程相关信息的命令:ps -elLf
-
查看进程相关信息的命令:ps -elf
2.线程的取消
-
线程可以自己调线程退出函数退出,也可以被其它线程杀掉,被其它线程杀掉的方式就叫线程的取消。
-
被杀掉的线程可以选择自己如何处理这个信号,忽略、立刻终止、运行至取消点后终止。
-
什么是取消点?
取消其实就是一些特殊函数:一般阻塞函数都是取消点,非阻塞性函数也可能是取消点。
-
线程取消函数 int pthread_cancel(pthread_t thread)
- 成功返回0, 失败返回错误码
- 参数:想要cancel线程的id
-
ps -elLf看到的线程id是核心级线程的id,pthread_create()传出的参数thid是用户级线程的id
-
获取线程id的函数pthread_t pthread_self(void)
永远不会失败,成功返回线程id
-
线程取消函数: 另外一个线程收到cancel信号默认行为是运行完取消点才结束,可以通过pthread_setcancelstate()函数进行设置修改默认行为。
-
一般不使用cancel进行线程取消,因为会造成内存的泄露,如子线程内在堆上申请了内存,取消点在释放内存的前面,当主线程杀子线程时,内存不会释放,造成内存泄露。
3.线程终止清理函数
-
pthread_cleanup_push(), pthread_cleanup_pop(); 这是一个函数对,管理是一个栈结构,调用push的时候就是把清理函数入栈,调用pop的时候,清理函数出栈。
-
void pthread_cleanup_push(void (*routine)(void *), void *arg);
- 参数1:清理函数
- 参数2:清理函数的参数
-
void pthread_cleanup_pop(int execute);
- 如果填非0值,代表清理函数出栈,后进先出
- 如果填0,表示清理函数不出栈
-
使用注意事项:
- 必须成对使用
- 必须在代码的同一层次使用
-
在清理函数对中间的代码如果意外退出, 执行清理函数才是正确方式。
-
线程清理函数得到相应的时机:三个时机
- 线程被cancel掉
- 显式的弹栈pop(1)
- pthread_exit() //只要pthread_clean_pop()写在pthread_exit()的后面,即使是pthread_cleanup_pop(0)也会调用,当线程pthread_exit()退出的时候,清理函数栈中还有函数,且标志位为1,还未被清理函数pthread_clearnup_pop(0)置为0,故清理函数弹栈执行。
-
线程清理函数不会相应的时机:两个
- pop(0)
- return ;
-
cancel代码当中尽量不要使用。
-
push放在紧邻需要清理的资源下面
-
pop尽量往后放。
-
对于 man 不了线程的相关函数需要增加 man 信息 , apt-get install manpages-posix-dev
-
#include <head.h> void cleanFunc(void* p) { printf("cleanFunc\n"); free(p); p = NULL; } void *threadFunc(void *p) { char *p1 = (char*)malloc(20); strcpy(p1, "hello"); //线程清理函数入栈 pthread_cleanup_push(cleanFunc, p1); printf("p1 = %s\n", p1); free(p1); p1 = NULL; printf("free success\n"); pthread_cleanup_pop(0); //线程清理函数出栈 pthread_exit(NULL); } int main(int argc, char **argv) { int ret = 0; pthread_t thid; ret = pthread_create(&thid, NULL, threadFunc, NULL); THREAD_ERROR_CHECK(ret, "pthread_create"); //线程的取消 ret = pthread_cancel(thid); THREAD_ERROR_CHECK(ret, "pthread_cancel"); printf("i am main thread\n"); pthread_join(thid, NULL); return 0; }
4.线程互斥
-
对资源的独占式访问
-
线程锁pthread_mutex_t mutex称为互斥锁或者互斥体。访问共享资源的时候,需要加锁对资源进行访问,访问完成之后解锁。如果已经有锁锁住该资源,那么就阻塞,直到该资源被释放。
-
锁的初始化
-
静态方式
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
-
动态的方式
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr) //成功返回0, 失败返回错误码 //参数1:需要初始化的锁变量 //参数2:锁的属性,通常为NULL
-
-
锁的使用
-
加锁
int pthread_mutex_lock(pthread_mutex_t *mutex); //成功返回0, 失败返回错误码 //参数:需要加锁的变量 //阻塞型函数
-
解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
-
锁的销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex); //成功返回0,失败返回错误码
-
测试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex); //它是非阻塞性函数,如果一把锁正在使用,那么使用trylock不会加锁加锁成功,也不会阻塞,会返回错误码。如果这把锁没有被使用,那么trylock的行为和lock一样。
-
-
锁的注意事项
- 不能对一把正在使用的锁进行销毁操作
- 加锁一次:锁的引用值+1;解锁时,引用计数-1。锁的引用计数为0的时候,锁才能被销毁。所以,对一把锁加锁1次,连续解锁两次,destroy不会成功。 对一把锁加锁一次、解锁两次,再加锁一次,再destroy会执行成功。
- pthread_mutex_lock是一个阻塞性函数。连续加锁会阻塞(哪怕先解锁,引用计数变成-1,再两次加锁)。
- 自死锁:自己连续加锁两次 ; abba锁
-
互斥锁的属性
-
普通锁、快速锁,pthread_mutex_init(&mutex, NULL);
-
嵌套锁:可以对一把锁连续加锁多次
pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); //线程锁属性类型设置函数 int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); //参数1:设置锁的属性变量 //参数2:需要设置的类型 //嵌套锁type填:PTHREAD_MUTEX_RECURSIVE
-
检错锁:它可以检测最简单的自死锁, 还可以检测多余的解锁。
pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); //线程锁属性类型设置函数 int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type); //type填PTHREAD_MUTEX_ERRORCHECK设置检错锁
-
-
对于不同属性的锁,加锁和解锁的各自表现
- 加锁:
- 对于普通锁:不允许连续加锁
- 对于嵌套锁:可以连续加锁多次
- 对于检错锁:连续加锁、连续解锁会返回错误码
- 解锁:
- 对于普通锁:解锁者可以是同进程内任意线程
- 对于嵌套锁:帮助文档要求必须由加锁者解锁,但是同进程的任意线程也可以解锁
- 对于检错锁:解锁者必须由加锁者解锁
- 加锁:
-
死锁的概念:指多个进程因竞争资源而造成的一种僵局。若无外力的干预,这些进程将无法向前推进
-
死锁产生的原因:1.系统资源的竞争 2.进程的推进顺序不合理
-
死锁产生的必要条件 1.互斥条件 2.不可剥夺条件 3. 循环等待 4.请求与保持
-
死锁的避免:破坏四个必要条件之一即可。
5.线程的同步
-
什么是同步:两个或两个以上随时间变化的量,在变化的过程当中保持一定的相对关系。
-
线程实现同步的方式是条件变量。
-
pthread_cond_t cond
-
条件变量的初始化:
-
动态方式
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr) //参数1:待初始化的条件变量 //参数2:条件变量的属性,一般都填NULL
-
静态方式
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
-
-
条件变量的销毁:只有在条件变量没有被使用的时候,才能被销毁
int pthread_cond_destroy(pthread_cond_t *cond)
-
条件变量的等待:
-
无条件等待
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex); //参数1:条件变量 //参数2:互斥锁
-
计时等待:等待一段时间,如果条件没有成立,就返回
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
-
-
函数的上下半部:指的是函数的行为发生了改变
-
对比买酒-卖酒模型(先在外面手动加锁进入商店):pthread_cond_wait的上半部:商店里没有酒,在登记表登记,解锁出商店,回家睡觉
下半部: 商店老板通知顾客商店有酒了,顾客睡醒,加锁进入商店买酒,删除登记本上的信息(然后在外面手动解锁)。
-
对比代码:pthread_cond_wait的上半部:1.条件变量上排队,2.解锁 3.睡眠;
下半部:1.被唤醒 2.加锁(如果此时该锁没有被使用,那么就加锁成功, 如果此时这个锁正在被使用的,会阻塞,直到该锁被释放,加锁) 3.pthread_cond_wait函数返回。
-
-
条件变量的激发:
-
每次激发一个条件变量
int pthread_cond_signal(pthread_cond_t *cond)
-
激发所有条件变量
int pthread_cond_broadcast(pthread_cond_t *cond)
-
-
宏定义的时候do-while(0)的意义是:防止该宏定义写在判断语句的后面,而该判断语句没有加括号。
-
买票卖票例子
#include <funcl.h> #define N 10 #define Round 3 typedef struct data_s{ int num; int round_s; pthread_cond_t cond; pthread_cond_t cond1; pthread_mutex_t mutex; }data_t; void *setTickets(void *p){ data_t *data = (data_t *)p; for(int i=0;i<Round;i++){ pthread_mutex_lock(&data->mutex); pthread_cond_wait(&data->cond,&data->mutex); data->num = N; data->round_s--; printf("%d time setTicks\n",i+1); pthread_cond_broadcast(&data->cond1); pthread_mutex_unlock(&data->mutex); } pthread_exit(NULL); } void *sale_Win1(void *p){ data_t *data = (data_t *)p; while(1){ if(data->num == 0 && data->round_s == 0){ printf("Win1 no tickets,quit!\n"); pthread_exit(NULL); } pthread_mutex_lock(&data->mutex); if(data->num > 0){ --data->num; printf("Win1 sales a ticket\n"); } else{ pthread_cond_signal(&data->cond); pthread_cond_wait(&data->cond1,&data->mutex); } pthread_mutex_unlock(&data->mutex); } pthread_exit(NULL); } void *sale_Win2(void *p){ data_t *data = (data_t *)p; while(1){ if(data->num == 0 && data->round_s == 0){ printf("Win2 no tickets,quit!\n"); pthread_exit(NULL); } pthread_mutex_lock(&data->mutex); if(data->num > 0){ --data->num; printf("Win2 sales a ticket\n"); } else{ pthread_cond_signal(&data->cond); pthread_cond_wait(&data->cond1,&data->mutex); } pthread_mutex_unlock(&data->mutex); } } int main(int argc,char*argv[]) { int ret = 0; pthread_t thid,thid1,thid2; data_t data; //初始化 data.num = 0; data.round_s = Round; ret = pthread_mutex_init(&data.mutex,NULL); THREAD_ERROR_CHECK(ret,"pthread_mutex_init"); ret = pthread_cond_init(&data.cond,NULL); THREAD_ERROR_CHECK(ret,"pthread_cond_init"); ret = pthread_cond_init(&data.cond1,NULL); THREAD_ERROR_CHECK(ret,"pthread_cond_init 1"); //线程创建 ret = pthread_create(&thid,NULL,setTickets,(void *)&data); THREAD_ERROR_CHECK(ret,"pthread_create 0"); ret = pthread_create(&thid1,NULL,sale_Win1,(void *)&data); THREAD_ERROR_CHECK(ret,"pthread_create 1"); ret = pthread_create(&thid2,NULL,sale_Win2,(void *)&data); THREAD_ERROR_CHECK(ret,"pthread_create 2"); //线程接收 ret = pthread_join(thid1,NULL); THREAD_ERROR_CHECK(ret,"pthread_join 1"); ret = pthread_join(thid2,NULL); THREAD_ERROR_CHECK(ret,"pthread_join 2"); ret = pthread_join(thid,NULL); THREAD_ERROR_CHECK(ret,"pthread_join 0"); pthread_mutex_destroy(&data.mutex); pthread_cond_destroy(&data.cond); return 0; }
6.线程安全
-
线程安全:如果一个函数能够安全的同时被多个线程调用而得到正确的结果
-
//线程不安全的函数 char *ctime(const time_t *timep); //ctime()函数返回的是静态全局区的地址,不同线程调用ctime函数会将结果相互覆盖 //线程安全函数 char *ctime_r(const time_t *timep, char *buf) //参数2:保存当前调用ctimr_r函数所生成的时间字符串的内存区,不同线程调用时传入的地址不同,所以保存时间字符串的位置不同,所以线程安全
-
可重入函数 : 简单来说就是可以被中断的函数 (或者说,该函数被中断后重新载入(调用)该函数能够正确执行)
-
线程安全函数和可重入函数的关系: 可重入函数一定是线程安全函数,线程安全函数不一定时可重入函数。
-
线程的属性(pthread_create 的第二个参数 attr 是一个结构体指针,结构中的元素分别指定新线程的运行属性,通过该结构体的不同填法,可以设置不同的线程属性)
-
设置线程分离属性(表示新线程是否与进程中其他线程脱离同步,如果设置该属性则新线程不能用 pthread_join()来等待,且在退出时自行释放所占用的资源)
-
主线程设置子线程的分离属性
pthread_attr_t attr; pthread_attr_init(&attr); int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); //参数2:设置为PTHREAD_CREATE_DETACHED是线程脱离同步,默认是 PTHREAD_CREATE_JOINABLE
-
子线程设置自己为分离属性:pthread_detach(pthread_self());
-
-
设置绑定属性的函数为 pthread_attr_setscope()
设置分离属性的函数是 pthread_attr_setdetachstate()
设置线程优先级的相关函数pthread_attr_getscehdparam()(获取线程优先级)和 pthread_attr_setschedparam()(设置线程优先级)