多线程与线程同步 视频教程:爱编程的大丙
视频文档地址:爱编程的大丙
1.线程与进程区别
2.创建线程 2.1.线程函数 每一个线程都有一个唯一的线程ID
,ID
类型为pthread_t
,这个ID是一个无符号长整型数 ,通过以下函数获取当前线程ID
:
1 pthread_t pthread_self (void ) ;
在一个进程中调用线程创建函数,就可以得到一个子线程,和进程不同,需要给每一个线程指定一个处理函数 否则线程无法工作.创建线程(实际上就是确定调用该线程函数的入口点 )
1 2 3 4 #include <pthread.h> int pthread_create (pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void *),void *arg)
参数:
pthread
:传出参数,无符号长整型,线程创建成功,会将线程ID写入到这个指针指向的内存中
attr
: 线程的属性,一般情况写入默认值即可,NULL
start_routine
:函数指针,用于回调,指向的函数,就是在线程中需要执行的任务。即子线程运行函数 。创建出子线程处理的动作
该函数指针指向函数的参数为void *
,因此可以将任意类型的参数传递给子线程执行的函数
回调函数的类型应该为void *
类型
arg
:子线程运行函数的参数。作为实参传递到start_routine指针指向的函数内部
返回值
:0表示成功 -1表示失败
2.2实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <stdio.h> #include <stdlib.h> #include <pthread.h> void * callback (void *arg) { for (int i = 0 ; i < 5 ; i++) { printf ("子线程:i = %d\n" ,i); } printf ("子线程:%ld\n" ,pthread_self()); return NULL ; } int main (void ) { pthread_t tid; pthread_create(&tid,NULL ,callback,NULL ); for (int i = 0 ;i<5 ;i++) { printf ("主线程:i = %d\n" ,i); } printf ("主线程:%ld\n" ,pthread_self()); return 0 ; }
Linux中编译,需要连接pthread动态库:
1 2 3 gcc pthrad_create.c -lpthread -o app 执行: ./app
运行结果:
结果分析:
1 2 3 可以发现此时并没有执行子线程: 子线程被创建之后,运行需要抢CPU时间片,在子线程没有抢到CPU时间片之前,主线程以及执行完毕----将地址空间释放 此时子线程就无法在地址空间执行
解决方案:
#include <usistd.h>
//main函数中return 0前添加
sleep(3)函数; //让主线程等一会--主线程放弃CPU资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 * ```C //修改代码: #include<stdio.h> #include<stdlib.h> #include<pthread.h> #include <unistd.h> /* 程序执行逻辑: 1.main函数 2.主线程中创建子线程 3.执行主线程 4.执行子线程 */ //子线程执行任务的回调函数 void* callback(void *arg) { for (int i = 0; i < 5; i++) { printf("子线程:i = %d\n",i); } printf("子线程:%ld\n",pthread_self()); //pthread_self()返回当前线程ID---长整型 return NULL; } int main(void) { pthread_t tid; //子线程的线程ID pthread_create(&tid,NULL,callback,NULL); for (int i = 0;i<5;i++) { printf("主线程:i = %d\n",i); } printf("主线程:%ld\n",pthread_self()); sleep(3); //主线程休眠3s // 退出进程 exit(0); }
方案一执行结果:
3.线程退出 线程退出函数,主要使用在主线程里。若想要线程退出,但是不会导致虚拟地址空间的释放(针对主线程),就可以调用线程库中的线程退出函数 (调用该函数,当前线程就会立马退出,不会影响其他线程的执行,在子线程或者主线程都可以使用 )
1 2 #include <pthread.h> void pthread_exit (void *retval) ;
运行结果,主线程退出后,子线程仍然可以继续运行,每次执行的顺序会不一样这取决于CPU系统的调度
4.线程回收 4.1线程函数 线程与进程相似,子线程退出时其内核资源主要由主线程回收,线程库中提供的线程回收函数:phread_join()
,该函数是一个阻塞函数
(若子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源回收,函数被调用一次,只能回收一个子线程,如果多个子线程则需要进行循环回收)
通过线程回收函数还可以获得子线程退出时传递出来的数据
#include <pthread.h>
int pthread_join(pthread_t thread,void **retval)
//这是一个阻塞函数,子线程在运行该函数就会阻塞
//子线程退出,函数解除阻塞,回收对应子线程资源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 * **参数:** * thread : 要被回收的子线程的线程ID * retval : 二级指针,指向一级指针的地址,是一个传出参数,这个地址中存储了`pthread_exit()`传递出的数据,若不需要该参数可以指定为NULL * **返回值:** * 线程回收成功返回0,回收失败返回错误号 #### 4.2回收子线程数据 ##### 4.2.1定义全局/静态变量 ```c #include<stdio.h> #include<stdlib.h> #include<pthread.h> #include <unistd.h> struct Test { int num; int age; }; //子线程执行任务的回调函数 void* callback(void *arg) { for (int i = 0; i < 5; i++) { printf("子线程:i = %d\n",i); } printf("子线程:%ld\n",pthread_self()); struct Test t; t.num = 100; t.age = 6; //线程退出 pthread_exit(&t);//将结构体地址传给pthread_exit中的指针参数 //将该数据通过线程退出函数传递出去 } int main(void) { pthread_t tid; //子线程的线程ID pthread_create(&tid,NULL,callback,NULL); printf("主线程:%ld\n",pthread_self()); //定义一个一级指针,用于指向线程tid传递出的数据 void *ptr; //pthread_join()参数为一个二级指针,用于指向,上面的一级指针,二级指针变量存储的是一级指针的地址 pthread_join(tid,&ptr); //此时一级指针ptr指向子线程退出时,传回数据的地址 struct Test *pt = (struct Test *) ptr; printf("子线程age:%d\n",pt->age); printf("子线程num:%d\n", pt->num); exit(0); }
Linux中运行结果:
1 2 ggc pt.c -lpthread -o app ./app
结果分析:
1 2 此时的结果,在主线程中打印,子线程退出时,传回的数据,并没有我们预期的结果 ptr指针,指向的是,子线程传出的数据地址,该地址是子线程的一块(局部变量)栈内存,该块栈内存,被多个线程平均分成若干份占用,但是一个子线程退出,就会将该块内存退还,那么此时内存里的资源也会被释放。
代码修改:
1 2 3 4 struct Test t ;static struct Test t ;
运行结果:
4.2.2使用子线程栈 通过函数 pthread_exit()
; 可以得知,子线程退出的时候,需要将数据记录到一块内存中,通过参数传出的是存储数据的内存的地址,而不是具体数据,由因为参数是 void* 类型,所有这个万能指针可以指向任意类型的内存地址。先来看第一种方式,将子线程退出数据保存在子线程自己的栈区:
实例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> struct Persion { int id; char name[36 ]; int age; }; void * working (void * arg) { printf ("我是子线程, 线程ID: %ld\n" , pthread_self()); for (int i=0 ; i<9 ; ++i) { printf ("child == i: = %d\n" , i); if (i == 6 ) { struct Persion p ; p.age =12 ; strcpy (p.name, "tom" ); p.id = 100 ; pthread_exit(&p); } } return NULL ; } int main () { pthread_t tid; pthread_create(&tid, NULL , working, NULL ); printf ("子线程创建成功, 线程ID: %ld\n" , tid); printf ("我是主线程, 线程ID: %ld\n" , pthread_self()); for (int i=0 ; i<3 ; ++i) { printf ("i = %d\n" , i); } void * ptr = NULL ; pthread_join(tid, &ptr); struct Persion * pp = (struct Persion*)ptr; printf ("子线程返回数据: name: %s, age: %d, id: %d\n" , pp->name, pp->age, pp->id); printf ("子线程资源被成功回收...\n" ); return 0 ; }
编译并且执行测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 编译代码 $ gcc pthread_join.c -lpthread # 执行程序 $ ./a.out 子线程创建成功, 线程ID: 140652794640128 我是主线程, 线程ID: 140652803008256 i = 0 i = 1 i = 2 我是子线程, 线程ID: 140652794640128 child == i: = 0 child == i: = 1 child == i: = 2 child == i: = 3 child == i: = 4 child == i: = 5 child == i: = 6 子线程返回数据: name: , age: 0 , id: 0 子线程资源被成功回收...
结果分析:
1 2 3 4 5 通过打印的日志可以发现,在主线程中没有没有得到子线程返回的数据信息,具体原因是这样的: 如果多个线程共用同一个虚拟地址空间,每个线程在栈区都有一块属于自己的内存,相当于栈区被这几个线程平分了,当线程退出,线程在栈区的内存也就被回收了,因此随着子线程的退出,写入到栈区的数据也就被释放了。 因此在主线程中无法成功的将子线程中栈区的数据成功打印出来
4.2.3使用主线程栈 虽然每个线程都有属于自己的栈区空间,但是位于同一个地址空间的多个线程是可以相互访问对方的栈空间上的数据的。由于很多情况下还需要在主线程中回收子线程资源,所以主线程一般都是最后退出,基于这个原因在下面的程序中将子线程返回的数据保存到了主线程的栈区内存中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> struct Persion { int id; char name[36 ]; int age; }; void * working (void * arg) { struct Persion * p = (struct Persion*)arg; printf ("我是子线程, 线程ID: %ld\n" , pthread_self()); for (int i=0 ; i<9 ; ++i) { printf ("child == i: = %d\n" , i); if (i == 6 ) { p->age =12 ; strcpy (p->name, "tom" ); p->id = 100 ; pthread_exit(p); } } return NULL ; } int main () { pthread_t tid; struct Persion p ; pthread_create(&tid, NULL , working, &p); printf ("子线程创建成功, 线程ID: %ld\n" , tid); printf ("我是主线程, 线程ID: %ld\n" , pthread_self()); for (int i=0 ; i<3 ; ++i) { printf ("i = %d\n" , i); } void * ptr = NULL ; pthread_join(tid, &ptr); printf ("name: %s, age: %d, id: %d\n" , p.name, p.age, p.id); printf ("子线程资源被成功回收...\n" ); return 0 ; }
运行结果,发现子线程中对主线程栈区更新的数据成功打印出来了。
因为即使子线程退出了,但是并不影响主线程栈区的数据
5.线程分离 在某些情况下,程序中的主线程有属于自己的业务处理流程,若让主线程负责子线程的资源回收,调用pthread_join()
只要子线程不退出主线程就会一直阻塞,主线程的任务就不能被执行。
线程分离函数:pthread_detach()
函数,调用这个函数之后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并且回收(为主线程减负)。 线程分离之后在主线程中使用pthread_join()就无法回收到子线程的资源
线程分离函数:
1 2 3 #include <pthread.h> int pthread_detach (pthread_t thread) ;
实例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> struct Test { int num; int age; }; void * callback (void *arg) { for (int i = 0 ; i < 5 ; i++) { printf ("子线程:i = %d\n" ,i); } printf ("子线程:%ld\n" ,pthread_self()); return NULL ; } int main (void ) { pthread_t tid; pthread_create(&tid,NULL ,callback,NULL ); printf ("主线程:%ld\n" ,pthread_self()); pthread_detach(tid); pthread_exit(NULL ); return 0 ; }
编译运行结果 :
虽然子线程进行分离之后,不要在主线程进行子线程的资源回收,但是当子线程还在运行的时候,主线程提前exit退出了,内存地址空间都不存在了,子线程也不会存在。
6.其他线程函数 6.1线程取消 其含义是:在某些特定情况下在一个线程中杀死另外一个线程,使用这个函数杀死一个线程分两步:
在线程 A 中调用线程取消函数 pthread_cancel,指定杀死线程 B,这时候线程 B 是死不了的
在线程 B 中进行一次系统调用 (从用户区切换到内核区),否则线程 B 可以一直运行。
1 2 3 #include <pthread.h> int pthread_cancel (pthread_t thread) ;
实例代码:
示例代码中,主线程调用线程取消函数,只要在子线程中进行了系统调用,当子线程执行到这个位置就挂掉了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <stdio.h> #include <pthread.h> #include <string.h> #include <stdlib.h> #include <unistd.h> void *callback (void *arg) { int j = 0 ; for (int i = 0 ;i<9 ;i++) { j++; } printf ("子线程ID:%ld\n" ,pthread_self()); for (int i = 0 ; i < 9 ; i++) { printf ("child i:%ld\n" ,i); } return NULL ; } int main () { pthread_t tid; pthread_create(&tid, NULL , callback, NULL ); printf ("子线程创建成功,线程ID:%ld\n" ,tid); printf ("我是主线程,线程ID:%ld" ,pthread_self()); for (int i = 0 ; i < 3 ; i++) { printf ("i = %d\n" , i); } pthread_cancel(tid); pthread_exit(NULL ); return 0 ; }
运行结果:
1 可以发现,主线程任务执行后,调用pthread_cancel()函数,杀死子进程,在子进程的第一个for 循环,此时还不存在系统调用,但是当子线程中执行到printf ("子线程ID:%ld\n" ,pthread_self());后此时printf 调用了系统函数,在子线程间接进行了系统调用,成功杀死子进程,子进程第二个for 打印printf ("child i:%ld\n" ,i)就不再执行。
系统调用的方式:
1 2 1. 直接调用 Linux 系统函数2. 调用标准 C 库函数,为了实现某些功能,在 Linux 平台下标准 C 库函数会调用相关的系统函数
6.2线程ID比较 在Linux系统中线程ID本身就是一个无符号长整形,因此可以直接使用比较操作符比较两个线程ID,但是线程库是可以跨平台使用的,在某些平台上pthread_t可能不是一个单纯的整型,这种情况比较两个线程ID必须使用比较函数;
比较函数原型:
1 2 #include <pthread.h> int pthread_equal (pthread_t t1, pthread_t t2) ;
参数: t1 和 t2 是要比较的线程的线程 ID
返回值:如果两个线程 ID 相等返回非 0 值,如果不相等返回 0
7.线程同步 假设有 4 个线程 A、B、C、D,当前一个线程 A 对内存中的共享资源进行访问的时候,其他线程 B, C, D 都不可以对这块内存进行操作,直到线程 A 对这块内存访问完毕为止,B,C,D 中的一个才能访问这块内存,剩余的两个需要继续阻塞等待,以此类推,直至所有的线程都对这块内存操作完毕。 线程对内存的这种访问方式就称之为线程同步,通过对概念的介绍,我们可以了解到所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。
7.1为什么要线程同步 在研究线程同步之前,先来看一个两个线程交替数数(每个线程数 50 个数,交替数到 100)的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <pthread.h> #define MAX 50 int number;void *funA_num (void *arg) { for (int i = 0 ; i < MAX; i++) { int cur = number; cur++; number = cur; printf ("Thread A,id = %ld,number = %d\n" ,pthread_self(),number); } return NULL ; } void *funB_num (void *arg) { for (int i = 0 ; i < MAX; i++) { int cur = number; cur++; number = cur; printf ("Thread B,id = %ld,number = %d\n" , pthread_self(), number); sleep(5 ); } return NULL ; } int main (int argc,char *argv) { pthread_t p1, p2; pthread_create(&p1,NULL ,funA_num,NULL ); pthread_create(&p2, NULL , funB_num, NULL ); pthread_join(p1,NULL ); pthread_join(p2,NULL ); return 0 ; }
代码结果:
结果分析:
1 2 3 4 5 6 通过对上面例子的测试,可以看出虽然每个线程内部循环了 50 次每次数一个数,但是最终没有数到 100 (最后还是数到了100 ),通过输出的结果可以看到,有些数字被重复数了多次,其原因就是没有对线程进行同步处理,造成了数据的混乱。 在测试程序中两个线程共用全局变量 number 当线程变成运行态之后开始数数,从物理内存加载数据,让后将数据放到 CPU 进行运算,最后将结果更新到物理内存中。如果数数的两个线程都可以顺利完成这个流程,那么得到的结果肯定是正确的。 如果线程 A 执行这个过程期间就失去了 CPU 时间片,线程 A 被挂起了最新的数据没能更新到物理内存。线程 B 变成运行态之后从物理内存读数据,很显然它没有拿到最新数据,只能基于旧的数据往后数,然后失去 CPU 时间片挂起。线程 A 得到 CPU 时间片变成运行态,第一件事儿就是将上次没更新到内存的数据更新到内存,但是这样会导致线程 B 已经更新到内存的数据被覆盖,活儿白干了,最终导致有些数据会被重复数很多次。
两个线程在数数的时候需要分时复用CPU时间片,并且测试程序中嗲用sleep()导致线程的CPU时间片没用完就被挂起,这让CPU的上下文切换(保存当前状态,下一次运行时,需要加载保存的状态)更为频繁 。更容易造成数据混乱的现象
CPU 对应寄存器、一级缓存、二级缓存、三级缓存是独占的,用于存储处理的数据和线程的状态信息,数据被 CPU 处理完成需要再次被写入到物理内存中,物理内存数据也可以通过文件 IO 操作写入到磁盘中。
7.2同步方式 对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步。常用的线程同步方式有四种:互斥锁、读写锁、条件变量、信号量 。所谓的共享资源 就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量 ,这些变量对应的共享资源也被称之为临界资源 。
找到临界资源之后,再找和临界资源相关的上下文代码,这样就得到了一个代码块,这个代码块可以称之为临界区。确定好临界区(临界区越小越好)之后,就可以进行线程同步了,线程同步的大致处理思路是这样的:
在临界区代码的上边,添加加锁函数,对临界区加锁。
哪个线程调用这句代码,就会把这把锁锁上,其他线程就只能阻塞在锁上了。
在临界区代码的下边,添加解锁函数,对临界区解锁。
出临界区的线程会将锁定的那把锁打开,其他抢到锁的线程就可以进入到临界区了。
通过锁机制能保证临界区代码最多只能同时有一个线程访问 ,这样并行访问就变为串行访问了
8.互斥锁 8.1互斥锁函数 互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块,被锁定的这个代码块,所有的线程只能顺序执行 (不能并行处理),这样多线程访问共享资源数据混乱的问题就可以被解决了,需要付出的代价就是执行效率的降低,因为默认临界区多个线程是可以并行处理的,现在只能串行处理
在Linux中互斥锁的类型为pthread_mutex_t
,创建一个这种类型的变量就得到了一把互斥锁:
在创建的锁对象中保存了当前这把锁的状态信息:锁定还是打开,如果是锁定状态还记录了给这把锁加锁的线程信息(线程 ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被解锁 ,被阻塞的线程才能被解除阻塞。一般情况下,每一个共享资源对应一个把互斥锁,锁的个数和线程的个数无关。
互斥锁函数操作原型:
1 2 3 4 5 int pthread_mutex_init (pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr) ;int pthread_mutex_destory (pthread_mutex_t *mutex) ;
参数:
mutex : 互斥锁变量的地址
attr : 互斥锁属性,一般默认即可,这个参数指定为NULL
返回值:
如果函数调用成功会返回 0,调用失败会返回相应的错误号
1 2 int pthread_mutex_lock (pthread_mutex_t *mutex) ;
这个函数被调用,首先会判断参数 mutex 互斥锁中的状态是不是锁定状态:
没有被锁定,是打开的,这个线程可以加锁成功,这个这个锁中会记录是哪个线程加锁成功了
如果被锁定了,其他线程加锁就失败了,这些线程都会阻塞在这把锁上
当这把锁被解开之后,这些阻塞在锁上的线程就解除阻塞了,并且这些线程是通过竞争的方式对这把锁加锁,没抢到锁的线程继续阻塞
1 2 int pthread_mutex_trylock (pthread_mutex_t *mutex) ;
调用这个函数对互斥锁变量加锁还是有两种情况:
如果这把锁没有被锁定是打开的,线程加锁成功
如果锁变量被锁住了,调用这个函数加锁的线程,不会被阻塞,加锁失败直接返回错误号
1 2 int pthread_mutex_unlock (pthread_mutex_t *mutex) ;
不是所有的线程都可以对互斥锁解锁,哪个线程加的锁,哪个线程才能解锁成功。
8.2互斥锁的使用 将7.1代码进行修改,两个线程操作同一个全局变量 ,number
,因此需要添加一把互斥锁,来控制两个线程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <pthread.h> #define MAX 50 int number;pthread_mutex_t mutex;void *funA_num (void *arg) { for (int i = 0 ; i < MAX; i++) { pthread_mutex_lock(&mutex); int cur = number; cur++; usleep(5 ); number = cur; pthread_mutex_unlock(&mutex); printf ("Thread A,id = %ld,number = %d\n" ,pthread_self(),number); } pthread_exit(NULL ); } void *funB_num (void *arg) { for (int i = 0 ; i < MAX; i++) { pthread_mutex_lock(&mutex); int cur = number; cur++; usleep(10 ); number = cur; printf ("Thread B,id = %ld,number = %d\n" , pthread_self(), number); pthread_mutex_unlock(&mutex); } pthread_exit(NULL ); } int main (int argc,char *argv) { pthread_t p1, p2; pthread_mutex_init(&mutex,NULL ); pthread_create(&p1,NULL ,funA_num,NULL ); pthread_create(&p2, NULL , funB_num, NULL ); pthread_join(p1,NULL ); pthread_join(p2,NULL ); pthread_mutex_destroy(&mutex); return 0 ; }
运行结果:
线程A\B一起数数到了100,但是并非完全交替数数,因为多个线程抢占CPU是随机的是一个概率问题
8.3死锁 当多个线程访问共享资源,需要加锁,如果锁使用不当,就会造成死锁这种现象。如果线程死锁造成的后果是:所有的线程都被阻塞,并且线程的阻塞是无法解开的(因为可以解锁的线程也被阻塞了)。
造成死锁的场景有如下几种:
加锁之后忘记解锁
// 场景1
void func()
{
for(int i=0; i<6; ++i)
{
// 当前线程A加锁成功, 当前循环完毕没有解锁, 在下一轮循环的时候自己被阻塞了
// 其余的线程也被阻塞
pthread_mutex_lock(&mutex);
....
.....
// 忘记解锁
}
}
// 场景2
void func()
{
for(int i=0; i<6; ++i)
{
// 当前线程A加锁成功
// 其余的线程被阻塞
pthread_mutex_lock(&mutex);
....
.....
if(xxx)
{
// 函数退出, 没有解锁(解锁函数无法被执行了)
return ;
}
pthread_mutex_lock(&mutex);
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 * 重复加锁,造成死锁 * ```c void func() { for(int i=0; i<6; ++i) { // 当前线程A加锁成功 // 其余的线程阻塞 pthread_mutex_lock(&mutex); // 锁被锁住了, A线程阻塞 pthread_mutex_lock(&mutex); .... ..... pthread_mutex_unlock(&mutex); } } // 隐藏的比较深的情况 void funcA() { for(int i=0; i<6; ++i) { // 当前线程A加锁成功 // 其余的线程阻塞 pthread_mutex_lock(&mutex); .... ..... pthread_mutex_unlock(&mutex); } } void funcB() { for(int i=0; i<6; ++i) { // 当前线程A加锁成功 // 其余的线程阻塞 pthread_mutex_lock(&mutex); funcA(); //在函数B中调用函数A 重复加锁 .... ..... pthread_mutex_unlock(&mutex); } }
在程序中有多个共享资源,因此有很多把锁,随意加锁,导致相互被阻塞
场景描述:
1.有两个共享资源:X, Y,X对应锁A, Y对应锁B
线程A访问资源X, 加锁A
线程B访问资源Y, 加锁B
2.线程A要访问资源Y, 线程B要访问资源X,因为资源X和Y已经被对应的锁锁住了,因此这个两个线程被阻塞
线程A被锁B阻塞了, 无法打开A锁
线程B被锁A阻塞了, 无法打开B锁
避免死锁
避免多次锁定,多检查
对共享资源访问完毕之后,一定要解锁,或者在加锁的使用 trylock
如果程序中有多把锁,可以控制对锁的访问顺序 (顺序访问共享资源,但在有些情况下是做不到的),另外也可以在对其他互斥锁做加锁操作之前,先释放当前线程拥有的互斥锁。
项目程序中可以引入一些专门用于死锁检测的模块
9.读写锁 读写锁是互斥锁的升级版 ,在做读操作的时候可以提高程序的执行效率 ,如果所有的线程都是做读操作, 那么读是并行的,但是使用互斥锁,读操作也是串行的。
使用读写锁 读:并行 写:串行
读写锁是一把锁 ,锁的类型为 pthread_rwlock_t
1 2 pthread_rwlock_t rwlock;
读写锁可以做两件事情,既可以锁定读操作 ,可以锁定写操作
以下是读写锁中记录的信息 :
锁的状态:锁定 / 打开
锁定的是什么操作:读操作 / 写操作,使用读写锁锁定了读操作,需要先解锁才能去锁定写操作,反之亦然。
哪个线程将这把锁锁上了
读写锁的使用方式也互斥锁的使用方式是完全相同的:找共享资源,确定临界区,在临界区的开始位置加锁(读锁 / 写锁),临界区的结束位置解锁 。
读写锁的特点:
使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的。
使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的(与互斥锁一样)。
使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问者两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高。
读写锁函数操作原型:
1 2 3 4 5 6 7 #include <pthread.h> pthread_rwlock_t rwlock;int pthread_rwlock_init (pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr) ;int pthread_rwlock_destroy (pthread_rwlock_t *rwlock) ;
1 2 int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock) ;
调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数的线程会被阻塞 。
1 2 3 int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock) ;
调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数加锁失败,对应的线程不会被阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作。
1 2 int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock) ;
调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数的线程会被阻塞 。
1 2 3 int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock) ;
调用这个函数,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数加锁失败,但是线程不会阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作。
解锁:
1 2 int pthread_rwlock_unlock (pthread_rwlock_t *rwlock) ;
9.1读写锁的使用 题目要求:8 个线程操作同一个全局变量,3 个线程不定时写同一全局资源,5 个线程不定时读同一全局资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> int number = 0 ;pthread_rwlock_t rwlock;void * writeNum (void * arg) { while (1 ) { pthread_rwlock_wrlock(&rwlock); int cur = number; cur ++; number = cur; printf ("++写操作完毕, number : %d, tid = %ld\n" , number, pthread_self()); pthread_rwlock_unlock(&rwlock); sleep(1 ); } pthread_exit(NULL ); } void * readNum (void * arg) { while (1 ) { pthread_rwlock_rdlock(&rwlock); printf ("--全局变量number = %d, tid = %ld\n" , number, pthread_self()); pthread_rwlock_unlock(&rwlock); sleep(1 ); } pthread_exit(NULL ); } int main () { pthread_rwlock_init(&rwlock, NULL ); pthread_t wtid[3 ]; pthread_t rtid[5 ]; for (int i=0 ; i<3 ; ++i) { pthread_create(&wtid[i], NULL , writeNum, NULL ); } for (int i=0 ; i<5 ; ++i) { pthread_create(&rtid[i], NULL , readNum, NULL ); } for (int i=0 ; i<3 ; ++i) { pthread_join(wtid[i], NULL ); } for (int i=0 ; i<5 ; ++i) { pthread_join(rtid[i], NULL ); } pthread_rwlock_destroy(&rwlock); return 0 ; }
运行结果
读操作可以并行。但是写操作只能串行,每次写操作造成number
均会变化,实现线程同步
10.条件变量 10.1条件变量函数 严格意义上来说,条件变量的主要作用不是处理线程同步,而是进行线程的阻塞 。如果在多线程程序中只使用条件变量无法实现线程的同步,必须要配合互斥锁来使用。虽然条件变量和互斥锁都能阻塞线程,但是二者的效果是不一样的,二者的区别如下:
假设有 A-Z 26 个线程,这 26 个线程共同访问同一把互斥锁,如果线程 A 加锁成功,那么其余 B-Z 线程访问互斥锁都阻塞,所有的线程只能顺序访问临界区
条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,这种情况下还是会出现共享资源中数据的混乱。
一般情况下条件变量用于处理生产者和消费者模型,并且和互斥锁配合使用。条件变量类型对应的类型为pthread_cond_t
,这样就可以定义一个条件变量类型的变量了:
被条件变量阻塞的线程的线程信息会被记录到这个变量中,以便在解除阻塞的时候使用 。
条件变量操作原型如下:
1 2 3 4 5 6 7 #include <pthread.h> pthread_cond_t cond;int pthread_cond_init (pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr) ;int pthread_cond_destroy (pthread_cond_t *cond) ;
参数 :
cond
: 条件变量的地址
attr
: 条件变量属性,一般使用默认属性,指定为 NULL
线程阻塞函数:
1 2 int pthread_cond_wait (pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex) ;
通过函数原型可以看出,该函数在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁主要功能是进行线程同步,让线程顺序进入临界区,避免出现数共享资源的数据混乱。该函数会对这个互斥锁做以下几件事情:
在阻塞线程时候,如果线程已经对互斥锁 mutex
上锁,那么会将这把锁打开,这样做是为了避免死锁
当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个mutex
互斥锁锁上,继续向下访问临界区
1 2 3 4 5 6 7 8 struct timespec { time_t tv_sec; long tv_nsec; }; int pthread_cond_timedwait (pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime) ;
这个函数的前两个参数和 pthread_cond_wait
函数是一样的,第三个参数表示线程阻塞的时长,但是需要额外注意一点:struct timespec
这个结构体中记录的时间是从1971.1.1到某个时间点的时间,总长度使用秒/纳秒表示。因此赋值方式相对要麻烦一点:
1 2 3 4 time_t mytim = time(NULL ); struct timespec tmsp ;tmsp.tv_nsec = 0 ; tmsp.tv_sec = time(NULL ) + 100 ;
1 2 3 4 int pthread_cond_signal (pthread_cond_t *cond) ;int pthread_cond_broadcast (pthread_cond_t *cond) ;
调用上面两个函数中的任意一个,都可以换线被 pthread_cond_wait
或者pthread_cond_timedwait
阻塞的线程,区别就在于 pthread_cond_signal
是唤醒至少一个被阻塞的线程(总个数不定),pthread_cond_broadcast
是唤醒所有被阻塞的线程。
10.2生产者和消费者 生产者和消费者模型的组成:
生产者线程->若干个
生产商品或者任务放入到任务队列中
任务队列满了就阻塞,不满的时候就工作
通过一个生产者的条件变量控制生产者线程阻塞和非阻塞
消费者线程->若干个
读任务队列,将任务或者数据取出
任务队列中有数据就消费,没有数据就阻塞
通过一个消费者的条件变量控制消费者线程阻塞和非阻塞
队列 -> 存储任务 / 数据,对应一块内存,为了读写访问可以通过一个数据结构维护这块内存
可以是数组、链表,也可以使用 stl
容器:queue /stack/list/vector
1 场景描述:使用条件变量实现生产者和消费者模型,生产者有 5 个,往链表头部添加节点,消费者也有 5 个,删除链表头部的节点
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> struct Node { int number; struct Node *next ; }; pthread_cond_t cond;pthread_mutex_t mutex;struct Node *head = NULL ;void *producer (void *arg) { while (1 ) { pthread_mutex_lock(&mutex); struct Node *pnew = (struct Node*) malloc (sizeof (struct Node)); pnew->number = rand() % 100 ; pnew->next = head; head = pnew; printf ("+++producer, number = %d, tid = %ld\n" , pnew->number, (pthread_self() % 100 )); pthread_mutex_unlock(&mutex); sleep(rand() % 3 ); pthread_cond_broadcast(&cond); } pthread_exit(NULL ); } void *consumer (void *arg) { if (1 ) { pthread_mutex_lock(&mutex); if (head == NULL ) { pthread_cond_wait(&cond, &mutex); } struct Node * pnode = head; printf ("--consumer: number: %d, tid = %ld\n" , pnode->number, (pthread_self() % 100 )); head = pnode->next; free (pnode); pthread_mutex_unlock(&mutex); sleep(rand() % 3 ); } pthread_exit(NULL ); } int main (int argc,char **argv) { pthread_cond_init(&cond, NULL ); pthread_mutex_init(&mutex, NULL ); pthread_t ptid[5 ]; pthread_t ctid[5 ]; for (int i=0 ; i<5 ; ++i) { pthread_create(&ptid[i], NULL , producer, NULL ); } for (int i=0 ; i<5 ; ++i) { pthread_create(&ctid[i], NULL , consumer, NULL ); } for (int i=0 ; i<5 ; ++i) { pthread_join(ptid[i], NULL ); } for (int i=0 ; i<5 ; ++i) { pthread_join(ctid[i], NULL ); } pthread_cond_destroy(&cond); pthread_mutex_destroy(&mutex); return 0 ; }
代码分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 void * consumer (void * arg) { while (1 ) { pthread_mutex_lock(&mutex); if (head == NULL ) { pthread_cond_wait(&cond, &mutex); } struct Node * pnode = head; printf ("--consumer: number: %d, tid = %ld\n" , pnode->number, pthread_self()); head = pnode->next; free (pnode); pthread_mutex_unlock(&mutex); sleep(rand() % 3 ); } return NULL ; }
代码改进
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> struct Node { int number; struct Node *next ; }; pthread_cond_t cond;pthread_mutex_t mutex;struct Node *head = NULL ;void *producer (void *arg) { while (1 ) { pthread_mutex_lock(&mutex); struct Node *pnew = (struct Node*) malloc (sizeof (struct Node)); pnew->number = rand() % 100 ; pnew->next = head; head = pnew; printf ("+++producer, number = %d, tid = %ld\n" , pnew->number, (pthread_self() % 100 )); pthread_mutex_unlock(&mutex); sleep(rand() % 3 ); pthread_cond_broadcast(&cond); } pthread_exit(NULL ); } void *consumer (void *arg) { if (1 ) { pthread_mutex_lock(&mutex); while (head == NULL ) { pthread_cond_wait(&cond, &mutex); } struct Node * pnode = head; printf ("--consumer: number: %d, tid = %ld\n" , pnode->number, (pthread_self() % 100 )); head = pnode->next; free (pnode); pthread_mutex_unlock(&mutex); sleep(rand() % 3 ); } pthread_exit(NULL ); } int main (int argc,char **argv) { pthread_cond_init(&cond, NULL ); pthread_mutex_init(&mutex, NULL ); pthread_t ptid[5 ]; pthread_t ctid[5 ]; for (int i=0 ; i<5 ; ++i) { pthread_create(&ptid[i], NULL , producer, NULL ); } for (int i=0 ; i<5 ; ++i) { pthread_create(&ctid[i], NULL , consumer, NULL ); } for (int i=0 ; i<5 ; ++i) { pthread_join(ptid[i], NULL ); } for (int i=0 ; i<5 ; ++i) { pthread_join(ctid[i], NULL ); } pthread_cond_destroy(&cond); pthread_mutex_destroy(&mutex); return 0 ; }
若队伍队列的大小存在限制,则需要对生产者使用条件变量
消费者与生产者需要使用不同的条件变量对各自阻塞的线程进行唤醒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> pthread_cond_t producer_cond;pthread_cond_t consumer_cond;pthread_mutex_t mutex;#define MAX 100 int count = 0 ;struct Node { int number; struct Node * next ; }; struct Node *head = NULL ;void *producer (void *arg) { while (1 ) { pthread_mutex_lock(&mutex); while (count >= MAX) { pthread_cond_wait(&producer_cond, &mutex); } struct Node *newnode = malloc (sizeof (struct Node)); newnode->number = rand() % 1000 ; newnode->next = head; head = newnode; count++; printf ("生产者,id:%ld , number:%d , count = %d\n" , (pthread_self() % 100 ), newnode->number, count); pthread_mutex_unlock(&mutex); pthread_cond_broadcast(&consumer_cond); sleep(rand() % 3 ); } return NULL ; } void *consumer (void *arg) { while (1 ) { pthread_mutex_lock(&mutex); while (head == NULL ) { pthread_cond_wait(&consumer_cond, &mutex); } struct Node *node = head; head = head->next; free (node); count--; printf ("消费者,id:%ld , number:%d , count = %d\n" , (pthread_self() % 100 ), node->number, count); pthread_cond_broadcast(&producer_cond); pthread_mutex_unlock(&mutex); sleep(rand() % 3 ); } return NULL ; } int main () { pthread_mutex_init(&mutex, NULL ); pthread_cond_init(&producer_cond, NULL ); pthread_cond_init(&consumer_cond, NULL ); pthread_t t1[5 ], t2[5 ]; for (int i = 0 ; i < 5 ; i++) { pthread_create(&t1[i], NULL , producer, NULL ); } for (int i = 0 ; i < 5 ; i++) { pthread_create(&t2[i], NULL , consumer, NULL ); } for (int i = 0 ; i < 5 ; i++) { pthread_join(t1[i], NULL ); pthread_join(t2[i], NULL ); } pthread_mutex_destroy(&mutex); pthread_cond_destroy(&consumer_cond); pthread_cond_destroy(&producer_cond); return 0 ; }
运行结果
11.信号量 11.1信号量函数 信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。
信号量(信号灯) 与互斥锁和条件变量的主要不同在于”灯”的概念,灯亮则意味着资源可用,灯灭则意味着不可用。信号量主要阻塞线程, 不能完全保证线程安全,如果要保证线程安全, 需要信号量和互斥锁一起使用。
信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程或者消费者线程的运行。信号的类型为sem_t
对应的头文件为<semaphore.h>
:
1 2 #include <semaphore.h> sem_t sem;
信号量操作函数:
1 2 3 4 5 6 #include <semaphore.h> int sem_init (sem_t *sem, int pshared, unsigned int value) ;int sem_destroy (sem_t *sem) ;
1 2 3 int sem_wait (sem_t *sem) ;
当线程调用这个函数,并且sem
中的资源数>0
,线程不会阻塞,线程会占用sem
中的一个资源,因此资源数-1,直到sem
中的资源数减为0
时,资源被耗尽,因此线程也就被阻塞了。
1 2 3 int sem_trywait (sem_t *sem) ;
当线程调用这个函数,并且sem
中的资源数>0
,线程不会阻塞,线程会占用sem
中的一个资源,因此资源数-1,直到sem
中的资源数减为0
时,资源被耗尽,但是线程不会被阻塞,直接返回错误号,因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况。
1 2 3 4 5 6 7 8 struct timespec { time_t tv_sec; long tv_nsec; }; int sem_timedwait (sem_t *sem, const struct timespec *abs_timeout) ;
该函数的参数abs_timeout
和pthread_cond_timedwait
的最后一个参数是一样的,使用方法不再过多赘述。当线程调用这个函数,并且sem
中的资源数>0
,线程不会阻塞,线程会占用sem
中的一个资源,因此资源数-1,直到sem
中的资源数减为0
时,资源被耗尽,线程被阻塞,当阻塞指定的时长之后,线程解除阻塞。
1 2 int sem_post (sem_t *sem) ;
调用该函数会将sem
中的资源数+1
,如果有线程在调用sem_wait
、sem_trywait
、sem_timedwait
时因为sem
中的资源数为0
被阻塞了,这时这些线程会解除阻塞,获取到资源之后继续向下运行。
1 2 3 int sem_getvalue (sem_t *sem, int *sval) ;
通过这个函数可以查看sem
中现在拥有的资源个数,通过第二个参数sval
将数据传出,也就是说第二个参数的作用和返回值是一样的。
11.2生产者与消费者 由于生产者和消费者是两类线程,并且在还没有生成之前是不能进行消费的,在使用信号量处理这类问题的时候可以定义两个信号量,分别用于记录生产者和消费者线程拥有的总资源数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 sem_t psem;sem_t csem;sem_init(&psem, 0 , 5 ); sem_init(&csem, 0 , 0 ); sem_wait(&psem); ...... ...... ...... sem_post(&csem); sem_wait(&csem); ...... ...... ...... sem_post(&psem);
通过上面的代码可以知道,初始化信号量的时候没有消费者分配资源,消费者线程启动之后由于没有资源自然就被阻塞了,等生产者生产出产品之后,再给消费者分配资源,这样二者就可以配合着完成生产和消费流程了。
11.3信号量的使用 如果生产者和消费者线程使用的信号量对应的总资源数为1 ,那么不管线程有多少个,可以工作的线程只有一个,其余线程由于拿不到资源,都被迫阻塞了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <semaphore.h> #include <pthread.h> struct Node { int number; struct Node * next ; }; sem_t psem;sem_t csem;pthread_mutex_t mutex;
11.3.1总资源数为1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> #include <semaphore.h> sem_t semp;sem_t semc;pthread_mutex_t mutex;struct Node { int number; struct Node * next ; }; struct Node *head = NULL ;void *producer (void *arg) { while (1 ) { sem_wait(&semp); struct Node *newnode = malloc (sizeof (struct Node)); newnode->number = rand() % 1000 ; newnode->next = head; head = newnode; printf ("生产者,id:%ld , number:%d\n" , (pthread_self() % 100 ), newnode->number); sem_post(&semc); sleep(rand() % 3 ); } return NULL ; } void *consumer (void *arg) { while (1 ) { sem_wait(&semc); struct Node *node = head; printf ("消费者,id:%ld , number:%d\n" , (pthread_self() % 100 ), node->number); head = head->next; free (node); sem_post(&semp); sleep(rand() % 3 ); } return NULL ; } int main () { pthread_mutex_init(&mutex, NULL ); sem_init(&semp, 0 , 1 ); sem_init(&semc, 0 , 0 ); pthread_t t1[5 ], t2[5 ]; for (int i = 0 ; i < 5 ; i++) { pthread_create(&t1[i], NULL , producer, NULL ); } for (int i = 0 ; i < 5 ; i++) { pthread_create(&t2[i], NULL , consumer, NULL ); } for (int i = 0 ; i < 5 ; i++) { pthread_join(t1[i], NULL ); pthread_join(t2[i], NULL ); } pthread_mutex_destroy(&mutex); sem_destroy(&semp); sem_destroy(&semc); return 0 ; }
通过测试代码可以得到如下结论:如果生产者和消费者使用的信号量总资源数为1,那么不会出现生产者线程和消费者线程同时访问共享资源的情况,不管生产者和消费者线程有多少个,它们都是顺序执行的。
运行结果
可以看到生产者与消费者交替运行
11.3.2总资源数大于1 如果生产者和消费者线程使用的信号量对应的总资源数为大于1,这种场景下出现的情况就比较多了:
多个生产者线程同时生产
多个消费者同时消费
生产者线程和消费者线程同时生产和消费
以上不管哪一种情况都可能会出现多个线程访问共享资源的情况,如果想防止共享资源出现数据混乱,那么就需要使用互斥锁进行线程同步,线程线性执行任务 ,处理代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <semaphore.h> #include <pthread.h> struct Node { int number; struct Node * next ; }; sem_t psem;sem_t csem;pthread_mutex_t mutex;
在编写上述代码的时候还有一个需要注意是事项,不管是消费者线程的处理函数还是生产者线程的处理函数内部有这么两行代码:
1 2 3 4 5 6 7 sem_wait(&csem); pthread_mutex_lock(&mutex); sem_wait(&csem); pthread_mutex_lock(&mutex);
这两行代码的调用顺序是不能颠倒的,如果颠倒过来就有可能会造成死锁 ,下面来分析一种死锁的场景:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void * producer (void * arg) { while (1 ) { pthread_mutex_lock(&mutex); sem_wait(&psem); ...... ...... sem_post(&csem); pthread_mutex_unlock(&mutex); sleep(rand() % 3 ); } return NULL ; } void * consumer (void * arg)
在上面的代码中,初始化状态下消费者线程没有任务信号量资源,假设某一个消费者线程先运行,调用pthread_mutex_lock(&mutex);
对互斥锁加锁成功,然后调用sem_wait(&csem);
由于没有资源,因此被阻塞了。其余的消费者线程由于没有抢到互斥锁,因此被阻塞在互斥锁上。对应生产者线程第一步操作也是调用pthread_mutex_lock(&mutex);
,但是这时候互斥锁已经被消费者线程锁上了,所有生产者都被阻塞,到此为止,多余的线程都被阻塞了,程序产生了死锁。
完整代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <pthread.h> #include <semaphore.h> sem_t semp;sem_t semc;pthread_mutex_t mutex;struct Node { int number; struct Node * next ; }; struct Node *head = NULL ;void *producer (void *arg) { while (1 ) { sem_wait(&semp); pthread_mutex_lock(&mutex); struct Node *newnode = malloc (sizeof (struct Node)); newnode->number = rand() % 1000 ; newnode->next = head; head = newnode; printf ("生产者,id:%ld , number:%d\n" , (pthread_self() % 100 ), newnode->number); pthread_mutex_unlock(&mutex); sem_post(&semc); sleep(rand() % 3 ); } return NULL ; } void *consumer (void *arg) { while (1 ) { sem_wait(&semc); pthread_mutex_lock(&mutex); struct Node *node = head; printf ("消费者,id:%ld , number:%d\n" , (pthread_self() % 100 ), node->number); head = head->next; free (node); pthread_mutex_unlock(&mutex); sem_post(&semp); sleep(rand() % 3 ); } return NULL ; } int main () { pthread_mutex_init(&mutex, NULL ); sem_init(&semp, 0 , 5 ); sem_init(&semc, 0 , 0 ); pthread_t t1[5 ], t2[5 ]; for (int i = 0 ; i < 5 ; i++) { pthread_create(&t1[i], NULL , producer, NULL ); } for (int i = 0 ; i < 5 ; i++) { pthread_create(&t2[i], NULL , consumer, NULL ); } for (int i = 0 ; i < 5 ; i++) { pthread_join(t1[i], NULL ); pthread_join(t2[i], NULL ); } pthread_mutex_destroy(&mutex); sem_destroy(&semp); sem_destroy(&semc); return 0 ; }
运行结果