C++实战-Linux多线程(入门到精通)
1.进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信方式,在进程间进行信息交换。2.调用fork()来创建进程的代价相对较高(复制一份地址空间),即便采用"写时复制"机制,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着fork()调用在时间上的开销依然不菲3.线程之间能够方便、快速地共享信息,只需要将数据复制到共享(栈不行,见上图)变
目录
线程的概念
1.与进程(process)类似,线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同的程序,并且共享同一份全局内存区域,其中包括初始化数据段(.data),未初始化数据段(.bss),栈内存段。
【注意:没有共享栈内存和代码段】
2.进程是CPU分配资源的最小单位,线程是操作系统调度执行的最小单位
3.线程是轻量级的进程(LWP:light weight process),在Linux环境下线程的本质仍然是进程
4.查看指定进程的LWP:ps -lf pid (注:lwp不是线程的id)
Linux内核线程实现原理
1.轻量级进程(light-weight process)也有PCB,创建线程使用的底层函数和进程一样,都是clone
2.从内核里看进程和线程是一样的,都有各自不同PCB,但是PCB中指向内存资源的三级页表是相同的
三级映射:进程PCB-->页目录(可以看出是数组,首地址位于PCB中)-->页表-->物理页面-->内存单元
3.进程可以蜕变成线程(看上面的虚拟地址空间就能分析出来)
4.线程可看作寄存器和栈的集合
5.LWP和进程ID的区别:LWP号是Linux内核划分时间轮片给线程的依据,线程ID是在进程内部区分线程的
a
6.对于进程而言,相同地址在不同的进程中,反复使用而不冲突。原因是他们虽然虚拟地址一样,但页目录、页表、物理页面各不相同。相同的虚拟地址,映射到不同的物理页面内存单元,最终访问不同物理页面。
但,线程不同。两个线程虽然有独立的PCB,但是共享同一个页目录、页表、物理页面。所以两个PCB共享同一个地址空间。
7.无论是创建进程的fork()还是创建线程的pthread_creat(),底层实现都是调用一个内核函数clone()。如果是复制对方的地址空间,那么就产生一个“进程”,如果是共享对方的地址空间,就产生一个"线程"。因此,Linux内核是不区分进程和线程的。只是在用户层面上进行区分。所以,线程所有的操作函数pthrad_*是库函数,而非系统调用。
进程和线程区别总结
1.进程间的信息难以共享。由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信方式,在进程间进行信息交换。
2.调用fork()来创建进程的代价相对较高(复制一份地址空间),即便采用"写时复制"机制,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着fork()调用在时间上的开销依然不菲
3.线程之间能够方便、快速地共享信息,只需要将数据复制到共享(栈不行,见上图)变量中即可
4.创建线程比创建进程通常要快10倍甚至更多。线程间是共享虚拟地址空间的,无需采用写时复制来复制内存,也无需复制页表(之前说过虚拟地址空间的实现,并不是真的去分配4G的内存,只需要实现相应的数据结构,页表、页目录)
线程之间共享和非共享资源
这个图太重要了,我们再看一下
共享资源 非共享资源 文件描述符表 线程ID 每种信号的处理方式 处理器现场和栈指针(内核栈) 当前工作目录、文件权限 独立的栈空间(用户空间栈) 用户ID和组ID和会话ID errno变量 虚拟地址空间(除栈 .txt) 信号屏蔽字 调度优先级 介绍下NPTL(第三方库):
当Linux最初开发时,在内核中并不能正真支持线程。通过clone()系统调用进程作为可调度的实体。这个调用创建了调用进程的一个拷贝,这个拷贝于调用共享相同的地址空间。但这个符合POSIX的要求,在信号处理、调度间同步等方面存在问题。
NPTL或称为Native POSIX Thread Library。是Linux线程的一个新实现,它克服了LinuxThreads的缺点,同时符合标准。
查看当前进程库版本:getconf GNU_LIBPTHREAD_VERSION
线程优缺点
优点:1.提高程序并发性 2.开销小 3.数据通信、共享数据方便
缺点:1.库函数、不稳定 3.调试、编写困难(gdb不支持) 4.对信号支持不好
优点相对突出,缺点均不是硬伤。Linux下由于实现方法导致进程、线程差别不是很大
线程相关函数(线程控制原语)
一般情况下,main函数所在的线程称为主线程。其余创建的线程称为子线程。
fork()函数--->创建进程
程序中默认只有一个线程,pthread_create()函数调用-->2个线程
pthread_create函数
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,
void*(*start_routine)(void*),void *arg);
功能:创建一个子线程
参数:
thread:传出参数,线程创建成功后,子线程的线程ID被写到该变量中
attr设置线程的属性,一般使用默认值 NULL
start_routine:函数指针,这个函数是子线程需要处理的逻辑代码
arg:给第三个参数使用
返回值:
成功:0
失败:返回错误号
补充说明:因为线程是通过NPTL库是第三方库,跟我们的系统调用不一样哦。
之前我们一直使用perror(ret),现在可行不通. 使用 char *strerror(int errnum)
#include <iostream> #include <pthread.h> #include <string.h> #include <unistd.h> using namespace std; void *func(void *arg) { printf("pthread:%lu\n",pthread_self()); return NULL; } int main(void) { //typedef unsigned long pthread_t tid; int ret = pthread_create(&tid,NULL,func,NULL); if(ret) { printf("%s\n",strerror(ret)); exit(-1); } sleep(1); printf("我是主线程:%d\n",getpid()); return 0; }
编译注意事项:线程椒通过第三方库实现的
g++ createPthread.cpp -o createPthread -lpthread(-pthread)
pthread_exit函数
int pthread_exit(void *retval);
功能:终止一个线程,在哪个线程中调用就代表终止哪个线程
参数:
retval:需要传递一个指针,作为一个返回值,在pthread_join()中可以获取到。
线程退出的状态,通常传NULL
补充:当主线程退出时,不会影响其他正常运行的线程
在线程中禁止使用exit函数,会导致进程内所有线程全部退出
#include <stdio.h> #include <pthread.h> #include <string.h> #include <unistd.h> void *func(void *arg) { int i = (int)arg; if(i == 2) { pthread_exit(NULL); } sleep(2); printf("我是第%d号线程,线程ID:%lu\n",i,pthread_self()); return NULL; } int main(void) { pthread_t tid; for(int i=0;i<5;++i) { pthread_create(&tid,NULL,func,(void *)i); } sleep(5); printf("我是主线程,线程ID:%lu\n",pthread_self()); return 0; }
pthread_self函数
pthread_t pthread_self(void)
功能:获取当前的线程的线程ID
pthread_equal函数
int pthread_equal(pthread_t t1,pthread_t t2);
功能:比较两个线程ID是否相等
返回值:相等非0,不相等0
补充:不同操作系统pthread_t类型的实现不一样,有的时无符号长整型,有的可能是结构体
#include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_t tid_one; void *func(void *arg) { if(pthread_equal(tid_one,pthread_self())) { printf("相等\n"); } else { printf("不相等\n"); } } int main(void) { pthread_t tid; pthread_create(&tid_one,NULL,func,NULL); pthread_join(tid_one,NULL); return 0; }
pthread_join函数
int pthread_join(pthread_t thread,void ** retval);
功能:和一个已经终止的线程进行连接,回收资源
回收子进程的资源
这个函数是阻塞的,调用一次只能回收一个子进程
一般在主线程中使用
参数:thread 需要回收的子进程ID
retval 接收子进程退出时的返回值
返回值:
0 成功
非0,失败,返回错误号
线程的分离
int pthread_detach(pthread_t thread);
功能:分离一个线程。被分离的线程在终止的时候,会自动释放资源返回给系统
注意事项:
1).不能多次分离,会产生不可预测的行为
2).不能去连接一个已经分离的线程,会报错【pthread_join】
参数:
需要分离的线程的ID
返回值:
成功0 失败错误号
重点说明:
线程分离状态:指定该状态,线程主动与主控制线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。
进程如果有这样的机制,将不会产生僵尸进程,僵尸进程的产生主要由于进程死亡后,大部分资源被释放,一点残留资源仍然在系统中,导致内核认为该进程仍然存在
一般情况下,线程终止后,其终止状态一直保留到其他线程调用pthread_join获取它的状态为止,但是线程也可以被设置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join这样的调用将返回EINVAL错误。
#include <stdio.h> #include <pthread.h> void* func(void *arg) { printf("我是子线程:%lu\n",pthread_self()); return NULL; } int main(void) { pthread_t tid; pthread_create(&tid,NULL,func,NULL); //设置线程分离 pthread_detach(tid); sleep(3); return 0; }
线程的取消
int pthread_cancel(pthread_t thread);
功能:取消线程(让线程终止)
取消某个进程,可以终止某个线程的运行。
但不是立马终止,而是当子线程执行到一个取消点,线程才会终止
取消点:系统调用(从用户态切换到内核态的时候) creat open pause read write..
如果线程终没有取消点,可以通过调用pthread_testcancel函数自行设置一个取消点
被取消的线程,退出值定义在Linux的pthread库中。常数PTHREAD_CANCELED的值是一个 -1。可在pthread.h中找到它的定义。
因此我们对一个已经被取消的线程使用pthread_join回收时,得到的返回值-1
#include <stdio.h> #include <pthread.h> void *func(void *arg) { //printf("我是子线程:%lu\n",pthread_self()); //printf会产生一个系统调用 pthread_testcancel(); //自己设置一个取消点 return NULL; } int main(void) { pthread_t tid; pthread_create(&tid,NULL,func,NULL); //取消这个线程 pthread_cancel(tid); //回收 int childRet; pthread_join(tid,(void **)&childRet); //阻塞的 printf("回收的返回值:%d\n",childRet); return 0; }
线程属性
Linux下的线程属性是可以根据实际项目需求进行设置,之前我们讨论的是采用线程默认的属性。默认属性已经可以解决大多数问题。如果我们对程序的性能提出更高的要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存使用从而增加最大线程数量。
主要属性:作用域、栈尺寸、栈地址、优先级、分离状态、调度策略
线程属性值不能直接设置,需要通过相关函数(可以理解为接口)进行操作:
int pthread_attr_init(pthread_attr_t *attr); //初始化线程属性变量 int pthread_attr_destroy(pthread_attr_t *attr); //释放线程属性的资源 int pthread_attr_getdetachstate(const pthread_attr_t *attr,int *detachstate); //获取线程分离的状态属性 int pthread_attr_setdetachstate(pthread_attr_t *attr,int detachstate); //设置线程分离的状态属性 查看线程的属性方法: man pthread_attr_XXX
案例:
//创建一个线程属性变量
pthread_attr_t attr;
//初始化属性变量
pthread_attr_init(&attr);
//设置属性
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
//设置线程栈的大小
pthread_attr_setstacksize(&attr,size);
.....
#include <stdio.h> #include <pthread.h> #include <string.h> void* func(void *arg) { printf("子线程:%lu\n",pthread_self()); return NULL; } int main(void) { pthread_t tid; //创建线程属性变量 pthread_attr_t attr; //初始化 pthread_attr_init(&attr); //设置线程分离 pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); //设置栈大小 int size = 256*1024; pthread_attr_setstacksize(&attr,size); pthread_create(&tid,&attr,func,NULL); while(1) { sleep(1); void* retval; int err = pthread_join(tid,&retval); if(err) printf("-------------err= %s\n", strerror(err)); else printf("-----------%d\n",(int)retval); } return 0; }
线程使用注意事项
1.主线程退出其他线程不退出,主线程调用 pthread_exit
2.避免僵尸线程
pthread_join
pthread_detach
设置线程属性为分离,然后 pthread_create
3.malloc和mmap申请的内存可以被其他线程释放
4.应避免在多线程模型中调用fork,除非马上exec。子进程中只有调用frok的线程存在,其他线程在子进程中均pthread_exit
5.信号的复杂语义很难和多线程共存,应避免在多线程引入信号机制
线程同步
先说同步的概念(不要觉得啰嗦,方便我们去理解线程同步):
所谓同步,对于不同的研究对象而言是具有不同的含义的。例如:设备同步,是指在两个设备之间规定一个共同的时间参考。秦始皇的“书同文,车同轨”岂不也是一种同步。而在编程中的同步是指协同、协助、相互配合,主要是为了协调步骤,按预定的先后次序运行。
线程同步
同步即协同步骤,按预定的先后次序运行。大家有没有想过一个问题,为什么我们要强调按预定的先后次序,主要是因为同一个进程内的线程之间是资源共享的,加上并发的原因,假设一个线程想要修改每一个数据,还没修改完,另一个线程就把它取出,是不是就会产生问题。
专业一点说:线程同步,指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其他线程为保证数据一致性,不能调用该功能。(一个线程对某一共享的资源没有调用完,其它线程不能调用)
详细分析:
1.线程的主要优势在于能够通过全局变量来共享信息。不过,这种便捷(便捷是与进程间通信比较得出的)的共享是有代价的,必须确保多个线程不会同时修改同一变量或者某一线程不会读取正在由其他线程修改的变量(你会发现可以同时读)。
2.临界区是指访问某一共享资源的代码片段,并且这段代码的执行应为原子操作。也就是同时访问同一共享资源的其他线程不应终断该片段的执行。
3.当有一个线程在对内存操作时,其他线程都不可以对这个内存地址进程操作,直到该线程完成操作,其他线程才能对该内存进行操作,而其他线程则处于等待状态。
那么如何才能更好的保持这种原子操作呢?
互斥量、信号量、XXX锁....机制
不晓得有没有发现,很多时候的发展就是为了解决某一个问题。在任意条件下,很难做到十全十美,或者说很难画出一个完美的圆,我们一直在不断的创新不断的去接近这个完美的圆。好似我们永远没有算不完π一样。
互斥量(互斥锁)
先说一个通俗的理解:现在有一个房间,并且这个房间同一个时刻只能容纳一个人,防止两个人或以上的人进入,现在给这个房间置办一把锁,当有人进去时,看门的人就把房间锁上,当人出来,把锁打开。又有人进去时,把锁锁上,如此而已。其实很多计算机中解决问题的办法跟我们实际生活有很大的联系的,细细体会
1.为了避免线程更新(修改)共享变量时出现问题,可以使用互斥量(mutex)来确保同时仅有一个线程可以访问某项共享资源。可以使用互斥量来保证对任意共享资源的原子访问。
2.互斥量有两种状态:已锁定(locked)和未锁定(unlocked)。任何时候,至多只有一个线程可以锁定该互斥量。试图对已经锁定的某一互斥量再次加锁将可能阻塞线程或者报错失败,具体取决于加锁时使用的方法。(我们回到上面的例子,我们管理人员每次只对一把锁打开,如果加了两把锁,那么屋子里的人出不来,屋子外的人进不去,而且管理员已经开了一把锁,他认为房间已经空了,等待人进去。导致永远阻塞在这了,呜呜...)
3.一旦线程锁定互斥量,随即成为该互斥量的所有者,只有所有者才能给互斥量解锁。一般情况下,对每一个共享资源会使用不同的互斥量,每一个线程在访问同一资源时将采用如下协议:
- 针对共享资源锁定互斥量(加锁)
- 访问共享资源(访问)
- 对互斥量解锁(解锁)
4.如果有多个线程试图执行这一块代码(临界区),事实上只有一个线程能够持有该互斥量(其他线程将遭遇阻塞),即同时只有一个线程能够进入这段代码区域
举个例子:
通过"锁"将资源的访问变长了互斥操作,而后与时间有关的错误也不会产生(按预计的次序执行)。
说明:当A线程对某个全局变量加锁访问,B在访问前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。
所以,互斥锁实质上是一把"建议锁"(又称为”协同锁“),建议程序中有多线程访问共享资源的时候使用该机制,但不是强制使用。(什么意思呢,就是某一个线程在访问共享资源之前,不访问锁,直接去访问共享资源,也可以访问到。我们使用互斥量需要按照规定步骤来,防止数据混乱。我直接进房间,我不管是否有管理员)
相关函数
互斥量的类型:pthread_mutex_t
pthread_mutex_init函数
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
作用:初始化互斥量
参数:mutex 需要初始化的互斥量变量
attr 互斥量相关属性,通常传 NULL
restrict:C语言的修饰符,被修饰的指针不能由另外一个指针进行操作
pthread_mutex_destroy函数
int pthread_mutex_destroy(pthread_mutex_t *mutex);
作用:释放互斥量的资源
pthread_mutex_lock函数
int pthread_mutex_lock(pthread_mutex_t *mutex);
作用:加锁,阻塞的(如果一个线程加锁了,那么其他线程只能阻塞等待)
pthread_mutex_trylock函数
int pthread_mutex_trylock(pthread_mutex_t *mutex);
作用:尝试加锁,非阻塞(如果加锁失败,不会阻塞,会直接返回)
pthread_mutex_unlock函数
int pthread_mutex_unlock(pthread_mutex_t *mutex);
作用:解锁
上个案例:(目标->能够完整打印HELLO WORLD或hello world)
不加互斥锁:
#include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_mutex_t mutex; //定义为全局变量,不能定义为栈上的临时变量 void *func(void *arg) { srand(time(NULL)); while(1) { //pthread_mutex_lock(&mutex); //加锁 printf("hello"); sleep(rand()%3); printf("world\n"); //pthread_mutex_unlock(&mutex); //解锁 sleep(rand()%3); } return NULL; } int main(void) { int n = 5; pthread_t tid; srand(time(NULL)); //设置随机种子 //初始化互斥量,在创建线程之前 pthread_mutex_init(&mutex,NULL); //创建线程 pthread_create(&tid,NULL,func,NULL); while(n--) { //pthread_mutex_lock(&mutex); //加锁 printf("HELLO"); sleep(rand()%3); printf("WORLD\n"); //pthread_mutex_unlock(&mutex); //解锁 sleep(rand()%3); } //销毁锁 pthread_mutex_destroy(&mutex); //关闭子线程 pthread_cancel(tid); //回收子线程,或者设置线程分离 pthread_join(tid,NULL); //pthread_detach(tid); return 0; }
加互斥锁
#include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_mutex_t mutex; //定义为全局变量,不能定义为栈上的临时变量 void *func(void *arg) { srand(time(NULL)); while(1) { pthread_mutex_lock(&mutex); //加锁 printf("hello"); sleep(rand()%3); printf("world\n"); pthread_mutex_unlock(&mutex); //解锁 sleep(rand()%3); } return NULL; } int main(void) { int n = 5; pthread_t tid; srand(time(NULL)); //设置随机种子 //初始化互斥量,在创建线程之前 pthread_mutex_init(&mutex,NULL); //创建线程 pthread_create(&tid,NULL,func,NULL); while(n--) { pthread_mutex_lock(&mutex); //加锁 printf("HELLO"); sleep(rand()%3); printf("WORLD\n"); pthread_mutex_unlock(&mutex); //解锁 sleep(rand()%3); } //销毁锁 pthread_mutex_destroy(&mutex); //关闭子线程 pthread_cancel(tid); //回收子线程,或者设置线程分离 pthread_join(tid,NULL); //pthread_detach(tid); return 0; }
我们的讨论到这里就结束了吗?
当然没有,我们来看一下特殊的情况
我现在把代码改成这个样子,会得到什么结果呢....
进入了死循环,主线程竞争不到CPU了
线程在操作完共享资源后本应该立即解锁,但修改后,线程抱着锁睡眠。睡醒解锁后又立即加锁。这两个库函数本身不会阻塞。所以在这两行代码之间失去CPU的概率很小。因此,另一个线程很难得到加锁的机会。
我们再来修改代码:
发现子线程没有结束,父线程阻塞等待回收子线程
这个原因就很明显了,pthread_join会阻塞等待子线程结束,子线程进入死循环中..所以...
死锁
死锁产生的四个必要条件当时学习操作系统时(课本上定义):
1.互斥条件(我们互斥锁解决的就是互斥的共享资源,某一时刻只能有一个线程进入)
2.请求和保持条件(每一个进程都有保持现有状态)
3.不剥夺条件(没有外力的影响)
4.循环等待条件(等待其他进程释放资源)
这是课本中给我们的定义,后面加上了一些解释,方便理解。
那么在实际编程过程中的场景(主要有三种情况):
1.忘记释放锁
2.重复加锁
3.多线程多锁,抢占锁资源
(第一个种情况很好理解,这里就不过多的解释了。我们重点分析下第二种和第三种情况)
有时,一个线程需要同时访问两个或更多不同的共享资源,而每一个共享资源都由不同的互斥量管理。当超过一个线程加锁同一组互斥量时,就可能产生死锁。(对同一个互斥量加锁两次)
解决:访问完共享资源后立即解锁,等待步骤完成之后再加锁
两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,如无外力作用,他们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。(线程1拥有锁A,请求锁B,线程2拥有锁B,请求锁A)
解决:trylock替代lock函数并解锁(当不获取所有锁时主动放弃所有锁)
上案例:
#include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_mutex_t mutex1; void* deadlock1(void *arg) { pthread_mutex_lock(&mutex1); printf("hello"); pthread_mutex_lock(&mutex1); printf("world1\n"); pthread_mutex_unlock(&mutex1); pthread_mutex_unlock(&mutex1); return NULL; } int main(void) { pthread_t tid1; //初始化 pthread_mutex_init(&mutex1,NULL); //创建线程 pthread_create(&tid1,NULL,deadlock1,NULL); //设置线程分离 pthread_detach(tid1); //退出主线程 pthread_exit(0); return 0; }
#include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_mutex_t mutex1; pthread_mutex_t mutex2; void* deadlock1(void *arg) { pthread_mutex_lock(&mutex1); printf("hello"); sleep(4); pthread_mutex_lock(&mutex2); printf("world1\n"); pthread_mutex_unlock(&mutex2); pthread_mutex_unlock(&mutex1); return NULL; } void* deadlock2(void *arg) { // sleep(1); pthread_mutex_lock(&mutex2); printf("HELLOE"); sleep(3); pthread_mutex_lock(&mutex1); printf("WORLD\n"); pthread_mutex_unlock(&mutex1); pthread_mutex_unlock(&mutex2); return NULL; } int main(void) { pthread_t tid1,tid2; //初始化 pthread_mutex_init(&mutex1,NULL); pthread_mutex_init(&mutex2,NULL); //创建线程 pthread_create(&tid1,NULL,deadlock1,NULL); pthread_create(&tid2,NULL,deadlock2,NULL); //设置线程分离 pthread_detach(tid1); pthread_detach(tid2); //退出主线程 pthread_exit(0); return 0; }
读写锁
与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享
当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞。但是考虑一种情况,当前持有互斥锁的线程只是要读访问共享资源,而同时有其他几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法获取访问共享资源了,但实际上多个线程同时读访问共享资源并不会导致问题。
在对数据的读写操作中,更多是读操作,写操作相对较少。例如:数据库数据的读写应用。为了满足当前能够允许多个读出,但只允许一个写入的需求,线程提供了读写锁来实现。
读写锁的特点:
1.如果有其他线程读数据,则允许其他线程执行读操作,但不允许写操作
2.如果有其他线程写数据,则其他线程都不允许读、写操作
3.写是独占的,写的优先级高(防止写着饿死)
说了这么多,发现就是操作系统课本中的读者写者问题....哈哈哈
相关函数
读写锁类型:pthread_rwlock_t
pthread_rwlock_init函数
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
作用:初始化一把读写锁
参数:
attr表示读写锁属性,通常使用默认属性,传NULL即可
pthread_rwlock_destroy函数
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
作用:销毁一把读写锁
pthread_rwlock_rdlock函数
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
作用:以读方式请求读写锁(请求读锁)
pthread_rwlock_wrlock函数
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
作用:以写方式请求读写锁(请求写锁)
pthread_rwlock_unlock函数
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
作用:解锁
pthread_rwlock_tryrdlock函数
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
作用:非阻塞请求读锁
pthread_rwlock_trywrlock函数
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
作用:非阻塞请求写锁
上个案例(同时有多个线程对同一共享数据读、写操作)
#include <stdio.h> #include <pthread.h> #include <unistd.h> pthread_rwlock_t rwlock; int counter = 0; //写线程 void *th_write(void *arg) { int t; int i = (int)arg; while(1) { pthread_rwlock_wrlock(&rwlock); //上写锁 sleep(2); printf("writer:%d %lu counter:%d\n",i,pthread_self(),++counter); pthread_rwlock_unlock(&rwlock); //解锁 usleep(10000); } } //读线程 void *th_read(void *arg) { int i = (int)arg; while(1) { pthread_rwlock_rdlock(&rwlock); //上读锁 printf("read:%d %lu counster:%d\n",i,pthread_self(),counter); pthread_rwlock_unlock(&rwlock); sleep(3); } } int main(void) { int i; pthread_t tid[8]; pthread_rwlock_init(&rwlock,NULL); for(i=0;i<3;++i) { pthread_create(&tid[i],NULL,th_write,(void *)i); } for(i=0;i<5;++i) { pthread_create(&tid[i],NULL,th_read,(void *)i); } //回收子线程 for(i=0;i<8;++i) { pthread_join(tid[i],NULL); } //销毁读写锁 pthread_rwlock_destroy(&rwlock); return 0; }
条件变量
条件变量本身不是锁,但是可以引起线程阻塞。通常与互斥锁配合使用。给多线程提供一个合适的场所。(线程同步问题,互斥锁和读写锁解决线程互斥)
相关函数
条件变量的类型:pthread_cond_t
pthread_cond_init函数
int pthread_cond_int(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
参数:attr表示条件变量的属性,通常传NULL
可以使用静态初始化方法,初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
pthread_cond_destroy函数
int pthread_cond_destroy(pthread_cond_t *cond);
作用:销毁一个条件变量
pthread_cond_wait函数
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
作用:1.阻塞等待一个条件变量cond满足[不满足:阻塞等待满足 满足:不阻塞]
2.释放已掌握的互斥量(解锁互斥量)相当于 pthread_mutex_unlock(&mutex);
[1,2为原子操作,不可分割]
3.当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁
pthread_mutex_lock(&mutex);
pthread_cond_timedwait函数
int pthread_cond_timewat(pthread_cond _t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
作用:限时等待一个条件变量
参数:
abstime是一个绝对时间,time(NULL)返回的就是一个绝对时间,
alarm(1)是相对时间,相对当前时间1s。
使用方法:
错误用法:
struct timespec t = {1,0};
pthread_cond_timedwait(&cond,&mutex,&t); //只能定时到 1970.1.1 00:00:01
正确用法:
time_t cur = time(NULL); //获取当前时间
struct timespec t;
t.tv_sec = cur+1; //定义一秒
pthread_cond_timedwait(&cond,&mutex,&t);
pthread_cond_signal函数
int pthread_cond_signal(pthread_cond_t *cond);
作用:唤醒一个或多个等待的线程
pthread_cond_broadcast函数
int pthread_cond_broadcast(pthread_cond_t *cond);
作用:通过广播唤醒所有的线程
案例见消费者生产者模型
生产者消费者模型
生产者模型中的对象:生产者、消费者、容器
一端专门生产商品,一端专门消费商品。那么如此以来就会有一些问题需要注意,当容器满了,生产者阻塞等待消费者消费商品使容器可以存放物品。当容器为空,则需要阻塞等待生产者生产物品并存放容器内。
进入容器的线程每一时刻保持只有一个(形成互斥关系,生产者与消费者保持同步关系)
为什么要讲生产者和消费者模型,因为在实际的项目开发中,这个模式用到的地方很多
生产者消费者模型实现(条件变量版)
线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,使比较方便的。假定有两个线程,一个模拟生产者的行为,一个模拟消费者的行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产者向其中添加商品,消费者从中消费产品。
因为程序中的插入删除操作比频繁(我就用链表来实现容器了)
#include <stdio.h> #include <unistd.h> #include <pthread.h> struct msg { struct msg *next; int num; }; struct msg *head; //静态方法初始化 pthread_cond_t cond = PTHREAD_COND_INITIALIZER; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //消费者 void* consumer(void *p) { struct msg *mp; while(1) { //取产品 pthread_mutex_lock(&mutex); while(head==NULL) //不能用 if 防止虚假唤醒 { pthread_cond_wait(&cond,&mutex); } mp = head; head = mp->next; pthread_mutex_unlock(&mutex); //消费产品 printf("消费:%d\n",mp->num); free(mp); sleep(rand()%5); } return NULL; } //生产者 void *product(void *arg) { struct msg *mp; while(1) { //生产产品 mp = malloc(sizeof(struct msg)); mp->num = rand()%200; printf("生产:%d\n",mp->num); //防止产品 pthread_mutex_lock(&mutex); mp->next = head; head = mp; pthread_mutex_unlock(&mutex); pthread_cond_signal(&cond); sleep(rand()%5); } return NULL; } int main(void) { pthread_t pid,cid; srand(time(NULL)); pthread_create(&pid,NULL,product,NULL); pthread_create(&cid,NULL,consumer,NULL); pthread_join(pid,NULL); pthread_join(cid,NULL); return 0; }
条件变量的优点:
相对于mutex而言,条件变量可以减少竞争。
如果直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间在的竞争是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争,提高程序效率。
信号量
由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一个对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并发执行,变成了串行执行。与直接使用单进程无异。
信号量是相对折中的处理方式,既能保证同步,数据不混乱,又能提高线程并发。
(进程间也可以使用哦)
信号量像是PV操作
相关函数
信号量的类型:sem_t
信号量的初始值,决定了占用信号量的线程个数
sem_init函数
int sem_init(sem_t *sem,int pshared,unsigned int value);
参数:
sem_t 信号量
pshared 0为线程,非0(一般为1)用于进程
value 指定信号量的初值
sem_destroy函数
int sem_destroy(sem_t *sem);
作用:销毁一个信号量
sem_wait函数
int sem_wait(sem_t *sem);
作用:先判断sem==0阻塞,value -1
对信号量的值减1,如果为0,阻塞
sem_trywait函数
int sem_trywait(sem_t *sem);
作用:尝试对信号量减1 (非阻塞)
sem_timedwait函数
sem_timedwait(sem_t *sem,const struct timespec *abs_timeout);
作用:限时尝试对信号量加锁 --
参数:abs_timeout是个绝对时间
定时1秒:
time_t cur = time(NULL); //获取当前时间
struct timespec t; //定义 timespec 结构体变量 t
t.tv_sec = sur + 1;
//t.tv_nsec = t.tv_sec + 100;
sem_timedwait(&sem,&t);
sem_post函数
int sem_post(sem_t *sem);
作用:对信号量加1
sem_getvalue函数
sem_getvalue(sem_t *sem,int *sval);
作用:获取当前信号量的值
案例见生产者消费者模型
生产者消费者模型实现(信号量版)
#include <stdio.h> #include <unistd.h> #include <pthread.h> #include <semaphore.h> #define NUM 5 int queue[NUM]; //缓冲区 sem_t produc_number,blank_number; void *product(void *arg) { int i = 0; while(1) { sem_wait(&blank_number); //缓冲区是否已满 queue[i] = rand()%100 + 1; //生产产品 printf("放缓冲区:%d\n",queue[i]); sem_post(&produc_number); //产品数 ++ i = (i+1)%NUM; sleep(rand()%3); } return NULL; } void *consumer(void *arg) { int i = 0; while(1) { sem_wait(&produc_number); //产品数量-- printf("取走缓冲区:%d\n",queue[i]); queue[i] = 0; sem_post(&blank_number); //格子数目++ i = (i+1)%NUM; //循环队列,下一个位置 sleep(rand()%3); } return NULL; } int main(void) { pthread_t pid,cid; sem_init(&blank_number,0,NUM); //初始时缓冲区空格子为5(或者说初始时生产者为5) sem_init(&produc_number,0,0); //初始时产品数为0(或者说消费者为0) pthread_create(&pid,NULL,product,NULL); pthread_create(&cid,NULL,consumer,NULL); pthread_join(pid,NULL); pthread_join(cid,NULL); //线程销毁 sem_destroy(&blank_number); sem_destroy(&produc_number); return 0; }
文件锁
借助fcntl函数来实现锁机制,操作文件的进程没有获取锁时,可以打开,但无法执行read、write操作,fcntl函数:获取、设置文件访问控制权限
int fcntl(int fd,int cmd,.../*arg*/);
参2:
F_SETLK(struct flock*) 设置文件锁(trylock)
F_SETLKW(struct flock*) 设置文件锁(lock) W-->wait
F_GETLK(struct flock*); 获取文件锁
参3:
struct flock{
...
short l_type; 锁的类型:F_RDLCK F_WRLCK F_UNLCK(解锁)
short l_whence; 偏移位置:SEEK_SET SEEK_CUR SEEK_END
short l_start; 起始位置:1000
short l_len; 长度:0表示整个问价加锁
short l_pid; 持有该锁的进程ID:F_GETLK
...
}
#include <stdio.h> #include <fcntl.h> #include <unistd.h> void sys_err(char *str) { perror(str); exit(-1); } int main(int arg,char *argv[]) { int fd; struct flock f_lock; if(arg < 2) { printf("./a.txt filename\n"); exit(1); } if(fd == open(argv[1],O_RDWR) < 0) { sys_err("open"); } // f_lock.l_type = F_WRLCK; //设置写锁 f_lock.l_type = F_RDLCK; //设置读锁 f_lock.l_whence = SEEK_SET; //文件头部 f_lock.l_start = 0; f_lock.l_len = 0; fcntl(fd,F_SETLKW,&f_lock); //上锁 printf("get flock\n"); sleep(10); f_lock.l_type = F_UNLCK; fcntl(fd,F_SETLK,&f_lock); //解锁 printf("un flock\n"); close(fd); return 0; }
依旧遵循“读共享,写独占”。但进程不加锁直接操作文件,依旧可以访问成功,但数据势必出现混乱。
多线程间共享文件描述符,而给文件加锁,是通过修改文件描述符所指向的文件结构体中的成员变量实现的,所以多线程无法使用文件锁。
更多推荐
所有评论(0)