文章目录
使用环境:Ubuntu18.04
使用工具:VMWare workstations ,xshell
作者在学习Linux的过程中对常用的命令进行记录,通过思维导图的方式梳理知识点,并且通过xshell连接vmware中ubuntu虚拟机进行操作,并将练习的截图注解,每句话对应相应的命令,读者可以无障碍跟练。
本次练习的重点在于Linux的进程了解和使用 多线程。
1 Linux多线程概述
1.1 线程和进程
进程是系统中程序执行的资源分配的基本单位。每个进程都有自己的数据段、代码段和堆栈段。这就造成了进程切换等操作时,需要上下文切换,保存现场等动作。为了减少cpu空转时间和减少上下文切换开销,就出现了线程的概念。
线程是共享内存空间中并行执行的躲到执行路径,拥有独立的执行序列,是基本的调度单位,每一个进程都至少有一个main主线程。线程与同进程中的其他线程共享进程空间(堆区、代码区、数据区、文件描述符等),只拥有自己的栈空间,因此在切换的时候只需要考虑栈地址空间,大大减少了进程内线程切换的开销。
线程和进程在使用上各有优缺点:线程执行开销小,占用的CPU少,线程之间切换快,但不利于资源的管理和保护;进程刚好相反,而且多进程的可移植性要更好一些。
和进程一样,线程也有一个类似于PCB(进程控制块)的TCB(线程控制块)。一个进程可以有多个线程,也就是说有多个TCB和堆栈寄存器,但是这些线程都共享一个用户地址空间。注意的是:由于线程共享进程的资源和地址空间,因此,任何线程对系统资源的操作都会给其他线程带来影响。
1.2 线程分类
一般情况下线程分为用户级线程和内核级线程。
- 用户级线程:主要解决上下文切换费时问题,调度算法和调度过程全部由用户决定,运行时不需要内核支持,即不需要切换为内核态。缺点是无法发挥多处理器的优势。
- 内核级线程:允许不同进程中的线程按照同一相对优先调度方法调度,可以发挥多处理器的并发优势。
现在大多数系统两种线程都保留。用户级线程和核心级线程有“一对一”、“一对多”的模型。
1.3 线程创建的Linux实现——和进程对比
Linux的线程是通过用户级的函数库实现的,一般采用pthread线程库实现线程的访问和控制。使用第三方Posix标准的pthread,具有良好的可移植性。
2 线程的创建和退出
创建线程实际上就是确定调用该线程函数的入口点,使用pthread_create创建。在线程创建以后,就开始运行相关的线程函数,在该函数运行完后,线程自动退出,这是线程退出的一种方式。另一种线程主动退出的函数是pthread_exit。注意在使用线程函数式,不能随意使用exit退出函数进行出错处理,因为exit会导致当前进程直接退出,应该使用pthread_exit代替exit函数。
由于一个进程中的多个线程是共享数据段,因此线程退出后,该线程占用的资源不会随之释放。如进程的wait函数系统调用同步终止并释放资源,线程也有一个pthread_join 函数,该函数可以将当前的进程挂起,等待进程结束。该函数是一个线程阻塞函数,嗲用它的函数将一直等待知道被等待的进程结束为止,当函数返回时,被等待线程的资源被回收。
//线程的头文件
#include <pthread.h>
函数int pthread_create(pthread_t* thread, pthread_attr_t * attr, void *(*start_routine)(void *), void * arg);
用来创建线程,成功返回0,失败返回对应的错误码。参数描述如下:
- 参数thread是传出参数,保存新线程的标识
- 参数attr是一个结构体指针,结构体中的元素分别指定新线程的运行属性,可以用pthread_attr_init等函数设置值,通常传入NULL
- 参数start_routine是一个函数指针,指向新线程的入口函数,入口函数带有一个void* 的参数有pthread_create的第4个参数传入
- 参数arg用于传递给第3个参数指向的入口函数的参数,可以为NULL,表示不传递。
函数void pthread_exit(void *retval);
表示线程的退出。参数可以被其他线程用pthread_join捕获。
示例 2-1:创建线程并且退出
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
void* ThreadFunc(void *pArg)
{
for(int i = 0;i<10;i++){
printf("我是子线程,参数是%ld\n",(long)pArg);
sleep(1);
}
pthread_exit(NULL);
}
int main()
{
pthread_t thid;
pthread_create(&thid,NULL,ThreadFunc,(void*)123);
for(int i = 0;i<10;i++){
printf("我是主线程,子线程是%x\n",thid);
sleep(2);
}
return 0;
}
结果分析: 如果不在主线程中添加sleep(2),子线程无法运行所有的for循环,在主线程退出后就会退出,读者可以自行尝试。编译文件时注意要带上线程库,即在编译后加上 -lpthread 。
3 线程的等待和退出
3.1 等待线程退出
线程从入口函数自然返回,或者调用函数pthread_exit都可以线程正常终止,函数的返回值可以被其他线程用pthread_join函数获取
#include <pthread.h>
int pthread_join(pthread_t thid, void **thread_return);
- 该函数是一个阻塞函数,一直等到th指定的线程返回,和进程中的wait或者waitpid类似。thread_return是一个传出参数,接受函数的返回值,如果线程通过调用pthread_exit终止,则pthread_exit中的参数相当于自然返回值,照样可以被其它线程用pthread_join获取到
- thid传递为0是,join返回ESRCH错误
示例 3-1-1:获取线程返回值
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
//线程函数
void *ThreadFunc(void *pArg)
{
long iArg = (long)pArg; //将 void*转换为 int
sleep(iArg); //传入参数为休眠时间
if(iArg < 3)
return (void *)(iArg*2); //有返回值
else
pthread_exit((void *)(iArg*2)); //和 return 达到的效果一样,都可以用于正常返回
}
int main()
{
pthread_t thid;
long iRet = 0;
pthread_create(&thid,NULL,ThreadFunc,(void*)2); //创建线程并且传入参数2
pthread_join(thid,(void**)&iRet); //接收子线程的返回值
printf("第一个子线程返回值:%ld\n",iRet);
pthread_create(&thid,NULL,ThreadFunc,(void*)4);
pthread_join(thid,(void**)&iRet);
printf("第二个子线程返回值:%ld\n",iRet);
return 0;
}
示例 3-1-2:子线程释放空间
pthread_join函数还有一个非常重要的作用,由于一个进程中多个线程共享数据段,因此通常在一个线程退出后,退出线程所占用的资源并不会随线程结束而释放。如果th线程类型不会自动清理资源,则th线程结束后,线程本身的资源必须通过其它线程调用pthread_join来清除,这相当于多进程中的waitpid() 函数。
#include<stdio.h>
#include<pthread.h>
#include<malloc.h>
#include<unistd.h>
void* threadfunc(void *args)
{
char* p = (char*)malloc(10); //自己分配了10个内存空间
for(int i = 1;i <= 10;i++){
printf("子线程运行%d\n",i);
sleep(1);
}
//子线程手动释放空间
free(p);
printf("子线程释放全部空间\n");
pthread_exit((void*)3);
}
int main()
{
pthread_t pthid;
pthread_create(&pthid,NULL,threadfunc,NULL);
for(int i = 1;i <= 5;i++){
//父线程的运行次数比子线程要少,当父线程结束时,如果没有pthread_join函数等待子线程结束的话,子线程也会退出
printf("父线程运行%d\n",i);
sleep(1);
//if(i%3 == 0)
// pthread_cancel(pthid);
//表示当i%3 == 0的时候就取消了子线程,该函数将导致子线程直接退出,不会执行上free代码,子进程自动释放空间失败
//此时必须使用pthread_cleanup_push和pthread_cleanup_pop函数释放空间
}
long retvalue = 0;
pthread_join(pthid,(void**)&retvalue); //等待子线程释放空间
printf("等待子线程结束,子线程返回值:%ld\n",retvalue);
return 0;
}
注意: pthread_join不会回收堆内存,只回收线程的栈内存和内核中的struct task_struct结构体占用的内存。
3.2 线程的取消
线程可以被其他线程杀掉,在Linux中的说法是,一个线程可以被另一个线程取消(cancel)。
线程取消的方法是一个线程向目标线程发送cansel信号,但是如何处理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 本身对这方面的支持并不完善,所以在实际应用中应该谨慎使用!!
int pthread_cancel(pthread_t thread);
需要增加 man 信息 ,apt-get install manpages-posix-de
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 pthread_cleanup_pop(int execute)
pthread_cleanup_push()/pthread_cleanup_pop() 采用先入后出的栈结构管理
void routine(void arg) 函数在调用 pthread_cleanup_push() 时压入清理函数栈,多次对pthread_cleanup_push() 的调用将在清理函数栈中形成一个函数链,在执行该函数链时按照压栈的相反顺序弹出。
execute 参数表示执行到 pthread_cleanup_pop() 时是否在弹出清理函数的同时执行该函数,为 0 表示不执行,非 0 为执行;这个参数并不影响异常终止时清理函数的执行。
pthread_cleanup_push()/pthread_cleanup_pop()是以宏定义方式实现的。
#define pthread_cleanup_push(routine,arg)
{
struct _pthread_cleanup_buffer _buffer; \
_pthread_cleanup_push (&_buffer, (routine), (arg));
#define pthread_cleanup_pop(execute) \
_pthread_cleanup_pop (&_buffer, (execute));
}
pthread_cleanup_push()和pthread_cleanup_pop()分别带有"{" 和 “}”,因此这两个函数必须成对出现,且必须位于程序的同一级别的代码段中才能编译。
pthread_cleanup_pop 的参数 execute 如果为非 0 值,则按栈的顺序注销掉一个原来注册的清理函数,并执行该函数;当 pthread_cleanup_pop()函数的参数为 0 时,仅仅在线程调用 pthread_exit 函数或者其它线程对本线程调用 pthread_cancel 函数时,才在弹出“清理函数”的同时执行该“清理函数”。
示例 3-3-1:
#include<stdio.h>
#include<pthread.h>
void CleanFunc(void* pArg)
{
printf("这是线程清理函数%d\n",(long)pArg);
}
void* ThreadFunc(void* pArg)
{
pthread_cleanup_push(CleanFunc,(void*)1);
pthread_cleanup_push(CleanFunc,(void*)2);
sleep(2);
pthread_cleanup_pop(1);
pthread_cleanup_pop(200); //参数非0即可
}
int main()
{
pthread_t thid;
pthread_create(&thid,NULL,Threadfunc,(void*)2);
pthread_join(thid,NULL);
return 0;
}
结果: 由于入栈顺序是1,2,所以弹出顺序是2,1。
考虑其他情况: 如果将pthread_cleanup参数改为0,就不会有任何输出,CleanFunc不会得到运行。如果在sleep(2)后再添加pthread_exit(NULL); 就和原结果一样了。
示例 3-3-2: 用 pthread_cleanup_push 和 pthread_cleanup_pop 来释放子线程分配的内存空间
#include<stdio.h>
#include<pthread.h>
#include<malloc.h>
#include<unistd.h>
//清理内存函数
void freemem(void* args)
{
free(args);
printf("清理子线程申请的空间\n");
}
//子线程指针函数
void* threadfunc(void* args)
{
char* p = (char*)malloc(10); //申请10个空间
pthread_cleanup_push(freemem,p); //将清理函数地址压入函数栈,将开辟的空间地址作为参数传入,并且一并压栈
for(int i = 1;i <= 10;i++){
printf("子线程执行%d次\n",i);
sleep(1);
}
pthread_exit((void*)3); //线程退出,并且传入参数3,可以被pthread_join捕捉
pthread_cleanup_pop(0); //参数为0没有效果,和pthread_cleanup_push成对
}
int main()
{
pthread_t pthid; //线程标识符
pthread_create(&pthid,NULL,threadfunc,NULL);
for(int i = 1;i <= 5;i++){
//父线程运行次数比子线程的少,当父线程结束时,如果没有pthread_join函数等待子线程执行的haul,子线程也会退出,即使没有执行完
printf("父线程执行%d次\n",i);
sleep(2);
if(i%2 ==0){ //满足条件取消子线程,导致子线程来不及free空间,就需要用到pthread_cleanup_push和pthread_cleanup_pop
//因为pthread_cleanup_pop参数为0,所以调用pthread_cancel时,会将栈中的函数弹出并执行,即释放空间
pthread_cancel(pthid);
}
}
long retvalue = 0;
pthread_join(pthid,(void**)&retvalue); //等待子线程释放空间,并获取子线程的返回值
printf("子线程返回值是%ld\n",retvalue); //如果为-1,说明子线程已关闭
return 0;
}
4 线程的同步与互斥
4.1 线程的互斥
在 Posix Thread 中定义了一套专门用于线程互斥的 mutex 函数。mutex 是一种简单的加锁的方法来控制对共享资源的存取,这个互斥锁只有两种状态(上锁和解锁),可以把互斥锁看作某种意义上的全局变量。为什么需要加锁,就是因为多个线程共用进程的资源,要访问的是公共区间时(全局变量),当一个线程访问的时候,需要加上锁以防止另外的线程对它进行访问,实现资源的独占。在一个时刻只能有一个线程掌握某个互斥锁,拥有上锁状态的线程能够对共享资源进行操作。若其他线程希望上锁一个已经上锁了的互斥锁,则该线程就会挂起,直到上锁的线程释放掉互斥锁为止。
- 创建锁和销毁锁
有两种方式创建互斥锁,动态和静态
- 静态方式:
POSIX 定义了一个宏 PTHREAD_MUTEX_INITIALIZER 来静态初始化互斥锁,方法如下:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
在 Linux Threads 实现中,pthread_mutex_t 是一个结构,而 PTHREAD_MUTEX_INITIALIZER 则是
一个宏常量。
- 动态方式:
动态方式是采用 pthread_mutex_init()函数来初始化互斥锁,API 定义如下:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
其中 mutexattr 用于指定互斥锁属性(见下),如果为 NULL 则使用缺省属性。通常为 NULL
- pthread_mutex_destroy()用于注销一个互斥锁,API 定义如下:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
销毁一个互斥锁意味着释放它所占有的资源,且要求锁当前处于开放状态。由于在Linux中,互斥锁并不占有任何资源,因此 Linux Threads 中的 pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回 EBUSY)没有其他动作。
- 互斥锁的属性设置
互斥锁的属性在创建锁的时候设定,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同,也就是说是否阻塞等待。有三种属性可以选择:
- PTHREAD_MUTEX_TIMED_NP,这是缺省值(直接写NULL一样),就是普通锁,又叫快速锁。当一个线程加锁以后,其余请求锁的线程将形成一个阻塞等待队列,并在解除后按照优先级获得锁。这种策略保证了资源分配的时间公平性。
示例:初始化锁
pthread_mutex_t lock;
pthread_mutex_init(&lock,NULL);
- PTHREAD_MUTEX_RECURSIVE,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程的请求,则在加锁线程解锁时重新竞争。
示例:初始化一个嵌套锁
pthread_mutex_t lock;
pthread_mutexattr mutexattr;
pthread_mutexattr_init(&mutexattr);
pthread_mutexattr_settype(&mutexattr,PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_init(&lock,&mutexattr);
- PTHREAD_MUTEX_ERRORCHECK,检错锁,如果同一个线程请求同一个锁,则返回 EDEADLK,否则与 PTHREAD_MUTEX_TIMED 类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。如果锁的类型是快速锁,一个线程加锁之后,又加锁,则此时就是死锁。
示例:初始化一个检错锁
pthread_mutex_t lock;
pthread_mutexattr_t mutexattr;
pthread_mutexattr_settype(&mutexattr, PTHREAD_MUTEX_ERRORCHECK);
pthread_mutex_init(&lock, &mutexattr);
- 锁操作
操作 | 函数 |
---|---|
加锁 | 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而不是挂起等待。
示例 4-1-1:比较 pthread_mutex_trylock()与 pthread_mutex_lock()
#include<stdio.h>
#include<pthread.h>
pthread_mutex_t lock;
void* pthfunc(void* args)
{
pthread_mutex_lock(&lock); //先加一锁
pthread_mutex_lock(&lock); //再加一次锁,会挂起阻塞
//pthread_mutex_trylock(&lock); //用trylock加锁,不会挂起阻塞
printf("test\n");
sleep(1);
pthread_exit(NULL);
}
int main()
{
pthread_t pthid = 0;
pthread_mutex_init(&lock,NULL); //初始化锁
pthread_create(&pthid,NULL,pthfunc,NULL); //注册捕捉函数
pthread_join(pthid,NULL); //等待线程消亡
pthread_mutex_destroy(&lock); //销毁锁
return 0;
}
- 加锁注意事项
如果线程在加锁后解锁前被取消,锁将永远保持锁定状态,因此如果在关键区段内有取消点存在,必须在退出回调函数pthread_cleanup_push/pthread_cleanup_pop中解锁。同时不应该在信号处理函数中使用互斥锁,否则容易造成死锁。
死锁是指多个进程因为竞争资源造成的僵局,若无外力作用,这些进程都将无法向前推进。
死锁产生的原因
-
系统资源的竞争
系统资源的竞争导致系统资源不足,以及资源分配不当,导致死锁。 -
进程运行推进顺序不合适
进程在运行过程中,请求和释放资源的顺序不当,会导致死锁。
死锁的四个必要条件
- 互斥条件:一个资源每次只能被一个进程使用,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
- 请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
- 循环等待条件: 若干进程间形成首尾相接循环等待资源的关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,
就不会发生死锁。
死锁的预防
我们可以通过破坏死锁产生的 4 个必要条件来 预防死锁,由于资源互斥是资源使用的固有特性是无法改变的。
-
破坏“不可剥夺”条件:一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到 系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行。
-
破坏”请求与保持条件“:第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源。第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源。
-
破坏“循环等待”条件:采用资源有序分配其基本思想是将系统中的所有资源顺序编号,将紧缺的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。
互斥锁示例:火车站买票(不加锁,就会出现负票数
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
int ticketcount = 10; // 火车票,公共资源(全局)
void* salewinds(void* args)
{
while(ticketcount>0){ //如果有票,则卖票
sleep(1); //1秒卖一张票
ticketcount--; //出票成功
printf("窗口一:还剩%d张票\n",ticketcount);
}
}
void* salewinds2(void* args)
{
while(ticketcount>0){ //如果有票,则卖票
sleep(1); //1秒卖一张票
ticketcount--; //出票成功
printf("窗口二:还剩%d张票\n",ticketcount);
}
}
int main()
{
pthread_t pthid1 = 0;
pthread_t pthid2 = 0;
pthread_create(&pthid1,NULL,salewinds,NULL); //线程 1创建
pthread_create(&pthid2,NULL,salewinds2,NULL); //线程 2创建
pthread_join(pthid1,NULL); //等待线程结束
pthread_join(pthid2,NULL); //等待线程结束
return 0;
}
互斥锁示例:火车站买票(加锁,就不会出现负票数,而且可以实现交替卖票
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
int ticketcount = 10; // 火车票,公共资源(全局)
pthread_mutex_t lock; // 定义线程锁
void* salewinds(void* args)
{
while(1){
pthread_mutex_lock(&lock); //访问全局变量(资源),先加锁
if(ticketcount>0){ //如果有票,则卖票
sleep(1); //1秒卖一张票
ticketcount--; //出票成功
printf("窗口一:还剩%d张票\n",ticketcount);
}
else{ //如果没有票就解锁退出
pthread_mutex_unlock(&lock); //解锁
pthread_exit(NULL); //退出线程
}
pthread_mutex_unlock(&lock); //每卖一张票就要准备一下,解锁
sleep(1); //让另一个窗口有时间卖票,否则资源成为独享资源了
}
}
void* salewinds2(void* args)
{
while(1){
pthread_mutex_lock(&lock); //访问全局变量(资源),先加锁
if(ticketcount>0){ //如果有票,则卖票
sleep(1); //1秒卖一张票
ticketcount--; //出票成功
printf("窗口二:还剩%d张票\n",ticketcount);
}
else{ //如果没有票就解锁退出
pthread_mutex_unlock(&lock); //解锁
pthread_exit(NULL); //退出线程
}
pthread_mutex_unlock(&lock); //每卖一张票就要准备一下,解锁
sleep(1); //让另一个窗口有时间卖票,否则资源成为独享资源了
}
}
int main()
{
pthread_t pthid1 = 0;
pthread_t pthid2 = 0;
pthread_mutex_init(&lock,NULL); //初始化锁
pthread_create(&pthid1,NULL,salewinds,NULL); //线程 1创建
pthread_create(&pthid2,NULL,salewinds2,NULL); //线程 2创建
pthread_join(pthid1,NULL); //等待线程结束
pthread_join(pthid2,NULL); //等待线程结束
pthread_mutex_destroy(&lock); //销毁锁
return 0;
}
总结:线程互斥mutex加锁步骤
- 定义一个全局的锁,如果是使用静态锁pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; ,则 main 函中不用对锁进行 init 初始化。
- 在 main 中调用 pthread_mutex_init 函数对锁进行初始化
- 在子线程函数中调用 pthread_mutex_lock 加锁,访问临界资源
- 在子线程函数中调用 pthread_mutex_unlock 解锁
- 最后在 main 中调用 pthread_mutex_destroy 函数进行销毁
4.2 线程的同步
写在前面,作者在整理这一块学习资料的时候,不明白为什么有了互斥锁还要用条件变量,所以参考了网上的一些资料:
互斥锁体现的是一种竞争,即互斥关系,我离开了,通知你进来。
条件变量体现的是一种协作,即同步关系,我准备好了,你可以开始了。
互斥锁缺点是只有两个状态,锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起配合使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。
4.2.1 条件变量
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待条件变量的条件成立而挂起;另一个线程使条件成立(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
- 条件变量的创建和注销
条件变量和互斥锁一样,都有静态和动态两种创建方式。
静态方式使 PTHREAD_COND_INITIALIZER 常量,如下:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
动态方式调用 pthread_cond_init()函数,API 定义如下:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
尽管 POSIX 标准中为条件变量定义了属性,但在 Linux Threads 中没有实现,因此 cond_attr 值通常为 NULL,且被忽略
注销一个条件变量需要调用 pthread_cond_destroy()
,只有在没有线程在该条件变量上等待的时候能注销这个条件变量,否则返回 EBUSY。因为 Linux 实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。API 定义如下:int pthread_cond_destroy(pthread_cond_t *cond);
- 条件变量的等待和激活
等待条件有两种方式:无条件等待 pthread_cond_wait()和计时等待 pthread_cond_timedwait():
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()和pthread_cond_timedwait()的竞争条件。mutex的互斥锁必须是普通锁,且在调用pthread_cond_wait()前必须由本线程加锁,而在更新条件等待队列以前,mutex保持锁定,必须在线程挂起等待前解锁。,条件满足从而离开之前,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()则激活所有等待线程。
- pthread_cond_wait()和 pthread_cond_timedwait()都被实现为取消点,也就是说如果pthread_cond_wait()被取消,则退出阻塞,然后将锁状态恢复,则此时 mutex 是保持锁定状态的,而当前线程已经被取消掉,那么解锁操作就会得不到执行,此时锁得不到释放,就会造成死锁,因而需要定义退出回调函数来为其解锁。
示例 4-2-1:互斥锁和条件变量的结合使用
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
pthread_mutex_t mutex; //定义锁
pthread_cond_t cond; //定义条件变量
//线程清理函数:解锁
void ThreadClean(void* arg)
{
pthread_mutex_unlock(&mutex);
}
void* child1(void* arg)
{
//pthread_cleanup_push(ThreadClean,NULL); //将解锁操作压栈
while(1){
printf("线程1 开始运行\n");
printf("线程1 线程锁%d\n",pthread_mutex_lock(&mutex)); //线程1上锁
pthread_cond_wait(&cond,&mutex); //无条件等待父进程发送信号
printf("线程1 条件已满足\n");
pthread_mutex_unlock(&mutex); //解锁
sleep(5);
}
//pthread_cleanup_pop(0); //弹出清理函数但不执行,即不解锁,导致线程2没法上锁
return 0;
}
void* child2(void* arg)
{
while(1){
sleep(3); //使线程1 有时间运行并解锁,让线程2可以请求锁
printf("线程2 开始运行\n");
printf("线程2 线程锁%d\n",pthread_mutex_lock(&mutex));
pthread_cond_wait(&cond,&mutex); //等待父进程发送信号
printf("线程2 条件已适用\n");
pthread_mutex_unlock(&mutex);
sleep(1);
}
return 0;
}
int main()
{
pthread_t tid1,tid2;
printf("条件变量测试\n");
pthread_mutex_init(&mutex,NULL); //初始化锁
pthread_cond_init(&cond,NULL); //初始化条件变量
pthread_create(&tid1,NULL,child1,NULL); //创建线程1
pthread_create(&tid2,NULL,child2,NULL); //创建线程2
while(1){
sleep(2); //使线程1 有时间运行并解锁,让线程2可以请求锁
//pthread_cancel(tid1); //取消线程1,如果不注释可以实现线程12交替
sleep(2);
pthread_cond_signal(&cond); //激活等待条件变量的线程
}
sleep(10);
return 0;
}
结果: 线程1和2交替运行
总结: 首先是线程拿到锁,等待条件,解锁。主线程不管理锁,只负责释放条件。线程1和2交替上锁,请求条件,解锁,实现线程1和线程2交替同步运行。读者也可以自行尝试将注释的代码解开,再运行。
条件变量和互斥锁一样,不能用于信号处理,在信号处理函数中调用pthread_cond_signal()或者pthread_cond_broadcast() 很可能产生死锁。
示例 4-2-2:利用互斥锁再次实现买火车票,并且在火车票卖完时通过条件变量重新补5张票
#include<pthread.h>
#include<stdio.h>
#include<unistd.h>
int ticketcount = 5; //票数
pthread_mutex_t lock; //互斥锁
pthread_cond_t cond; //条件变量
//卖票窗口1,线程1
void* salewinds1(void* args)
{
while(1){
pthread_mutex_lock(&lock); //因为要访问全局的共享变量ticketcount,所以就要加锁
if(ticketcount > 0) { //如果有票
printf("窗口1开始卖票,剩余票数%d\n",ticketcount);
ticketcount--;
if(ticketcount == 0)
pthread_cond_signal(&cond); //通知没票了,这里可以阻塞线程,不让其退出
printf("窗口1卖出一张票,剩余%d张\n",ticketcount);
}
else{ //如果没票了就解锁退出
pthread_mutex_unlock(&lock);
break;
}
pthread_mutex_unlock(&lock);
sleep(1);
}
}
//卖票窗口2,线程2
void* salewinds2(void* args)
{
while(1){
pthread_mutex_lock(&lock); //因为要访问全局的共享变量ticketcount,所以就要加锁
if(ticketcount > 0) { //如果有票
printf("窗口2开始卖票,剩余票数%d\n",ticketcount);
ticketcount--;
if(ticketcount == 0)
pthread_cond_signal(&cond); //通知没票了,这里可以阻塞线程,不让其退出
printf("窗口2卖出一张票,剩余%d张\n",ticketcount);
}
else{ //如果没票了就解锁退出
pthread_mutex_unlock(&lock);
break;
}
pthread_mutex_unlock(&lock);
sleep(1);
}
}
//线程3,重新设置票数
void* setticket(void* args)
{
int i = 0; //设置补票次数变量
while(i++ < 1){ //
pthread_mutex_lock(&lock); //访问全局变量ticketcount,需要加锁
if(ticketcount > 0){ //如果有票就解锁并且阻塞,等待窗口发来缺票的条件信号
pthread_cond_wait(&cond,&lock); //将锁解开,并且将此线程挂起阻塞,等待发来的条件变量signal信号
//如果signal发来了条件变量信号,此时会重新上锁开始补票
}
ticketcount = 5; //补票
printf("补票成功\n");
pthread_mutex_unlock(&lock); //解锁
sleep(1);
}
pthread_exit(NULL);
}
int main()
{
pthread_t pthid1,pthid2,pthid3;
pthread_mutex_init(&lock,NULL); //初始化锁
pthread_cond_init(&cond,NULL); //初始化条件变量
pthread_create(&pthid1,NULL,salewinds1,NULL);
pthread_create(&pthid2,NULL,salewinds2,NULL);
pthread_create(&pthid3,NULL,setticket,NULL);
pthread_join(pthid1,NULL); //等待子线程执行完毕
pthread_join(pthid2,NULL); //等待子线程执行完毕
pthread_join(pthid3,NULL); //等待子线程执行完毕
pthread_mutex_destroy(&lock); //销毁锁
pthread_cond_destroy(&cond); //销毁条件变量
return 0;
}
4.3 消费者生产者问题
示例 4-3-1:使用链表实现消费者生产者问题(互斥锁和条件变量)
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<malloc.h>
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; //定义一个静态锁
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //定义一个静态条件变量
struct node{ //定义单链表
int num;
struct node *next;
}*head = NUUL; //初始化一个空的头指针
//定义释放内存的清理函数
static void cleanup_handler(void* arg)
{
printf("清除内存\n");
free(arg);
pthread_mutex_unlock(&mtx);
}
//定义消费者线程函数
static void* thread_func(void* arg) //消费者
{
struct node* p = NULL;
pthread_cleanup_push(cleanup_p); //将清理内存函数压栈
while(1){
pthread_mutex_lock(&mtx); //访问公共head,需要加锁
while(head == NULL) {pthread_cond_wait(&cond,&mtx);} //如果此时没有结点,则释放锁挂起等待条件变量
p = head; //删除表头结点
head = head->next;
printf("取出了队头,买到了%d号票\n",p->num);
free(p);
pthread_mutex_unlock(&mtx);//释放锁
}
pthread_exit(NULL);
pthread_cleanup_pop(0);
}
int main()
{
pthread_t tid; //设置进程id
struct node* p; //辅助结点
pthread_create(&tid,NULL,thread_func,NULL); //创建线程函数
for(int i = 0; i < 10;i++){
p = (struct node*)malloc(sizeof(struct node));
p->num = i;
pthread_mutex_lock(&mtx); //访问公共head,要加锁
p->next = head; //头插法
head = p;
pthread_cond_sign(&cond); //释放
pthread_mutex_unlock(&mtx);
sleep(1);
}
printf("生产结束,所以取消消费者线程\n");
pthread_cancel(tid);
pthread_join(tid,NULL);
printf("线程退出\n");
return 0;
}
5 线程安全和线程属性
5.1 线程安全
线程安全 是指:如果一个函数能够同时被多个线程调用而得到正确的结果,那么我们说这个函数是线程安全的。线程安全的原因,多数是由于全局变量和静态变量的操作不规范。
可重入函数 :如果一个函数只访问自己的局部变量或参数,就称为可重入函数。反之称为,不可重入函数。显然可重入函数是线程安全的。
- 可重入的概念只和函数访问的变量有关,和是否使用锁无关,锁可以让函数变成线程安全的函数。
- 可重入函数一定是线程安全的。
- 如果一个函数中有全局变量,这个函数既不是可重入函数,也不是线程安全函数。
- 线程安全函数可以使得不同线程访问同一块地址空间,可重用函数要求不同的执行流对数据的操作会不影响使用结果,两者是相同的。
5.2 线程的属性
前面用到的pthread_create函数的第二个参数,创建线程时将其置为NULL,第二个参数attr是一个结构体指针,结构体中的元素时线程的属性,下面就来介绍一下这些属性:
- __detachstate 表示新线程是否与进程中其他线程脱离同步,如果置位则新线程不能用 pthread_join()来同步,且在退出时自行释放所占用的资源。缺省为 PTHREAD_CREATE_JOINABLE 状态。这个属性也可以在线程创建并运行以后用 pthread_detach()来设置,而一旦设置为PTHREAD_CREATE_DETACHED 状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE 状态。
- __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_INHERIT_S。
- __scope ,表示线程间竞争 CPU 的范围,也就是说线程优先级的有效范围。POSIX 的标准中定义了两个值:PTHREAD_SCOPE_SYSTEM 和 PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争 CPU 时间,后者表示仅与同进程中的线程竞争 CPU。目前 Linux 仅实现了PTHREAD_SCOPE_SYSTEM 一值。
属性设置函数: 成功返回0,失败返回-1
- 属性初始化函数
int pthread_attr_init (pthread_attr_t *attr)
,attr传出参数,表示线程属性,后面的线程设置属性函数相似 - 设置绑定属性
pthread_attr_setscope(pthread_attr_t *attr, init scope
,scope表示绑定或不绑定 - 设置线程是否分离属性
pthread_attr_setdetachstate(pthread_attr_t *attr, init detachstate);
,detachstate设置线程是否与其他线程分离 - 获取线程优先级
int pthread_attr_getschedparam(pthread_attr_t *attr, struct sched_param *param);
,param返回线程优先级 - 设置线程优先级
int pthread_attr_setschedparam(pthread_attr_t *attr, struct sched_param *param);
示例 5-1:线程属性设置函数的使用
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
#include<error.h>
void* threadFunc(void *arg)
{
printf("线程函数正在运行,参数是%s\n",(char*)arg);
sleep(4);
printf("线程结束现在退出。\n");
thread_finished = 1;
pthread_exit(NULL);
}
char mes[] = "Hello,World.";
int thread_finish = 0;
int main()
{
int res = 0;
pthread_t thid;
void* thread_result;
pthread_attr_t thread_attr; //定义属性结构体
struct sched_param sheduling_value;
res = pthread_attr_init(&thread_attr);
if(res != 0){
perror("属性创建失败\n");
exit(EXIT_FAILURE); //宏定义 -1
}
//设置调度策略
res = pthread_attr_setschedupolicy(&thread_attr,SCHED_OTHER);
if(res != 0){
perror("设置调度策略失败\n");
exit(EXIT_FAILURE); //宏定义 -1
}
//设置脱离状态
res = pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED);
if(res != 0){
perror("设置脱离状态失败\n");
exit(EXIT_FAILURE); //宏定义 -1
}
//创建线程
res = pthread_create(&thid,&thread_attr,threadFunc,(void*)mes);
if(res != 0){
perror("线程创建失败\n");
exit(EXIT_FAILURE); //宏定义 -1
}
//获取线程的最大优先级
int max_priority = sched_get_priority_max(SCHED_OTHER);
//获取线程的最小优先级
int min_priority = sched_get_priority_min(SCHED_OTHER);
//重新设置优先级别
sheduling_value.sched_priority = min_priority + 5);
//设置优先级别
res = pthread_attr_setscheduparam(&thread_attr,&scheduling_value);
if(res != 0){
perror("设置优先级失败\n");
exit(EXIT_FAILURE); //宏定义 -1
}
pthread_attr_destroy(&thread_attr);
while(!thread_finished){
printf("等待进程结束\n");
sleep(3);
}
printf("进程结束了\n");
exit(0);
return 0;
}