LINUX 多线程
目录
1. Linux 多线程概述
1.1概述 | 1.2. 线程分类 | 1.3. 线程创建的 Linux 实现 |
---|
2. 线程的创建和退出
创建线程函数 pthread_create() | 实例1:创建线程 | 实例2:创建线程传参(堆空间栈空间) |
---|---|---|
实例3:主线程子线程各加2000万 | ||
线程退出函数 pthread_exit() | example:线程的退出 | |
获取线程自身ID函数 pthread_self() |
3. 线程的等待退出
4. 线程的同步与互斥
4.1. 线程的互斥
5. 线程安全和线程属性
5.1. 线程安全
可重入函数 | 实例23:主线程子线程都获得当前时间 | 实例24:主线程子线程都获得当前时间,并使用 ctime_r 安全函数 |
---|
1. Linux 多线程概述
1.1. 概述
线程的出现:
进程是系统中程序执行和资源分配的基本单位。每个进程有自己的数据段、代码段和堆栈段。这就造成进程在进行切换等操作时都需要有比较负责的上下文切换等动作。为了进一步减少处理器的空转时间,支持多处理器和减少上下文切换开销,也就出现了线程。
线程的描述:
线程也称轻量级进程。
线程是在共享内存空间中并行执行的多道执行路径,是一个更加接近于执行体的概念,拥有独立的执行序列,是进程的基本调度单元,每个进程至少都有一个 main 线程。
(线程是不可以独立存在的,它必须依附于进程,即一个线程一定是隶属于某个进程的)
程序执行实际是栈空间的弹栈压栈。子线程实际就是在栈空间另一个地方的一个函数,它和main函数同时执行,它有新的起始的栈顶指针。启动一个线程以后就是给这个线程一个新的入口函数,各自拥有自己私有的栈空间,如果同时执行 getcwd()
其实是一个结果,因为是共享的
线程与同进程中的其他线程共享进程空间{堆,代码,数据,文件描述符,信号等},只拥有自己的栈空间,大大减少了上下文切换的开销。
线程和进程在使用上各自的优缺点:
线程:线程执行开销小(创建线程时间短),占用的 CPU 少,线程之间的切换快(上下文少),但不利于资源的管理和保护;
(启动线程很快,是因为不需要构造整个 task struct
结构体,只需要构造结构体里线程私有的部分,其他的数据都是共享的)
(某一个线程崩溃会影响整个进程,让进程崩溃)
进程:程执行开销大,占用的 CPU 多,线程之间的切换慢,但有利于资源的管理和保护(一个进程崩溃不会影响其他进程)。从可移植性来讲,多进程的可移植性要好些。
同进程一样,线程也将相关的变量值放在线程控制表内。一个进程可以有多个线程,也就是有多个线程控制表及堆栈寄存器,但却共享一个用户地址空间。要注意的是,由于线程共享了进程的资源和地址空间,因此,任何线程对系统资源的操作都会给其他线程带来影响。
1.2. 线程分类
线程按调度者分为用户级线程和核心级线程
- 用户级线程:主要解决上下文切换问题,调度算法和调度过程全部由用户决定,在运行时不需要特定 的内核支持。缺点是无法发挥多处理器的优势(假线程,协程)
- 核心级线程:允许不同进程中的线程按照同一相对优先调度方法调度,发挥多处理器的并发优势 现在大多数系统都采用用户级线程和核心级线程并存的方法。一个用户级线程可以对应一个或多个核心级线程,也就是“一对一”或“一对多”模型。
一般使用的线程库为NPTL线程库(Native POSIX Threads Library,本地POSIX线程库)
1.3. 线程创建的 Linux 实现
Linux 的线程是通过用户级的函数库实现的,一般采用 pthread 线程库实现线程的访问和控制。它用第 3 方 posix 标准的 pthread,具有良好的可移植性。编译的时候要在后面加上 -lpthread(-pthread)
(显式链接)
创建 | 退出 | 等待 | |
---|---|---|---|
多进程 | fork() |
exit() |
wait() |
多线程 | pthread_create() |
pthread_exit() |
pthread_join() (合并) |
在子线程使用exit()会导致整个进程结束
(所有pthread开头的)接口不再使用 perror,而是返回 error 错误码
2. 线程的创建和退出
创建线程实际上就是确定调用该线程函数的入口点,线程的创建采用函数 pthread_create
。
在线程创建以后,就开始运行相关的线程函数,线程退出方式有两种:
-
在该函数运行完之后,线程就退出。
-
另一种线程退出的方式是使用函数
pthread_exit()
函数,这是线程主动退出行为。
(这里要注意的是,在使用线程函数时,不能随意使用 exit
退出函数进行出错处理,由于 exit
的作用是使调用进程终止,往往一个进程包括了多个线程,所以在线程中通常使用 pthread_exit
函数来代替进程中的退出函数 exit
)
由于一个进程中的多个线程是共享数据段的,因此通常在线程退出之后,退出线程所占用的资源并不会随着线程的终止而得到释放。
正如进程之间可以通过 wait()
函数系统调用来同步终止并释放资源一样,线程之间也有类似的机制,那就是 pthread_join
函数。pthread_join
函数可以用于将当前线程挂起, 等待线程的结束。这个函数是一个线程阻塞函数,调用它的函数将一直等待直到被等待的线程结束为止, 当函数返回时,被等待线程的资源被回收。
函数原型:
头文件:#include <pthread.h>
① 函数 pthread_create()
int pthread_create(pthread_t* thread, pthread_attr_t * attr, void *(*start_routine)(void *), void * arg);
作用:创建线程
参数:
thread
是传出参数,保存新线程的标识(线程id,提前定义好取地址放入)attr
是一个结构体指针,结构中的元素分别指定新线程的运行属性,attr
可以用pthread_attr_init
等
函数设置各成员的值,但通常传入为 NULL(普通线程) 即可start_routine
是一个函数指针,指向新线程的入口点函数,(进程函数,返回值是void,入参是void**)线程入口点函数带有一个 void *的参数由pthread_create
的第 4 个参数传入arg
用于传递给第 3 个参数指向的入口点函数的参数,可以为 NULL,表示不传递
返回值:成功,则返回 0;失败,则返回对应错误码
实例1:创建线程
首先封装 THREAD_ERROR_CHECK
来获取错误信息
#define THREAD_ERROR_CHECK(ret, funcName) {if(ret != 0) {printf("%s:%s\n", funcName, strerror(ret)); return -1;}}
再封装 Makefile
SRCS:=$(wildcard *.c)
ELFS:=$(SRCS:%.c=%)
CC:=gcc
all:$(ELFS)
%:%.c
$(CC) $< -o $@ -lpthread
clean:
rm -rf $(ELFS)
创建进程程序:
void* threadFunc(void* p) -线程函数
{
printf("I am child thread\n"); -一定要加\n
}
int main()
{
pthread_t pthid; - 定义一个线程id(其实为整型数)
int ret;
创建线程(&pthid 取地址线程id,NULL 普通线程,threadFunc 线程函数,线程函数参数)
ret = pthread_create(&pthid, NULL, threadFunc, NULL);
THREAD_ERROR_CHECK(ret, "pthread_create");
线程创建,启动,运行,打印需要时间;如果不加有可能不会打印子进程的printf(),看速度和优先级
一般使用 pthread_join 此处为特例
usleep(1);
printf("I am main thread\n");
return 0;
}
进程:只能父亲等孩子,不能孩子等父亲,有从属关系;
线程:除了所有线程不能等 main() 线程(main() 退出整个进程都结束了,其他线程不能合并 main() 线程),其他线程可以任意互相合并
实例2:创建线程——传参
使用堆空间传参
void* threadFunc(void* p)
{
printf("I am child thread %s\n", (char*)p);
}
int main()
{
pthread_t pthid;
int ret;
char *pArg=(char*)malloc(20);
strcpy(pArg, "hello");
ret = pthread_create(&pthid, NULL, threadFunc, pArg); -pArg可以不用强转,任何类型指针转void*不需要强转
THREAD_ERROR_CHECK(ret, "pthread_create");
pthread_join(pthid, NULL); -等待子线程(线程id,NULL)(回收子线程的实体),子线程已结束就会唤醒主线程,主线程接着运行
printf("I am main thread\n");
return 0;
}
实际传的是堆空间,如何传栈空间?
栈空间是私有的怎么传?
主线程把某个变量的地址给子线程,子线程就可以访问了,虽然各自是各自私有的,整个进程地址空间对内存(栈,堆,数据段)的访问权限是一样的,如果有地址就可以互相访问了。
使用栈空间传参
void* threadFunc(void* p)
{
printf("I am child thread %d\n", *(int*)p); -得到整型数的值
}
int main()
{
pthread_t pthid, pthid1;
int ret;
int val = 10;
ret = pthread_create(&pthid, NULL, threadFunc, &val);
THREAD_ERROR_CHECK(ret, "pthread_create");
pthread_join(pthid, NULL); -等待子线程
printf("I am main thread\n");
return 0;
}
使用栈空间传参2
启动两个线程
void* threadFunc(void* p)
{
printf("I am child thread %d\n", *(int*)p);
}
int main()
{
pthread_t pthid, pthid1;
int ret;
int val = 1;
ret = pthread_create(&pthid, NULL, threadFunc, &val);
THREAD_ERROR_CHECK(ret, "pthread_create");
val = 2;
ret = pthread_create(&pthid1, NULL, threadFunc, &val);
THREAD_ERROR_CHECK(ret, "pthread_create");
pthread_join(pthid, NULL); -等待子线程
pthread_join(pthid1, NULL); -等待子线程
printf("I am main thread\n");
return 0;
}
预期得到结果,打印为1和2
实际得到结果:
I am child thread 2
I am child thread 2
I am main thread
原因:
并发,启动子线程时,主线程同时在运行,子线程还没有来得及执行到打印时,主线程已经运行到了 ret = 2,改了ret的值,由于线程共同访问一个空间,所以都是2。
进行改进
void* threadFunc(void* p)
{
printf("I am child thread %ld\n", (long)p);
}
int main()
{
pthread_t pthid, pthid1;
int ret;
long val = 1;
ret = pthread_create(&pthid, NULL, threadFunc, (void*)val); -直接把整型数放到空间里,用8个字节的空间来传递数据(指针变量)
THREAD_ERROR_CHECK(ret, "pthread_create");
val = 2;
ret = pthread_create(&pthid1, NULL, threadFunc, (void*)val); -直接把整型数放到空间里
THREAD_ERROR_CHECK(ret, "pthread_create");
pthread_join(pthid, NULL);//等待子线程
pthread_join(pthid1, NULL);//等待子线程
printf("I am main thread\n");
return 0;
}
启动的一瞬间,这个值已经赋值给了启动的线程的位置,所以在打印时,val不会被修改(因为,改的是val,读的是形参里的值)
运行结果:
I am child thread 2
I am child thread 1
I am main thread
实例3:主线程子线程各加2000万
#define N 20000000 -可以产生并发的值
void* threadFunc(void* p)
{
int i;
int *pVal = (int*)p; -原本不要这句
for(i = 0; i < N; i++)
{
*pVal += 1; - 原本是 *(int*)p += 1;
}
}
int main()
{
pthread_t pthid;
int ret;
int val = 0;
ret = pthread_create(&pthid, NULL, threadFunc, &val);
THREAD_ERROR_CHECK(ret, "pthread_create");
int i;
for(i = 0; i < N; i++)
{
val += 1;
}
pthread_join(pthid, NULL); -等待子线程
printf("I am main thread,val=%d\n", val);
return 0;
}
运行结果:
I am main thread,val=37740895
I am main thread,val=35513594
I am main thread,val=40000000
I am main thread,val=37716209
I am main thread,val=37736644
很大概率不为4000万(N越大的时候。相加的值越不容易是2*N)
② 函数 pthread_exit()
void pthread_exit(void *retval);
作用:表示线程的退出
参数:value_ptr
是函数的返回代码,可以被其它线程用 pthread_join
函数捕获,只要 pthread_join
中的第二个参数不是NULL,这个值将被传递给 value_ptr
。
( int pthread_join(pthread_t th, void **thread_return);
)
example:线程的退出
void print()
{
printf("I am print\n");
pthread_exit(NULL);
- 使用 pthread_exit,在print让线程结束,不会执行print之后的
- 如果使用return,线程继续执行,会打印 I am child, after print
- 使用 exit(),进程直接结束只打印子进程print前的一句和exit之前一句
}
void* threadFunc(void* p)
{
printf("I am child thread\n");
print(); -线程到此结束
printf("I am child, after print\n"); -不会被打印
return NULL;
}
int main()
{
pthread_t pthid;
int ret;
ret = pthread_create(&pthid, NULL, threadFunc, NULL);
THREAD_ERROR_CHECK(ret, "pthread_create");
pthread_join(pthid, NULL);
printf("I am main thread\n");
return 0;
}
运行结果:
I am child thread
I am print
I am main thread
③ 函数 pthread_self()
pthread_t pthread_self(void);
作用:获得线程自身的ID
pthread_t
的类型为 unsigned long int
,所以在打印的时候要使用 %lu
方式,否则显示结果出问题
通常过程的形式为:
pthread_t pthid;
pthread_create(&pthid,NULL,pthfunc,NULL);
或 pthread_create(&pthid,NULL,pthfunc,(void*)3);
pthread_exit(NULL);或 pthread_exit((void*)3); - 3 作为返回值被 pthread_join 函数捕获
编译时记得带上-lpthread
或 -pthread
3.线程的等待退出
3.1. 等待线程退出
线程从入口点函数自然返回,或者主动调用 pthread_exit()
函数,都可以让线程正常终止
线程从入口点函数自然返回时,函数返回值可以被其它线程用 pthread_join
函数获取
pthread_join()
函数:
int pthread_join(pthread_t thread, void **thread_return);
pthread_join()
函数是一个阻塞函数,一直等到参数 thread
指定的线程返回(与多进程中的 wait
或 waitpid
类似)
参数:
thread
:线程id,传递 0 值时,join 返回 ESRCH 错误thread_return
:thread_return
是一个传出参数,接收线程函数的返回值。
(如果线程通过调用pthread_exit()
终止,则pthread_exit()
中的参数相当于自然返回值,照样可以被其它线程用pthread_join
获取到)
(为什么是二级指针,要拿的返回值是一级指针,把一级指针取地址放进pthread_join()
函数,才能去接子进程的指针,因为二级指针才能修改主函数里某个一级指针的值)
实例4:使用 pthread_join
void* threadFunc(void* p)
{
p = malloc(20);
strcpy((char*)p, "I am good man");
printf("I am child thread %s\n", (char*)p);
pthread_exit(p);
}
int main()
{
pthread_t pthid;
int ret;
ret = pthread_create(&pthid, NULL, threadFunc, NULL);
THREAD_ERROR_CHECK(ret, "pthread_create");
char *pRet;
ret = pthread_join(pthid, (void**)&pRet); -任何类型指针转void*不需要强转,而任意类型二级指针转void**则需要强转
THREAD_ERROR_CHECK(ret, "pthread_join");
printf("I am main thread %s\n", pRet);
return 0;
}
运行结果:
I am child thread I am good man
I am main thread I am good man
上面的例子,使用指针返回了指针,但是一般来说返回的不是指针,而是用指针返回一个整型数,代表子线程执行成功还是失败
实例5:使用 pthread_join
并返回整型数
void* threadFunc(void* p)
{
printf("I am child thread\n");
pthread_exit((void*)10);
}
int main()
{
pthread_t pthid;
int ret;
ret = pthread_create(&pthid, NULL, threadFunc, NULL);
THREAD_ERROR_CHECK(ret, "pthread_create");
long threadRet; -不能够用int,要用long,否则访问越界
ret = pthread_join(pthid, (void**)&threadRet); -函数内部只做一次解引用,结果还是整型数
THREAD_ERROR_CHECK(ret, "pthread_join");
printf("I am main thread, the return value is %ld\n", threadRet);
return 0;
}
运行结果:
I am child thread
I am main thread, the return value is 10
该函数还有一个非常重要的作用,由于一个进程中的多个线程共享数据段,因此通常在一个线程退出后,退出线程所占用的资源并不会随线程结束而释放。如果 pthid
线程类型并不是自动清理资源类型的, 则 pthid
线程退出后,线程本身的资源必须通过其它线程调用 pthread_join
来清除,这相当于多进程程序中的 waitpid
。
实例6:子线程释放空间
头函数中添加 #include <malloc.h>
void* threadfunc(void *args)
{
char *p = (char*)malloc(10); -自己分配了内存
int i = 0;
for(; i < 10; i++)
{
printf("hello, my name is wangxiao!\n");
sleep(1);
}
free(p); -如果父线程中没有调用 pthread_cancel,此处可以执行
printf("p is freed\n");
pthread_exit((void*)3);
}
int main()
{
pthread_t pthid;
pthread_create(&pthid, NULL, threadfunc, NULL);
int i = 1;
父线程的运行次数比子线程的要少,当父线程结束的时候,如果没有 pthread_join 函数等待子线程执行的话,子线程也会退出。
for(; i < 5; i++)
{
printf("hello,nice to meet you!\n");
sleep(1);
// if(i % 3 == 0)
// pthread_cancel(pthid);
表示当 i%3==0 的时候就取消子线程,该函数将导致子线 程直接退出,不会执行上面紫色的 free 部分的代码,即释放空间失败。
要想释放指针类型的变量 p,此时必须要用 pthread_cleanup_push 和 pthread_cleanup_pop 函数释放空间,见后面的例子
}
int retvalue = 0;
pthread_join(pthid,(void**)&retvalue); -等待子线程释放空间,并获取子线程的返回值
printf("return value is :%d\n",retvalue);
return 0;
}
3.2. 线程的取消
线程也可以被其它线程杀掉,在 Linux 中的说法是一个线程被另一个线程取消(cancel)。
线程取消的方法是一个线程向目标线程发 cancel 信号,但是如何处理 cancel 信号则由目标线程自 己决定,目标线程或者忽略、或者立即终止、或者继续运行至 cancelation-point (取消点) 后终止。
取消点:
根据 POSIX 标准,pthread_join()
、pthread_testcancel()
、pthread_cond_wait()
、 pthread_cond_timedwait()
、sem_wait()
、sigwait()
等函数以及 read()
、write()
等会引起阻塞的系统调用都是 Cancelation-point,而其他 pthread 函数都不会引起 Cancelation 动作。
但是 pthread_cancel 的手册页 声称,由于 Linux 线程库与 C 库结合得不好,因而目前 C 库函数都不是 Cancelation-point;但 CANCEL 信号会使线程从阻塞的系统调用中退出,并置 EINTR 错误码,因此可以在需要作为 Cancelation-point 的系统调用前后调用 pthread_testcancel(),从而达到 POSIX 标准所要求的目标,即如下代码段:
pthread_testcancel();
retcode = read(fd, buffer, length);
pthread_testcancel();
但是从 RedHat9.0 的实际测试来看,至少有些 C 库函数的阻塞函数是取消点,如 read()
,getchar()
等
而 sleep()
函数不管线程是否设置了 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL)
都起到取消点作用。
总之,线程的取消一方面是一个线程强行杀另外一个线程,从程序设计角度看并不是一种好的风格,另一方面目前 Linux 本身对这方面的支持并不完善,所以在实际应用中应该谨慎使用。
函数 pthread_cancel()
int pthread_cancel(pthread_t thread);
作用:发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着 thread
会终止。
(若是在整个程序退出时,要终止各个线程,应该在成功发送 cancel 指令后,使用 pthread_join
函数,等待指定的线程已经完全退出以后,再继续执行;否则,很容易产生 “段错误”。)关于pthread_cancel()函数
增加 pthread_cancel
接口 man 信息 ,apt install manpages-posix-dev
实例7:cancel一个线程
void* threadFunc(void* p)
{
printf("I am child thread\n");
return NULL;
}
int main()
{
pthread_t pthid;
int ret;
ret = pthread_create(&pthid, NULL, threadFunc, NULL);
THREAD_ERROR_CHECK(ret, "pthread_create");
ret = pthread_cancel(pthid); -不加 cancel 返回值得0
THREAD_ERROR_CHECK(ret, "pthread_cancel");
long threadRet;
ret = pthread_join(pthid, (void**)&threadRet);
THREAD_ERROR_CHECK(ret, "pthread_join");
printf("I am main thread %ld\n", threadRet);
return 0;
}
线程被 cancel 以后 pthread_join
拿到的返回值是 -1
运行结果:
I am child thread
I am main thread -1
cancel一个带while的线程
void* threadFunc(void* p)
{
while(1);
return NULL;
}
int main()
{
pthread_t pthid;
int ret;
ret = pthread_create(&pthid, NULL, threadFunc, NULL);
THREAD_ERROR_CHECK(ret, "pthread_create");
ret = pthread_cancel(pthid);
THREAD_ERROR_CHECK(ret, "pthread_cancel");
long threadRet;
ret = pthread_join(pthid, (void**)&threadRet);
THREAD_ERROR_CHECK(ret, "pthread_join");
printf("I am main thread %ld\n", threadRet);
return 0;
}
cancel 会失败,卡住出现死锁情况
ps -elf 查看进程状态
ps -elLf 查看线程状态
把线程的信息称为线程控制表
死锁状态下实例7的线程状态:
其中 futex 是linux下的一种快速同步(互斥)机制(系统调用),是用户态快速锁(睡觉和唤醒原理),监控用户态某一个变量的值,当这个变量的值发生变化后,就会唤醒在内核里的那个线程。
线程为什么没有返回失败,也没有 cancel 成功?
因为子线程在while(1),所以 pthread_join 卡住等待子线程,出现了类似死锁现象。cancel失败只有一种情况:线程id填错了,只要线程id填对了 pthread_cancel()
一定返回成功。**线程是否cancel成功,和 pthread_cancel()
是否返回成功没有关系。**具体有没有 cancel ,要看线程有没有到 cancel 点。
实例7只是为了测试结果,子线程死循环(while(1))实际是没有意义的。
3.3. 线程终止清理函数
不论是可预见的线程终止还是异常终止,都会存在资源释放的问题,在不考虑因运行出错而退出的前提下,如何保证线程终止时能顺利的释放掉自己所占用的资源,特别是锁资源,就是一个必须考虑解决的问题。
最经常出现的情形是资源独占锁的使用:线程为了访问临界共享资源而为其加上锁,但在访问过程中该线程被外界取消,或者发生了中断,则该临界资源将永远处于锁定状态得不到释放。外界取消操作是不可预见的,因此的确需要一个机制来简化用于资源释放的编程。
在 POSIX 线程 API 中提供了一个 pthread_cleanup_push()
/ pthread_cleanup_pop()
函数对用于自动释放资源
从 pthread_cleanup_push()
的调用点到 pthread_cleanup_pop()
之间的程序段中的终止动作都将执行 pthread_cleanup_push()
所指定的清理函数。API 定义如下:
void pthread_cleanup_push(void (*routine) (void *), void *arg) ;
作用:挂接清理函数,它的返回值类型为 void
参数:
routine
:清理函数函数指针
arg
:是传给清理函数的 typeless pointer (无类型指针)(自己申请空间的起始地址)
void routine(void *arg)
函数在调用 pthread_cleanup_push()
时压入清理函数栈,多次对 pthread_cleanup_push()
的调用将在清理函数栈中形成一个函数链,在执行该函数链时按照压栈的相反顺序弹出。
void pthread_cleanup_pop(int execute)
作用:
用来触发清理函数,是按照相反的顺序来触发清理函数的。如果它的入口参数 execute
为 0 ,他会弹出清理函数,但对应的清理函数不执行
参数:
execute
参数表示执行到 pthread_cleanup_pop()
时是否在弹出清理函数的同时执行该函数, 为 0 表示不执行,非 0 为执行;这个参数并不影响异常终止时清理函数的执行。
线程被 cancel 以后不会立马结束,先将清理函数的函数指针压到栈空间的一个地方,即清理函数栈,一旦被 cancel ,先去清理函数栈看是否为空,如果不为空,有清理函数,就要把清理函数弹出并执行
pthread_cleanup_push()
函数和 pthread_cleanup_pop()
函数必须成对出现
cancel 和 清理函数 需要一起使用
pthread_cleanup_push()/pthread_cleanup_pop()是以宏方式实现的,这是 pthread.h 中的宏定义:
可见,pthread_cleanup_push()
带有一个"{",而 pthread_cleanup_pop()
带有一个"}",因此这两个函数必须成对出现,且必须位于程序的同一级别的代码段中才能通过编译。
pthread_cleanup_pop
的参数 execute
的取值
- 如果为非 0 值,则按栈的顺序注销掉一个原来注册的清理函数, 并执行该函数;
- 为 0 时,仅仅在线程调用
pthread_exit
函数或者其它线程对本线程调用pthread_cancel
函数时,才在弹出“清理函数”的同时执行该“清理函数”。
实例8:使用清理函数
void cleanup(void *p)
{
free(p);
printf("free success\n");
}
void* threadFunc(void* p)
{
p = malloc(20); -不是cancel点
pthread_cleanup_push(cleanup, p); -将压栈清理函数放到栈空间的某个地方
sleep(1); -确保cancel,也是cancel点
printf("I am child thread\n"); -sleep是cancel点,不会执行到这一句
pthread_cleanup_pop(1); -和push成对出现才可以
return NULL;
}
int main()
{
pthread_t pthid;
int ret;
ret = pthread_create(&pthid, NULL, threadFunc, NULL);
THREAD_ERROR_CHECK(ret, "pthread_create");
ret = pthread_cancel(pthid);
THREAD_ERROR_CHECK(ret, "pthread_cancel");
long threadRet;
ret = pthread_join(pthid, (void**)&threadRet);
THREAD_ERROR_CHECK(ret, "pthread_join");
printf("I am main thread %ld\n", threadRet);
return 0;
}
运行结果:
free success
I am main thread -1
实例9:查看清理函数的执行顺序
void cleanup(void* p)
{
printf("I am cleanup %ld\n", (long)p);
}
void* threadFunc(void *p)
{
pthread_cleanup_push(cleanup, (void*)1);
pthread_cleanup_push(cleanup, (void*)2);
pthread_exit(NULL); -线程到这里退出
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
}
int main()
{
pthread_t pthid;
pthread_create(&pthid, NULL, threadFunc, NULL);
long threadRet;
int ret = pthread_join(pthid, (void**)&threadRet);
THREAD_ERROR_CHECK(ret, "pthread_join");
printf("threadRet = %ld\n", threadRet);
return 0;
}
运行结果:
I am cleanup 2
I am cleanup 1
threadRet = 0
先压栈的清理函数,后执行
有一种情况清理函数不会被执行:
void cleanup(void* p){ ... }
void* threadFunc(void *p)
{
pthread_cleanup_push(cleanup,(void*)1);
pthread_cleanup_push(cleanup,(void*)2);
return NULL;// return情况下不会执行清理函数
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
}
int main() { ... }
改为:
void cleanup(void* p){ ... }
void* threadFunc(void *p)
{
pthread_cleanup_push(cleanup,(void*)1);
pthread_cleanup_push(cleanup,(void*)2);
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
return NULL;
}
int main() { ... }
即为执行清理函数的第3种情况
清理函数得到执行有三种情况:
- 线程被取消(被cancel)
- 线程通过
pthread_exit
退出 pthread_cleanup_pop(1)
;
清理函数不会执行的情况:
- pop(0)
- return;
实例10:将free放到清理函数
void cleanup(void* p)
{
free(p); -在清理函数中,就不会在正常的执行流程中
printf("I am cleanup\n" );
}
void* threadFunc(void *p)
{
p = malloc(20);
pthread_cleanup_push(cleanup, p);
sleep(1); -代码执行
// free(p); 错误,free两次出错
pthread_cleanup_pop(1); -要提前free用的是pop,清理函数一执行就会被弹出,就没有了
pthread_exit(NULL);
}
int main(int argc,char* argv[])
{
pthread_t pthid;
pthread_create(&pthid, NULL, threadFunc, NULL);
long threadRet;
int ret = pthread_join(pthid,(void**)&threadRet);
THREAD_ERROR_CHECK(ret, "pthread_join");
printf("threadRet=%ld\n", threadRet);
return 0;
}
4. 线程的同步与互斥
4.1. 线程的互斥
在 Posix Thread 中定义了一套专门用于线程互斥的 mutex 函数。mutex 是一种简单的加锁的方法来控制对共享资源的存取,这个互斥锁只有两种状态(上锁和解锁),可以把互斥锁看作某种意义上的全局变量。为什么需要加锁,就是因为多个线程共用进程的资源,要访问的是公共区间时(全局变量), 当一个线程访问的时候,需要加上锁以防止另外的线程对它进行访问,实现资源的独占。在一个时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行操作。若其他线程希望上锁一 个已经上锁了的互斥锁,则该线程就会挂起,直到上锁的线程释放掉互斥锁为止。
1. 创建和销毁锁
有两种方法创建互斥锁,静态方式和动态方式。
静态方式(一般不用这种方式):
POSIX 定义了一个宏 PTHREAD_MUTEX_INITIALIZER
来静态初始化互斥锁:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
在 Linux Threads
实现中,pthread_mutex_t
是一个结构,而 PTHREAD_MUTEX_INITIALIZER
则是 一个宏常量
动态方式:
先添加头文件:#include <pthread.h>
① pthread_mutex_init()
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr)
作用:互斥锁的初始化
参数:其中 mutex
, attr
用于指定互斥锁属性(见下),如果为 NULL 则使用缺省属性。通常为 NULL
② pthread_mutex_destroy()
int pthread_mutex_destroy(pthread_mutex_t *mutex);
作用:注销一个互斥锁
销毁一个互斥锁即意味着释放它所占用的资源,且要求锁当前处于开放状态。由于在 Linux 中,互 斥锁并不占用任何资源,因此 Linux Threads
中的 pthread_mutex_destroy()
除了检查锁状态以外(锁定状态则返回 EBUSY)没有其他动作。
实例11:两个2000万加锁相加得4000万
define 20000000
typedef struct{
int val;
pthread_mutex_t mutex;
}Data_t;
void* threadFunc(void* p)
{
int i;
Data_t* threadInfo = (Data_t*)p;
for(i=0; i<N; i++)
{
pthread_mutex_lock(&threadInfo->mutex);
threadInfo->val += 1;
pthread_mutex_unlock(&threadInfo->mutex);
}
}
int main()
{
pthread_t pthid;
int ret;
Data_t threadInfo;
threadInfo.val = 0;
ret = pthread_mutex_init(&threadInfo.mutex, NULL);
THREAD_ERROR_CHECK(ret, "pthread_mutex_init");
ret = pthread_create(&pthid, NULL, threadFunc,&threadInfo);
THREAD_ERROR_CHECK(ret, "pthread_create");
int i;
for(i=0; i<N; i++)
{
pthread_mutex_lock(&threadInfo.mutex);
threadInfo.val += 1;
pthread_mutex_unlock(&threadInfo.mutex);
}
pthread_join(pthid, NULL); -等待子线程
printf("I am main thread, val = %d", threadInfo.val);
return 0;
}
为了获得精确时间,使用 gettimeofday()
int gettimeofday(struct timeval*tv, struct timezone *tz);
gettimeofday()
会把目前的时间用 tv
结构体返回,当地时区的信息则放到 tz
所指的结构中
gettimeofday()
函数中 tv
或者 tz
都可以为空。如果为空则就不返回其对应的结构体。
程序加上时间计算:
#define N 20000000
typedef struct{
int val;
pthread_mutex_t mutex;
}Data_t;
void* threadFunc(void* p)
{
int i;
Data_t* threadInfo = (Data_t*)p;
for(i=0; i<N; i++)
{
pthread_mutex_lock(&threadInfo->mutex);
threadInfo->val += 1;
pthread_mutex_unlock(&threadInfo->mutex);
}
}
int main()
{
pthread_t pthid;
int ret;
Data_t threadInfo;
threadInfo.val=0;
ret = pthread_mutex_init(&threadInfo.mutex, NULL);
THREAD_ERROR_CHECK(ret, "pthread_mutex_init");
struct timeval start, end;
gettimeofday(&start, NULL);
ret = pthread_create(&pthid, NULL, threadFunc, &threadInfo);
THREAD_ERROR_CHECK(ret, "pthread_create");
int i;
for(i=0; i<N; i++)
{
pthread_mutex_lock(&threadInfo.mutex);
threadInfo.val += 1;
pthread_mutex_unlock(&threadInfo.mutex);
}
pthread_join(pthid, NULL); -等待子线程
gettimeofday(&end, NULL);
printf("I am main thread, val = %d,use time = %ld us\n",threadInfo.val, (end.tv_sec-start.tv_sec)*1000000+end.tv_usec-start.tv_usec);
return 0;
}
运行结果:
I am main thread, val = 40000000, use time= 589056 us
I am main thread, val = 40000000, use time= 579149 us
I am main thread, val = 40000000, use time= 585380 us
I am main thread, val = 40000000, use time= 545059 us
I am main thread, val = 40000000, use time= 576452 us
大约在0.57秒左右
而我们使用信号量时,得到结果
result = 40000000, use time = 37515421 us
result = 40000000, use time = 32912636 us
大约在36秒左右
2. 锁操作
加锁 | int pthread_mutex_lock(pthread_mutex_t *mutex); |
---|---|
解锁 | int pthread_mutex_unlock(pthread_mutex_t *mutex); |
测试加锁 | int pthread_mutex_trylock(pthread_mutex_t *mutex); |
① pthread_mutex_lock
:
加锁,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。对于普通锁类型,解锁者可以是同进程内任何线程;而检错锁则必须由加锁者解锁才有效,否则返回 EPERM;对于嵌套锁,文档和实现要求必须由加锁者解锁,但实验结果表明并没有这种限制, 这个不同目前还没有得到解释。在同一进程中的线程,如果加锁后没有解锁,则任何其他线程都无法再获得锁
② pthread_mutex_unlock
:
根据不同的锁类型,实现不同的行为:
- 对于快速锁,
pthread_mutex_unlock
解除锁定; - 对于递规锁,
pthread_mutex_unlock
使锁上的引用计数减 1; - 对于检错锁,如果锁是当前线程锁定的,则解除锁定,否则什么也不做。
③ pthread_mutex_trylock
:
(尝试加锁)语义与 pthread_mutex_lock()
类似,不同的是在锁已经被占据时返回 EBUSY 而不是挂起等待。
实例12:使用pthread_mutex_trylock
获得返回值 EBUSY
typedef struct{
pthread_mutex_t mutex;
}Data_t;
void* threadFunc(void* p)
{
Data_t* pArg = (Data_t*)p;
int ret = pthread_mutex_trylock(&pArg->mutex);
CHILD_THREAD_ERROR_CHECK(ret, "pthread_mutex_trylock");
printf("child lock success\n");
pthread_exit(NULL);
}
int main()
{
Data_t threadInfo;
pthread_t pthid;
pthread_mutex_init(&threadInfo.mutex, NULL);
pthread_create(&pthid, NULL, threadFunc, &threadInfo);
pthread_mutex_lock(&threadInfo.mutex); -删去这句加锁成功
printf("main thread lock success\n");
int ret = pthread_join(pthid, NULL);
THREAD_ERROR_CHECK(ret, "pthread_join");
return 0;
}
运行结果:
main thread lock success
pthread_mutex_trylock:Device or resource busy
易错点:
1. 如果主线程加锁了,子线程解锁,是否可以解锁?
2. cancel以后如何清理锁资源?
问题1:
typedef struct{
pthread_mutex_t mutex;
}Data_t;
void* threadFunc(void* p)
{
Data_t* pArg = (Data_t*)p;
pthread_mutex_unlock(&pArg->mutex);
pthread_mutex_lock(&pArg->mutex);
printf("child lock success\n");
pthread_exit(NULL);
}
int main()
{
Data_t threadInfo;
pthread_t pthid;
pthread_mutex_init(&threadInfo.mutex, NULL);
pthread_create(&pthid, NULL, threadFunc, &threadInfo);
pthread_mutex_lock(&threadInfo.mutex);
printf("main thread lock success\n");
int ret = pthread_join(pthid, NULL);
THREAD_ERROR_CHECK(ret, "pthread_join");
return 0;
}
答:如果主线程加锁了,子线程可以解锁
问题2:
创建两个子线程,每个子线程都对锁进行加锁
typedef struct{
pthread_mutex_t mutex;
}Data_t;
void cleanup(void *p)
{
pthread_mutex_unlock((pthread_mutex_t*)p);
printf("unlock success\n");
}
void* threadFunc(void* p)
{
Data_t* pArg = (Data_t*)p;
pthread_mutex_lock(&pArg->mutex);
pthread_cleanup_push(cleanup, &pArg->mutex);
printf("child lock success\n");
sleep(2);
pthread_cleanup_pop(1); -和lock配对
printf("you can't see me\n");
pthread_exit(NULL);
}
int main(int argc,char* argv[])
{
Data_t threadInfo;
pthread_t pthid, pthid1;
pthread_mutex_init(&threadInfo.mutex,NULL);
pthread_create(&pthid,NULL,threadFunc, &threadInfo);
pthread_create(&pthid1,NULL,threadFunc, &threadInfo);
int ret;
sleep(1);
ret = pthread_cancel(pthid);
THREAD_ERROR_CHECK(ret, "pthread_cancel");
ret = pthread_cancel(pthid1);
THREAD_ERROR_CHECK(ret, "pthread_cancel");
long threadRet;
ret = pthread_join(pthid1, (void**)&threadRet);
THREAD_ERROR_CHECK(ret, "pthread_join");
printf("pthid1 ret = %ld\n", threadRet);
ret = pthread_join(pthid, (void**)&threadRet);
THREAD_ERROR_CHECK(ret, "pthread_join");
printf("pthid ret = %ld\n", threadRet);
return 0;
}
运行结果:
child lock success
unlock success
pthid1 ret = -1
child lock success
unlock success
pthid ret = -1
当一个线程在加锁中间的时候被 cancel,离开时要解锁,否认已经加锁的进程永远留着不被 cancel 掉,此时使用pthread_cleanup_push
,pthread_cleanup_pop
,进行清理。(一定要传入地址)
这是加锁解锁会被 cancel,要考虑的。
要注意的点:
- 主线程加锁了,子线程可以解锁,但不常用
- 线程清理锁的资源,线程是正常结束还是被终止,都要保证线程占有的资源的释放
3. 互斥锁属性设置
互斥锁的属性在创建锁的时候指定,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同也就是是否阻塞等待。有三个值可供选择:
PTHREAD_MUTEX_TIMED_NP
,这是缺省值(直接写 NULL 就是表示这个缺省值),也就是普通锁(或快速锁)。当一个线程加锁以后,其余请求锁的线程将形成一个阻塞等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
example:初始化一个快速锁
pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL);
PTHREAD_MUTEX_RECURSIVE
,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次 unlock 解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
example:初始化一个嵌套锁
pthread_mutex_t lock;
pthread_mutexattr_t mutexattr; 定义一个锁属性
pthread_mutexattr_init(&mutexattr); -初始化锁属性
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_RECURSIVE); -设置锁属性
pthread_mutex_init(&lock, &mutexattr); -初始化锁
实例13:设置锁的属性,允许同一个线程对同一个锁成功获得多次
int main(int argc,char* argv[])
{
pthread_mutex_t mutex;
pthread_mutexattr_t mattr;
int ret;
ret = pthread_mutexattr_init(&mattr);
THREAD_ERROR_CHECK(ret,"pthread_mutexattr_init");
pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_RECURSIVE); -允许同一个线程对同一把锁加锁多次
pthread_mutex_init(&mutex, &mattr);
pthread_mutex_lock(&mutex);
ret = pthread_mutex_lock(&mutex);
THREAD_ERROR_CHECK(ret, "pthread_mutex_lock");
printf("you can see me\n");
return 0;
}
PTHREAD_MUTEX_ERRORCHECK
,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK
, 否则与PTHREAD_MUTEX_TIMED
类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。如果锁的类型是快速锁,一个线程加锁之后,又加锁,则此时就是死锁。
example:初始化一个嵌套锁
pthread_mutex_t lock;
pthread_mutexattr_t mutexattr;
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_ERRORCHECK); -设置锁属性
pthread_mutex_init(&lock, &mutexattr);
实例14:设置锁的属性,在对线程两次加锁就会报错返回
int main(int argc,char* argv[])
{
pthread_mutex_t mutex;
pthread_mutexattr_t mattr;
int ret;
ret = pthread_mutexattr_init(&mattr);
THREAD_ERROR_CHECK(ret, "pthread_mutexattr_init");
pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_ERRORCHECK);//允许同一个线程对同一把锁加锁多次
pthread_mutex_init(&mutex, &mattr);
pthread_mutex_lock(&mutex);
ret = pthread_mutex_lock(&mutex);
THREAD_ERROR_CHECK(ret, "pthread_mutex_lock");
printf("you can see me\n");
return 0;
}
运行结果:
pthread_mutex_lock:Resource deadlock avoided
实例15:(进程间使用互斥锁)两个数各加2000万,两个进程使用共享内存,并使用互斥锁
typedef struct{
int val;
pthread_mutex_t mutex;
}Data_t,*pData_t;
int main(int argc,char* argv[])
{
int semArrId=semget(1000,1,IPC_CREAT|0600);
ERROR_CHECK(semArrId,-1,"semget");
int shmid;
shmid=shmget(1000,1<<20,IPC_CREAT|0600);
ERROR_CHECK(shmid,-1,"shmget");
pData_t p;
p=(pData_t)shmat(shmid,NULL,0);
ERROR_CHECK(p,(pData_t)-1,"shmat");
p->val=0;
pthread_mutexattr_t mattr;
pthread_mutexattr_init(&mattr);
pthread_mutexattr_setpshared(&mattr,PTHREAD_PROCESS_SHARED);//设置为进程共享
int ret=pthread_mutex_init(&p->mutex,&mattr);
THREAD_ERROR_CHECK(ret,"pthread_mutex_init");
struct timeval start,end;
gettimeofday(&start,NULL);
if(!fork())
{
int i;
for(i=0;i<N;i++)
{
//加锁
pthread_mutex_lock(&p->mutex);
p->val+=1;
pthread_mutex_unlock(&p->mutex);
//解锁
}
}else{
int i;
for(i=0;i<N;i++)
{
pthread_mutex_lock(&p->mutex);
p->val+=1;
pthread_mutex_unlock(&p->mutex);
}
wait(NULL);
gettimeofday(&end,NULL);
printf("result=%d,use time=%ld\n",p->val,(end.tv_sec-start.tv_sec)*1000000+end.tv_usec-start.tv_usec);
}
return 0;
}
4.加锁注意事项
如果线程在加锁后 解锁前被取消,锁将永远保持锁定状态,因此如果在关键区段内有取消点存在, 则必须在退出回调函数 pthread_cleanup_push
/ pthread_cleanup_pop
中解锁。同时不应该在信号处理函数中使用互斥锁,否则容易造成死锁。
死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
① 死锁产生的原因:
- 系统资源的竞争
系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁 - 进程运行推进顺序不合
进程在运行过程中,请求和释放资源的顺序不当,会导致死锁
② 死锁的四个必要条件:
- 互斥条件
一个资源每次只能被一个进程使用,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待 - 请求与保持条件
进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放 - 不可剥夺条件
进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放) - 循环等待条件
若干进程间形成首尾相接循环等待资源的关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足, 就不会发生死锁。
③ 死锁的预防
我们可以通过破坏死锁产生的 4 个必要条件来预防死锁,由于资源互斥是资源使用的固有特性是无法改变的。
- 破坏“不可剥夺”条件:
一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。 - 破坏”请求与保持条件“:
第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。 第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。 - 破坏“循环等待”条件:
采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的, 稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。
**5. 互斥锁实例**优先级反转: 当一个高优先级任务通过信号量机制访问共享资源时,该信号量已被一低优先级任务占有,因此造成高优先级任务被许多具有较低优先级任务阻塞,实时性难以得到保证
优先级反转信息
Example:火车站售票(此处不加锁,则会出现卖出负数票的情况)
typedef struct{
int tickets;
pthread_mutex_t mutex;
}Data_t;
void cleanup(void *p)
{
pthread_mutex_unlock((pthread_mutex_t*)p);
printf("unlock success\n");
}
void* saleWindows1(void* p)
{
Data_t* pArg = (Data_t*)p;
int i = 0;
while(1)
{
pthread_mutex_lock(&pArg->mutex);
if(pArg->tickets>0)
{
//printf("I am saleWindows1 start sale,%d\n",pArg->tickets); //打印过程
pArg->tickets--;
i++;
//printf("I am saleWindows1 sale finish,%d\n",pArg->tickets); //打印过程
pthread_mutex_unlock(&pArg->mutex);
}else{
pthread_mutex_unlock(&pArg->mutex);
printf("I am saleWindows1,%d\n",i);
break;
}
}
return NULL;
}
void* saleWindows2(void* p)
{
Data_t* pArg=(Data_t*)p;
int i=0;
while(1)
{
pthread_mutex_lock(&pArg->mutex);
if(pArg->tickets>0)
{
//printf("I am saleWindows2 start sale,%d\n",pArg->tickets); //打印过程
pArg->tickets--;
i++;
//printf("I am saleWindows2 sale finish,%d\n",pArg->tickets); //打印过程
pthread_mutex_unlock(&pArg->mutex);
}else{
pthread_mutex_unlock(&pArg->mutex);
printf("I am saleWindows2,%d\n",i);
break;
}
}
return NULL;
}
int main(int argc,char* argv[])
{
Data_t threadInfo;
threadInfo.tickets=20000000;
pthread_t pthid,pthid1;
pthread_mutex_init(&threadInfo.mutex,NULL);
pthread_create(&pthid,NULL,saleWindows1,&threadInfo);
pthread_create(&pthid1,NULL,saleWindows2,&threadInfo);
int ret;
long threadRet;
ret=pthread_join(pthid1,(void**)&threadRet);
THREAD_ERROR_CHECK(ret,"pthread_join");
ret=pthread_join(pthid,(void**)&threadRet);
THREAD_ERROR_CHECK(ret,"pthread_join");
printf("sale over\n");
return 0;
}
运行结果:
I am saleWindows1,10177164
I am saleWindows2,9822836
sale over
I am saleWindows1,10050987
I am saleWindows2,9949013
sale over
I am saleWindows2,10001719
I am saleWindows1,9998281
sale over
I am saleWindows1,10092739
I am saleWindows2,9907261
sale over
总结:
线程互斥 mutex:加锁步骤如下:
4.2. 线程的同步
进程间通信 | 进程间同步 |
---|---|
管道 共享内存 消息队列 信号量 (信号) | 管道 消息队列 信号量 |
4.2.1 条件变量
条件变量是利用线程间共享的全局变量进行同步的一种机制。主要包括两个动作:一个线程等待条件变量的条件成立而挂起;另一个线程使条件成立(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
1. 创建和注销
条件变量和互斥锁一样,都有静态、动态两种创建方式:
① 创建条件变量
静态方式:
使用 PTHREAD_COND_INITIALIZER
常量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
动态方式:
调用 pthread_cond_init()
函数:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
作用:初始化一个条件变量
参数:
cond
是一个指向结构pthread_cond_t
的指针cond_attr
是一个指向结构pthread_condattr_t
的指针,值通常为 NULL,且被忽略
(尽管 POSIX 标准中为条件变量定义了属性,但在 Linux Threads 中没有实现)
② 注销条件变量
注销一个条件变量需要调用 pthread_cond_destroy()
,只有在没有线程在该条件变量上等待的时候能注销这个条件变量,否则返回 EBUSY。因为 Linux 实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。
调用 pthread_cond_destroy()
函数:
int pthread_cond_destroy(pthread_cond_t *cond);
作用:注销一个条件变量
参数 cond
:是一个指向结构 pthread_cond_t
的指针
2. 等待和激发
等待
等待条件有两种方式:无条件等待 pthread_cond_wait()
和 计时等待 pthread_cond_timedwait()
:
无条件等待 pthread_cond_wait()
— 是cancel点
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
计时等待 pthread_cond_timedwait()
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
作用:表示经历 abstime
段时间后,即使条件变量不满足,阻塞也被解除。
线程解开 mutex
指向的锁并被条件变量 cond
阻塞。
无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求 pthread_cond_wait()
(或 pthread_cond_timedwait()
,下同)的竞争条件(Race Condition)。
mutex
互斥锁必须是普通锁(PTHREAD_MUTEX_TIMED_NP),且在调用 pthread_cond_wait()
前必须由本 线程加锁(pthread_mutex_lock()
),而在更新条件等待队列以前,mutex
保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开 pthread_cond_wait()
之前,mutex
将被重新加锁,以与进入 pthread_cond_wait()
前的加锁动作对应。
(也就是说在做 pthread_cond_wait
之前,往往要用 pthread_mutex_lock
进行加锁,而调用 pthread_cond_wait
函数会将锁解开,然后将线程挂起阻塞。直 到条件被 pthread_cond_signal
激发,再将锁状态恢复为锁定状态,最后再用 pthread_mutex_unlock
进行解锁)
激发
激发条件有两种形式
pthread_cond_signal()
:激活 一个 等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个**
pthread_cond_broadcast()
:则激活 所有 等待线程
超时是指 当前时间 + 多长时间超时
3. 其他
pthread_cond_wait()
和 pthread_cond_timedwait()
都被实现为取消点,也就是说如果 pthread_cond_wait()
被取消,则退出阻塞,然后将锁状态恢复,则此时 mutex 是保持锁定状态的,而当前线程已经被取消掉,那么解锁的操作就会得不到执行,此时锁得不到释放,就会造成死锁,因而需要定义退出回调函数来为其解锁。
实例16:子线程等待,主线程进行
一般流程
pthread_mutex_lock(&mutex);
prhtead_cond_wait(&cond, &mutex);
pthread_mutex_unlock(&mutex);
上半部
1.排队在对应的cond
2.解锁
3.睡觉
下半部
1.被唤醒
2.加锁
3.返回
typedef struct{
pthread_mutex_t mutex;
pthread_cond_t cond;
}Data_t,*pData_t;
void* threadFunc(void* p)
{
pData_t pArg=(pData_t)p;
pthread_mutex_lock(&pArg->mutex);
pthread_cond_wait(&pArg->cond,&pArg->mutex); -(条件变量,锁)
pthread_mutex_unlock(&pArg->mutex);
printf("I am child thread wake up\n");
pthread_exit(NULL);
}
int main(int argc,char* argv[])
{
Data_t threadInfo;
pthread_mutex_init(&threadInfo.mutex,NULL); -初始化锁
pthread_cond_init(&threadInfo.cond,NULL); -初始化条件变量
pthread_t pthid; -定义线程id
pthread_create(&pthid,NULL,threadFunc,&threadInfo); -创建线程
usleep(5000); -如果不加,主线程已经发信号 子进程都还没有创建条件变量
pthread_cond_signal(&threadInfo.cond); -让条件变量成立 一瞬间成立
pthread_join(pthid,NULL);
printf("I am main thread\n");
return 0;
}
内核在调度时候时间片分给线程,为什么内核里都是内核线程,不叫内核进程?所有的内核的线程共用一块地址空间,内核所有的线程是共享的,就像一个进程。
实例17:两个线程 pthread_cond_wait
同一条件变量
typedef struct{
pthread_mutex_t mutex;
pthread_cond_t cond;
}Data_t,*pData_t;
void cleanup(void *p)
{
pthread_mutex_unlock((pthread_mutex_t*)p);
printf("unlock ok\n");
}
void* threadFunc(void* p)
{
pData_t pArg=(pData_t)p;
pthread_mutex_lock(&pArg->mutex);
pthread_cleanup_push(cleanup,&pArg->mutex);
pthread_cond_wait(&pArg->cond,&pArg->mutex);
printf("wait return\n");
pthread_cleanup_pop(1);
printf("I am child thread wake up\n");
pthread_exit(NULL);
}
#define N 2 -两个线程
int main(int argc,char* argv[])
{
Data_t threadInfo;
pthread_mutex_init(&threadInfo.mutex,NULL);
pthread_cond_init(&threadInfo.cond,NULL);
pthread_t pthid[N];
int i;
for(i=0;i<N;i++)
{
pthread_create(pthid+i,NULL,threadFunc,&threadInfo);
}
int ret;
for(i=0;i<N;i++)
{
ret=pthread_cancel(pthid[i]);
THREAD_ERROR_CHECK(ret,"pthread_cancel");
}
for(i=0;i<N;i++)
{
pthread_join(pthid[i],NULL);
}
printf("I am main thread\n");
return 0;
}
运行结果:
unlock ok
unlock ok
I am main thread
实例18:两个线程 pthread_cond_wait
同一条件变量并激活(pthread_cond_signal)
typedef struct{
pthread_mutex_t mutex;
pthread_cond_t cond;
}Data_t,*pData_t;
void cleanup(void *p)
{
pthread_mutex_unlock((pthread_mutex_t*)p);
printf("unlock ok\n");
}
void* threadFunc(void* p)
{
pData_t pArg=(pData_t)p;
pthread_mutex_lock(&pArg->mutex);
pthread_cleanup_push(cleanup,&pArg->mutex);
printf("start wait\n");
pthread_cond_wait(&pArg->cond,&pArg->mutex);
printf("wait return\n");
pthread_cleanup_pop(1);
printf("I am child thread wake up\n");
pthread_exit(NULL);
}
#define N 2
int main(int argc,char* argv[])
{
Data_t threadInfo;
pthread_mutex_init(&threadInfo.mutex,NULL);
pthread_cond_init(&threadInfo.cond,NULL);
pthread_t pthid[N];
int i;
for(i=0;i<N;i++)
{
pthread_create(pthid+i,NULL,threadFunc,&threadInfo);
}
int ret;
sleep(3);
for(i=0;i<N;i++)
{
ret=pthread_cond_signal(&threadInfo.cond);
THREAD_ERROR_CHECK(ret,"pthread_cond_signal");
}
for(i=0;i<N;i++)
{
pthread_join(pthid[i],NULL);
}
printf("I am main thread\n");
return 0;
}
运行结果:
start wait
start wait
wait return
unlock ok
I am child thread wake up
wait return
unlock ok
I am child thread up
I am main thread
实例19:使用 pthread_cond_timedwait
,超时唤醒
typedef struct{
pthread_mutex_t mutex;
pthread_cond_t cond;
}Data_t,*pData_t;
void* threadFunc(void* p)
{
pData_t pArg=(pData_t)p;
struct timespec t;
t.tv_sec=time(NULL)+3; -这里是绝对时间,即相对于现在的时间,不可以是相对时间(3),会超时
t.tv_nsec=0;
int ret;
pthread_mutex_lock(&pArg->mutex);
ret=pthread_cond_timedwait(&pArg->cond,&pArg->mutex,&t);
pthread_mutex_unlock(&pArg->mutex);
printf("I am child thread wake up,ret=%d\n",ret);
pthread_exit(NULL);
}
int main(int argc,char* argv[])
{
Data_t threadInfo;
pthread_mutex_init(&threadInfo.mutex,NULL);
pthread_cond_init(&threadInfo.cond,NULL);
pthread_t pthid;
pthread_create(&pthid,NULL,threadFunc,&threadInfo);
sleep(1);
pthread_cond_signal(&threadInfo.cond);
pthread_join(pthid,NULL);
printf("I am main thread\n");
return 0;
}
使用 t.tv_sec=3
(相对时间)时,运行结果:
I am child thread wake up,ret=0
I am main thread
这正常运行结果:
I am child thread wake up,ret=0
I am main thread
实例20:火车售票,利用条件变量,当火车票卖完的时候,再重新设置票数为 10
(改进 互斥锁实例:火车站售票)
typedef struct{
int tickets;
pthread_mutex_t mutex;
pthread_cond_t cond;
}Data_t,*pData_t;
void cleanup(void *p)
{
pthread_mutex_unlock((pthread_mutex_t*)p);
printf("unlock success\n");
}
void* saleWindows1(void* p)
{
Data_t* pArg=(Data_t*)p;
int i=0;
while(1)
{
pthread_mutex_lock(&pArg->mutex);
if(pArg->tickets>0)
{
printf("I am saleWindows1 start sale,%d\n",pArg->tickets);
pArg->tickets--;
i++;
if(!pArg->tickets)
{
pthread_cond_signal(&pArg->cond);
}
printf("I am saleWindows1 sale finish,%d\n",pArg->tickets);
pthread_mutex_unlock(&pArg->mutex);
sleep(1);
}else{
pthread_mutex_unlock(&pArg->mutex);
printf("I am saleWindows1,%d\n",i);
break;
}
}
return NULL;
}
void* saleWindows2(void* p)
{
Data_t* pArg=(Data_t*)p;
int i=0;
while(1)
{
pthread_mutex_lock(&pArg->mutex);
if(pArg->tickets>0)
{
printf("I am saleWindows2 start sale,%d\n",pArg->tickets);
pArg->tickets--;
i++;
if(!pArg->tickets)
{
pthread_cond_signal(&pArg->cond);
}
printf("I am saleWindows2 sale finish,%d\n",pArg->tickets);
pthread_mutex_unlock(&pArg->mutex);
sleep(1);
}else{
pthread_mutex_unlock(&pArg->mutex);
printf("I am saleWindows2,%d\n",i);
break;
}
}
return NULL;
}
void* setTickets(void* p)
{
Data_t* pArg=(pData_t)p;
pthread_mutex_lock(&pArg->mutex);
if(pArg->tickets>0)
{
pthread_cond_wait(&pArg->cond,&pArg->mutex);
}
pArg->tickets=20;
pthread_mutex_unlock(&pArg->mutex);
}
int main(int argc,char* argv[])
{
Data_t threadInfo;
threadInfo.tickets=20;
pthread_t pthid,pthid1,pthid2;
pthread_mutex_init(&threadInfo.mutex,NULL);
pthread_cond_init(&threadInfo.cond,NULL);
pthread_create(&pthid,NULL,saleWindows1,&threadInfo);
pthread_create(&pthid1,NULL,saleWindows2,&threadInfo);
pthread_create(&pthid2,NULL,setTickets,&threadInfo); -重新设置票量(放票)
int ret;
long threadRet;
ret=pthread_join(pthid1,(void**)&threadRet);
THREAD_ERROR_CHECK(ret,"pthread_join");
ret=pthread_join(pthid,(void**)&threadRet);
THREAD_ERROR_CHECK(ret,"pthread_join");
printf("sale over\n");
return 0;
}
标准的条件变量的使用方式:
回调函数保护 → 等待条件前锁定 → pthread_cond_wait()
返回后解锁
条件变量机制和互斥锁一样,不能用于信号处理中,在信号处理函数中调用 pthread_cond_signal()
或者 pthread_cond_broadcast()
很可能引起死锁。
实例21:pthread_cond_broadcast 两个唤醒(激活)
typedef struct{
pthread_mutex_t mutex;
pthread_cond_t cond;
}Data_t,*pData_t;
void cleanup(void *p)
{
pthread_mutex_unlock((pthread_mutex_t*)p);
printf("unlock ok\n");
}
void* threadFunc(void* p)
{
pData_t pArg=(pData_t)p;
pthread_mutex_lock(&pArg->mutex);
pthread_cleanup_push(cleanup,&pArg->mutex);
printf("start wait\n");
pthread_cond_wait(&pArg->cond,&pArg->mutex);
printf("wait return\n");
pthread_cleanup_pop(1);
printf("I am child thread wake up\n");
pthread_exit(NULL);
}
#define N 2
int main(int argc,char* argv[])
{
Data_t threadInfo;
pthread_mutex_init(&threadInfo.mutex,NULL);
pthread_cond_init(&threadInfo.cond,NULL);
pthread_t pthid[N];
int i;
for(i=0;i<N;i++)
{
pthread_create(pthid+i,NULL,threadFunc,&threadInfo);
}
int ret;
sleep(3);
ret=pthread_cond_broadcast(&threadInfo.cond);
THREAD_ERROR_CHECK(ret,"pthread_cond_broadcast");
for(i=0;i<N;i++)
{
pthread_join(pthid[i],NULL);
}
printf("I am main thread\n");
return 0;
}
4.3. 生产者消费者问题
代码:
#define BUFFER_SIZE 16 - 表示一次最多可以不间断的生产 16 个产品
struct prodcons {
int buffer[BUFFER_SIZE]; -数据
pthread_mutex_t lock; -加锁
int readpos, writepos; -读 pos 写位置
pthread_cond_t notempty; -不空,可以读
pthread_cond_t notfull; -不满,可以写
};
/* 初始化*/
void init(struct prodcons * b)
{
pthread_mutex_init(&b->lock, NULL); -初始化锁
pthread_cond_init(&b->notempty, NULL); -初始化条件变量
pthread_cond_init(&b->notfull, NULL); -初始化条件变量
b->readpos = 0; -初始化读取位置从 0 开始
b->writepos = 0; -初始化写入位置从 0 开始
}
-销毁操作
void destroy(struct prodcons *b)
{
pthread_mutex_destroy(&b->lock);
pthread_cond_destroy(&b->notempty);
pthread_cond_destroy(&b->notfull);
}
void put(struct prodcons * b, int data) -生产者
{
pthread_mutex_lock(&b->lock);
while ((b->writepos + 1) % BUFFER_SIZE == b->readpos) { -判断是不是满了
printf("wait for not full\n");
pthread_cond_wait(&b->notfull, &b->lock); -此时为满,不能生产,等待不满的信号
}
-下面表示还没有满,可以进行生产
b->buffer[b->writepos] = data;
b->writepos++; -写入点向后移一位
if (b->writepos >= BUFFER_SIZE) b->writepos = 0; -如果到达最后,就再转到开头
pthread_cond_signal(&b->notempty); -此时有东西可以消费,发送非空的信号
pthread_mutex_unlock(&b->lock);
}
int get(struct prodcons * b)-消费者
{
pthread_mutex_lock(&b->lock);
while (b->writepos == b->readpos) { -判断是不是空
printf("wait for not empty\n");
pthread_cond_wait(&b->notempty, &b->lock); -此时为空,不能消费,等待非空信号
}
-下面表示还不为空,可以进行消费
int data = b->buffer[b->readpos];
b->readpos++; -读取点向后移一位
if (b->readpos >= BUFFER_SIZE) b->readpos = 0; -如果到达最后,就再转到开头
pthread_cond_signal(&b->notfull); -此时可以进行生产,发送不满的信号
pthread_mutex_unlock(&b->lock);
return data;
}
/*--------------------------------------------------------*/
#define OVER (-1) -定义结束标志
struct prodcons buffer; -定义全局变量
/*--------------------------------------------------------*/
void * producer(void * data)
{
int n = 0;
for (; n < 50; n++)
{
printf(" put-->%d\n", n);
put(&buffer, n);
}
put(&buffer, OVER);
printf("producer stopped!\n");
pthread_exit(NULL);
}
/*--------------------------------------------------------*/
void * consumer(void * data)
{
while (1)
{
int d = get(&buffer);
if (d == OVER ) break;
printf(" %d-->get\n", d);
}
printf("consumer stopped!\n");
pthread_exit(NULL);
}
/*--------------------------------------------------------*/
int main(void) {
pthread_t th_a, th_b;
init(&buffer);
pthread_create(&th_a, NULL, producer, 0);
pthread_create(&th_b, NULL, consumer, 0);
pthread_join(th_a, NULL);
pthread_join(th_b, NULL);
destroy(&buffer);
return 0;
}
5. 线程安全和线程属性
5.1. 线程安全
先看一段代码:
void* threadFunc(void* p1) {
time_t now;
time(&now);
char *p=ctime(&now);
printf("I am child thread p=%s\n",p);
sleep(3);
printf("I am child thread p=%s\n",p);
}
int main() {
pthread_t pthid;
pthread_create(&pthid,NULL,threadFunc,NULL);
sleep(1);
time_t now;
time(&now);
char *p=ctime(&now);
printf("I am main thread p=%s\n",p);
pthread_join(pthid,NULL);
return 0;
}
线程安全:
如果一个函数能够安全的同时被多个线程调用而得到正确的结果,那么,我们说这个函
数是线程安全的。简单来说线程安全就是多个线程并发同一段代码时,不会出现不同的结果,我们就可以说该线程是安全的;
线程安全产生的原因:
大多是因为对全局变量和静态变量的操作。
可重入函数:
访问时有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入函数。简而言之,可重入函数,描述的是函数被多次调用但是结果具有可再现性。
可重入并不一定要是多线程的。可重入只关注一个结果可再现性。
在 APUE 中,可函数可重入的概念最先是在讲 signal 的 handler 的时候提出的。此时进程(线程)正在执行函数 fun()
,在函数 fun()
还未执行完的时候,突然进程接收到一个信号 sig, 此时,需要暂停执行 fun()
,要转而执行 sig 信号的处理函数 sig_handler()
,那么,如果在 sig_handler()
中,也恰好调用了函数 fun()
信号的处理是以软终端的形式进行的,那么,当 sig_handler()
执行完返回之后,CPU 会继续从 fun()
被打断的地方往下执行。
这里讲的比较特殊,最好的情况是,进程中调用了 fun()
函数,信号处理函数 sig_handle()
中也调用了 fun()。如果 fun()函数是可重入的,那么,多次调用 fun()
函数就具有可再现性。从而, 两次调用 fun()
的结果是正确的预期结果。非可重入函数,则恰好相反。
- 可重入概念只和函数访问的变量类型有关,和是否使用锁没有关系。
- 线程安全,描述的是函数能同时被多个线程安全的调用,并不要求调用函数的结果具有可再现性。 也就是说,多个线程同时调用该函数,允许出现互相影响的情况,这种情况的出现需要某些机制比如互斥锁来支持,使之安全。
- 可重入函数是线程安全函数的一种,其特点在于它们被多个线程调用时,不会引用任何共享数据。
- 线程安全是在多个线程情况下引发的,而可重入函数可以在只有一个线程的情况下来说。
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放 则会产生死锁,因此是不可重入的。
- 线程安全函数能够使不同的线程访问同一块地址空间,而可重入函数要求不同的执行流对数据的操作互不影响使结果是相同的。
线程不安全函数 — 不可重入函数
线程安全函数 — 可重入函数
不可重入函数:不可以两个线程同时执行的函数(含有静态局部变量,用了静态局部变量,或者用了全局变量)(用了静态局部变量,或者用了全局变量,是不安全的)
可重入函数:n个线程可以同时执行的函数
(并发操作要加锁)
实例23:主线程子线程都获得当前时间
void* threadFunc(void* p1)
{
time_t now;
time(&now);
char *p=ctime(&now); -第一次执行ctime 的一个空间
printf("I am child thread p=%s\n",p);
sleep(3);
printf("I am child thread p=%s\n",p);
}
int main()
{
pthread_t pthid;
pthread_create(&pthid,NULL,threadFunc,NULL);
sleep(1);
time_t now;
time(&now);
char *p=ctime(&now); -第二次执行ctime 是共享这个空间的,被改变了
printf("I am main thread p=%s\n",p);
pthread_join(pthid,NULL);
return 0;
}
运行结果:
I am child thread p=Tue Mar 17 23:13:26 2020 (子进程执行打印p时间,睡觉3秒)
I am main thread p=Tue Mar 17 23:13:27 2020 (子进程睡觉时间,主进程更改p时间,打印新p时间)
I am child thread p=Tue Mar 17 23:13:27 2020 (子进程继续,打印时间是被更改的新p时间)
我们引入线程安全
有两种ctime,一种 ctime_r
是安全的,多的参数buf,要自己定义一个空间给它,它把转换好的字符串时间直接填到buf里,这样子线程和主线程使用各自的空间
char *ctime(const time_t *timep);
char *ctime_r(const time_t *timep, char *buf);
实例24:主线程子线程都获得当前时间,并使用 ctime_r
安全函数
void* threadFunc(void* p1)
{
time_t now;
time(&now);
char buf[1000]={0}; -子线程自己的存时间空间
char *p=ctime_r(&now,buf);
printf("I am child thread p=%s\n",p);
sleep(3);
printf("I am child thread p=%s\n",p);
}
int main(int argc,char* argv[])
{
pthread_t pthid;
pthread_create(&pthid,NULL,threadFunc,NULL);
sleep(1);
time_t now;
time(&now);
char buf[1000]={0}; -主线程自己的存时间空间
char *p=ctime_r(&now,buf);
printf("I am main thread p=%s\n",p);
pthread_join(pthid,NULL);
return 0;
}
I am child thread p=Tue Mar 17 23:40:55 2020 (子进程执行打印p中自己的buf空间存的时间,睡觉3秒)
I am main thread p=Tue Mar 17 23:40:56 2020 (子进程睡觉时间,主进程也打印p中自己的buf空间存的时间)
I am child thread p=Tue Mar 17 23:40:55 2020 (子进程继续,打印时间还是之前p中自己的buf空间存的时间)
5.2. 线程的属性
pthread_create
的第二个参数 attr
是一个结构体指针,结构中的元素分别指定新线程的运行属性,各成员属性为:
-
__detachstate ,表示新线程是否与进程中其他线程脱离同步,如果置位则新线程不能用
pthread_join()
来同步,且在退出时自行释放所占用的资源。缺省为PTHREAD_CREATE_JOINABLE
状态。
这个属性也可以在线程创建并运行以后用pthread_detach()
来设置,而一旦设置为PTHREAD_CREATE_DETACHED
状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE
状态。(JOIN会返回错误码) -
__schedpolicy,表示新线程的调度策略,主要包括 :
SCHED_OTHER
(正常、非实时)、SCHED_RR
(实时、轮转法) 和SCHED_FIFO
(实时、先入先出)
三种,缺省为SCHED_OTHER
, 后两种调度策略仅对超级用户有效。运行时可以用pthread_setschedparam()
来改变。 -
__schedparam,一个
sched_param
结构,目前仅有一个 sched_priority 整型变量表示线程的运行优先级。这个参数仅当调度策略为实时(即 SCHED_RR 或 SCHED_FIFO)时才有效,并可以在运行时通过
pthread_setschedparam()
函数来改变,缺省为 0。 -
__inheritsched,有两种值可供选择:
PTHREAD_EXPLICIT_SCHED
和PTHREAD_INHERIT_SCHED
, 前者表示新线程使用显式指定调度策略和调度参数(即 attr 中的值),而后者表示继承调用者线程的值。 缺省为PTHREAD_EXPLICIT_SCHED
。 -
__scope,表示线程间竞争 CPU 的范围,也就是说线程优先级的有效范围。
POSIX 的标准中定义了 两个值:
PTHREAD_SCOPE_SYSTEM 和 PTHREAD_SCOPE_PROCESS
前者表示与系统中所有线程一 起竞争CPU 时间,后者表示仅与同进程中的线程竞争 CPU。目前 Linux 仅实现了 PTHREAD_SCOPE_SYSTEM 一值。
6. 总结
线程
- 什么是线程?线程和进程的区别?
(线程的概念,进程和线程的区别,理解,记住,面试考点)
进程是程序执行时的一个实例,是操作系统资源管理的基本单位。
线程是进程的一个执行流,更加接近执行体的概念,是CPU调度和分派的基本单位, 线程与同属一个进程的其他的线程共享进程所拥有的全部资源。
进程——资源分配的基本单位,线程——程序执行的最小单位。 - 线程的创建,退出,取消,等待。
NPTL线程库,编译时加-pthread,成功返回0,失败返回错误码
创建:pthread_create(&thid,NULL,threadFunc,pArg);
退出:pthread_exit((void*)retVal);
线程的取消:pthread_cancel(thid);
(向其他线程发送cancel信号,收到信号的线程运行 到cancel点,被取消。 )(理解cancel的异步性)
线程的等待:pthread_join(thid,(void**)retVal)
,用retVal
去接pthread_exit
的返回值,如果线程是被其他线程取消的,retVal
的值是-1。 (join的线程没有退出时,join是会阻塞的。)
man 7 pthreads
:查看man手册中线程的知识(对POSIX Thread有很详尽的介绍) - 线程的终止清理
-终止清理函数cleanupFunc(void *arg)
,在线程退出的时候清理线程的资源,–清理函数采用先入后出的栈结构管理。
-pthread_cleanup_push((void (*cleanupFunc)(void *),(void*)arg)
,清理函数入栈。
-pthread_cleanup_pop(1)
,弹栈,push和pop要配对使用,否则会编译报错。
-清理函数会执行的三种情况 :1. 被其他线程cancel; 2. 调用pthread_exit退出; 3. pthread_cleanup_pop(1)。
-清理函数不会执行的情况 :1.
线程通过return 
;返回; 2. 线程弹栈时调用pop(0);
线程间的关系
- 线程间的互斥
-什么是互斥?又称为间接制约关系,是指系统中的线程共享了某些资源,一次只允许 一个线程访问。当一个线程正在访问该资源时,其他线程必须要等待。
-pthread_mutex_t
互斥锁,多个线程共用某一个资源,为了防止访问的冲突,用 互斥锁保证线程能够实现对资源的独占。 (理解锁的作用,特性和使用方法。)
-pthread_mutex_t mutex
,拥有锁的线程才能对共享资源进行操作。
-pthread_mutex_init(&mutex,NULL);
锁的初始化。
-加锁:pthread_mutex_lock(&mutex);
如果锁已经被占用,再去加锁,会阻塞
-解锁:pthread_mutex_unlock(&mutex);
-pthread_mutex_trylock(&mutex);
锁已经被占用时,返回EBUSY,而不是挂起等待。
-锁的销毁:pthread_mutex_destroy(&mutex);
锁在解锁状态下才能被销毁。
-线程在结束的时候,把锁的资源释放掉(解锁),否则锁就会永远保持锁定的状态。pthread_cleanup_push(cleanup,&mutex);
(使用锁的时候要注意解锁,避免造成死锁。)
-锁的属性:
(不是重点)
-pthread_mutexattr_t mattr
;
-可以设置PTHREAD_PROCESS_SHARED
,进程间共享同一把锁
-普通锁:PTHREAD_MUTEX_TIMED_NP
。最常用
-嵌套锁:允许同一个线程对同一把锁加锁多次。并且通过多次unlock解锁。不同 的线程去竞争这把锁,在解锁的时候重新竞争。
-检错锁:同一个线程对同一把锁加锁两次,第二次加锁时返回一个DEADLK,避免 了最简单情况下的死锁。 - 线程间的同步
-定义:同步, 又称为直接制约关系。是指多个线程为了合作完成某一个任务,必须按照规定的某种先后顺序来运行。 (理解同步的概念,使用场景,使用方法。 )
-pthread_cond_t cond
;利用条件变量进行同步。
-初始化:pthread_cond_init(&cond,NULL);
-销毁:pthread_cond_destroy(&cond);
必须是没有线程等待在条件变量上,才能被销毁
等待条件变量:
-无条件等待:pthread_cond_wait(&cond,&mutex);
(理解为什么这里要使用锁,理解cond_wait的机制,上半部下半部。 )
-限时等待:pthread_cond_timedwait(&cond,&mutex,abstime);
-pthread_cond_wait(&cond,&mutex);
上半部:排队,解锁,睡眠(挂起,等待其他线 程用signal唤醒条件变量) 下半部:醒来,加锁
-激发条件变量:
-pthread_cond_signal(&cond)
,唤醒一个等待在条件变量的线程。
-pthread_cond_broadcast(&cond);唤醒所有等待在条件变量上的线程(万箭齐发)。
生产者消费者。模拟卖票,熟悉锁和条件变量的配合使用。 - 线程的安全 和 线程属性。
-线程安全:如果一个函数可以同时被多个线程调用,并且不出错,线程安全函数。 (需要理解线程安全和可重入的关系,使用锁可以变成线程安全的函数。)
-可重入函数:避免使用全局变量和静态局部变量。 ( 多线程里避免使用不可重入函数,自己编写的函数给线程使用时,避免使用全局变量和静态局部变量。)
-线程的属性:pthread_attr_t attr
; (不重要 )
-线程的分离属性:PTHREAD_CREATE_DETACHED
线程运行结束之后,自动把资源还给系统,不会被其他线程join,如果join此线程会返回错误码。
-默认是PTHREAD_CREATE_JOINABLE
可以被其他线程join