Linux下实现多线程的生产者消费者问题
一、原理的理解
生产者-消费者问题是一个经典的线程同步问题,该问题最早由Dijkstra提出,用以演示他提出的信号量机制。在同一个线程地址空间内执行的两个线程。生产者线程生产物品,然后将物品放置在一个空缓冲区中供消费者线程消费。消费者线程从缓冲区中获得物品,然后释放缓冲区。当生产者线程生产物品时,如果没有空缓冲区可用,那么生产者线程必须等待消费者线程释放出一个空缓冲区。当消费者线程消费物品时,如果没有满的缓冲区,那么消费者线程将被阻塞,直到新的物品被生产出来。
多个生产/消费者在有界缓冲上操作。它利用N个字节的共享内存作为有界循环缓冲区,利用写一字符模拟放一个产品,利用读一字符模拟消费一个产品。当缓冲区空时消费者应阻塞睡眠,而当缓冲区满时生产者应当阻塞睡眠。一旦缓冲区中有空单元,生产者线程就向空单元中入写字符,并报告写的内容和位置。一旦缓冲区中有未读过的字符,消费者线程就从该单元中读出字符,并报告读取位置。生产者不能向同一单元中连续写两次以上相同的字符,消费者也不能从同一单元中连续读两次以上相同的字符。
二、完成步骤
1、思路分析
本作业是完善课件上的线程综合实例的练习生产者-消费者问题,重构这个程序的框架,完成性能分析,使之进一步理解掌握Linux下线程的同步、通信以及互斥和多线程的安全问题。
一般情况下,解决互斥方法常用信号量和互斥锁,即semaphore和mutex,而解决这个问题,多采用一个类似资源槽的结构,每个槽位标示了指向资源的指针以及该槽位的状态,生产者和消费者互斥查询资源槽,判断是否有产品或者有空位可以生产,然后根据指针进行相应的操作。同时,为了告诉生产者或者消费者资源槽的情况,还要有一个消息传送机制,无论是管道还是线程通信。
然而,本次试验有几个特殊的要求:
A、循环缓冲。
B、除了stderr,stdout等外,只用小于2个的互斥锁、
C、放弃资源槽分配机制,采用额外的数据结构。
D、生产者一直持续生产,形成生产消费的良性循环。
首先,使用一个互斥锁,意味着资源槽机制就不能使用了。因为资源槽虽以用一个互斥锁完成,但是需要有额外的通信,如果使用管道通信,则管道也必须是互斥,这就不满足1个互斥锁的要求。其次,要求生产者一直生产,这就否定了另外一种方法:消费者、生产者的位置均平等,消费者消费的时候生产者不能生产,生产者生产的时候消费者不能消费。因此,就需要采用A要求,也就是循环链表的形式。
为了保证互斥要求,需要定义一个数据结构,这个数据结构包含两个指针,一个读一个写,同时有一个资源数目量,告诉生产者和消费者是否可以生产或者消费。由于该数据结构很小,因而可以对此结构互斥访问。同时,对于每组数据,都有一个标志位,表示此组数据是否被占用,生产者和消费者均可以先占用此位置然后完成相应的操作。
当消费者互斥访问此结构时,首先判断是否有数据可以取,如果没有,直接等待,若有数据可取,先更改标志位占用此数据,并将资源数目-1。然后交出互斥,把数据拷贝到自己缓冲区内,清空数据。当生产者访问时,首先判断有没有空位可以生产,如果没有,直接等待,若有数据可以生产,先判断该位是否被占用,如果没被占用,则占用此位置进行生产。生产完成后,将占用位改为未占用,同时将资源数目+1。这个过程可以如图所示:
2、重要函数和数据结构
(1)数据结构
#define N 5 // 生产者的数目 #define N2 15 // 消费者数目 #define M 20 // 缓冲数目 #define debug 0 //调试模式 #define nowait 1 //是否添加额外等待 pthread_mutex_t mutex; // 互斥信号量 int prod_id = 0; //生产者id int proc_id = 0; //消费者id //成功以及失败次数计数 int Succ[N+N2]; int Fail[N+N2]; //用于生产的填充数据 char dt[30]="aaaaaaaaaabbbbbbbbbbcccccccccc"; int toexit=0;//退出标记 //共用数据区 struct data_struc { int Occupied; //该位置是否被占用 无论生产还是消费 char d[1024]; //数据 struct data_struc* nx; //下一个指针 }; //互斥量 struct mtx { struct data_struc* rd_p; struct data_struc* wr_p; int cnt; //产品数量 }; struct mtx *mymutex; |
(2)函数说明
void printinfo();//调试函数,打印公用数据区的内容和状态 void Create_Empty_DS();//初始化循环链表 void *buy();//消费者 void *sell();//生产者 void *Recount();//数据统计,同时控制程序自动退出 |
3、程序流程图
A、主函数
B、统计线程
C、生产者线程
D、消费者线程
三、实验数据
1、程序运行截图
虚拟机:VMware Workstation7.0,win7;硬件:i3,2G DDR1333;编译:gcc product.c –pg –lpthread;报告生成:gprof –b。
运行时截图:
CPU占用情况(无额外等待):
2、原始数据
由于程序要求使用gprof进行分析,而在机器配置较好的情况下,使用gprof可能分析不出每个函数执行时间,甚至连函数都分析不全,因而在实际代码中添加了一些空循环来增加cpu占用。
为了更便于观察,首先假设如下场景:消费者、生产者在执行各自动作前,均usleep(1),同时,消费者在成功消费之后,也会usleep(3)来为其他消费者让位。同时,也将测试没有任何等待的数据。程序内有一计时线程,每个一秒会显示一次生产消费情况,当60秒后程序自动退出,从而保存数据已被分析。
生产者数量 |
1 |
消费者数量 |
1 |
缓冲长度 |
20 |
额外等待 |
有 |
CPU占用 |
<0.3% |
||
函数分析情况 |
|||||
无法分析到消费者、生产者线程 |
|||||
程序运行情况 |
|||||
生产者: #1 成功次数:12243 失败次数:12213 消费者: #1 成功次数:12223 失败次数:0 |
生产者数量 |
1 |
消费者数量 |
1 |
缓冲长度 |
20 |
额外等待 |
无 |
CPU占用 |
>97% |
||
函数分析情况 |
|||||
% cumulative self self total time seconds seconds calls Ts/call Ts/call name 54.59 2.08 2.08 buy 45.41 3.81 1.73 sell 0.00 3.81 0.00 1 0.00 0.00 Create_Empty_DS Call graph granularity: each sample hit covers 4 byte(s) for 0.26% of 3.81 seconds index % time self children called name <spontaneous> [1] 54.6 2.08 0.00 buy [1] ----------------------------------------------- <spontaneous> [2] 45.4 1.73 0.00 sell [2] ----------------------------------------------- 0.00 0.00 1/1 main [8] [3] 0.0 0.00 0.00 1 Create_Empty_DS [3] ----------------------------------------------- Index by function name [3] Create_Empty_DS [1] buy [2] sell |
|||||
程序运行情况 |
|||||
生产者: #1 成功次数:21260 失败次数:1146117485 消费者: #1 成功次数:21240 失败次数:1151177763 |
生产者数量 |
1 |
消费者数量 |
5 |
缓冲长度 |
20 |
额外等待 |
有 |
CPU占用 |
<0.3% |
||
函数分析情况 |
|||||
无法分析到消费者、生产者线程 |
|||||
程序运行情况 |
|||||
生产者: #1 成功次数:24829 失败次数:0 消费者: #1 成功次数:4900 失败次数:15102 消费者: #2 成功次数:5261 失败次数:14386 ….. 消费者: #5 成功次数:5069 失败次数:14752 |
生产者数量 |
1 |
消费者数量 |
5 |
缓冲长度 |
20 |
额外等待 |
无 |
CPU占用 |
>97% |
||
函数分析情况 |
|||||
% cumulative self self total time seconds seconds calls Ts/call Ts/call name 85.29 2.90 2.90 buy 14.71 3.40 0.50 sell 0.00 3.40 0.00 1 0.00 0.00 Create_Empty_DS Call graph granularity: each sample hit covers 4 byte(s) for 0.29% of 3.40 seconds index % time self children called name <spontaneous> [1] 85.3 2.90 0.00 buy [1] ----------------------------------------------- <spontaneous> [2] 14.7 0.50 0.00 sell [2] ----------------------------------------------- 0.00 0.00 1/1 main [8] [3] 0.0 0.00 0.00 1 Create_Empty_DS [3] ----------------------------------------------- Index by function name [3] Create_Empty_DS [1] buy [2] sell |
|||||
程序运行情况 |
|||||
生产者: #1 成功次数:10800 失败次数:362944304 消费者: #1 成功次数:1980 失败次数:368988095 … 消费者: #5 成功次数:2560 失败次数:391801113 |
生产者数量 |
3 |
消费者数量 |
10 |
缓冲长度 |
20 |
额外等待 |
有 |
CPU占用 |
约4% |
||
函数分析情况 |
|||||
% cumulative self self total time seconds seconds calls Ts/call Ts/call name 94.08 1.59 1.59 sell 5.92 1.69 0.10 buy 0.00 1.69 0.00 1 0.00 0.00 Create_Empty_DS Call graph granularity: each sample hit covers 4 byte(s) for 0.59% of 1.69 seconds index % time self children called name <spontaneous> [1] 94.1 1.59 0.00 sell [1] ----------------------------------------------- <spontaneous> [2] 5.9 0.10 0.00 buy [2] ----------------------------------------------- 0.00 0.00 1/1 main [8] [3] 0.0 0.00 0.00 1 Create_Empty_DS [3] ----------------------------------------------- Index by function name [3] Create_Empty_DS [2] buy [1] sell |
|||||
程序运行情况 |
|||||
生产者: #1 成功次数:161677 失败次数:0 生产者: #2 成功次数:161412 失败次数:0 生产者: #3 成功次数:161535 失败次数:0 消费者: #1 成功次数:44264 失败次数:75030 消费者: #2 成功次数:47878 失败次数:67808 消费者: #3 成功次数:47805 失败次数:67967 …… 消费者: #10 成功次数:45942 失败次数:71664 |
生产者数量 |
3 |
消费者数量 |
10 |
缓冲长度 |
20 |
额外等待 |
无 |
CPU占用 |
>97% |
||
函数分析情况 |
|||||
% cumulative self self total time seconds seconds calls Ts/call Ts/call name 77.28 2.96 2.96 buy 22.72 3.83 0.87 sell 0.00 3.83 0.00 1 0.00 0.00 Create_Empty_DS Call graph granularity: each sample hit covers 4 byte(s) for 0.26% of 3.83 seconds index % time self children called name <spontaneous> [1] 77.3 2.96 0.00 buy [1] ----------------------------------------------- <spontaneous> [2] 22.7 0.87 0.00 sell [2] ----------------------------------------------- 0.00 0.00 1/1 main [8] [3] 0.0 0.00 0.00 1 Create_Empty_DS [3] ----------------------------------------------- Index by function name [3] Create_Empty_DS [1] buy [2] sell |
|||||
程序运行情况 |
|||||
生产者: #1 成功次数:4640 失败次数:174184091 生产者: #2 成功次数:4000 失败次数:179724333 生产者: #3 成功次数:4120 失败次数:170018676 消费者: #1 成功次数:1280 失败次数:182205853 消费者: #2 成功次数:1340 失败次数:173044281 ….. 消费者: #9 成功次数:1020 失败次数:166877134 消费者: #10 成功次数:1380 失败次数:164803475 |
生产者数量 |
5 |
消费者数量 |
20 |
缓冲长度 |
40 |
额外等待 |
有 |
CPU占用 |
约9% |
||
函数分析情况 |
|||||
% cumulative self self total time seconds seconds calls Ts/call Ts/call name 96.37 4.25 4.25 sell 3.63 4.41 0.16 buy 0.00 4.41 0.00 1 0.00 0.00 Create_Empty_DS Call graph granularity: each sample hit covers 4 byte(s) for 0.23% of 4.41 seconds index % time self children called name <spontaneous> [1] 96.4 4.25 0.00 sell [1] ----------------------------------------------- <spontaneous> [2] 3.6 0.16 0.00 buy [2] ----------------------------------------------- 0.00 0.00 1/1 main [8] [3] 0.0 0.00 0.00 1 Create_Empty_DS [3] ----------------------------------------------- Index by function name [3] Create_Empty_DS [2] buy [1] sell |
|||||
程序运行情况 |
|||||
生产者: #1 成功次数:139689 失败次数:0 生产者: #2 成功次数:139235 失败次数:0 …. 生产者: #5 成功次数:137801 失败次数:0 消费者: #1 成功次数:25922 失败次数:96488 消费者: #2 成功次数:39009 失败次数:70402 消费者: #3 成功次数:39645 失败次数:69154 ….. 消费者: #20 成功次数:33775 失败次数:80811 |
生产者数量 |
5 |
消费者数量 |
20 |
缓冲长度 |
40 |
额外等待 |
无 |
CPU占用 |
>97% |
||
函数分析情况 |
|||||
% cumulative self self total time seconds seconds calls Ts/call Ts/call name 80.29 2.81 2.81 buy 19.71 3.50 0.69 sell 0.00 3.50 0.00 1 0.00 0.00 Create_Empty_DS Call graph granularity: each sample hit covers 4 byte(s) for 0.29% of 3.50 seconds index % time self children called name <spontaneous> [1] 80.3 2.81 0.00 buy [1] ----------------------------------------------- <spontaneous> [2] 19.7 0.69 0.00 sell [2] ----------------------------------------------- 0.00 0.00 1/1 main [8] [3] 0.0 0.00 0.00 1 Create_Empty_DS [3] ----------------------------------------------- Index by function name [3] Create_Empty_DS [1] buy [2] sell |
|||||
程序运行情况 |
|||||
生产者: #1 成功次数:4120 失败次数:81703999 …. 生产者: #5 成功次数:4200 失败次数:91874644 消费者: #1 成功次数:1080 失败次数:83483253 消费者: #2 成功次数:1240 失败次数:85970149 消费者: #3 成功次数:1360 失败次数:106265813 消费者: #4 成功次数:960 失败次数:82522392 消费者: #5 成功次数:1360 失败次数:93462020 消费者: #6 成功次数:1160 失败次数:91337608 消费者: #7 成功次数:1520 失败次数:102954859 消费者: #8 成功次数:1080 失败次数:91363902 消费者: #9 成功次数:760 失败次数:105319175 消费者: #10 成功次数:1320 失败次数:102144117 消费者: #11 成功次数:720 失败次数:90933552 消费者: #12 成功次数:1120 失败次数:90537228 消费者: #13 成功次数:920 失败次数:90670465 消费者: #14 成功次数:840 失败次数:109153419 消费者: #15 成功次数:1320 失败次数:91949204 消费者: #16 成功次数:960 失败次数:80094023 消费者: #17 成功次数:1120 失败次数:81866777 消费者: #18 成功次数:1000 失败次数:87802980 消费者: #19 成功次数:800 失败次数:79470741 消费者: #20 成功次数:960 失败次数:93098801 |
生产者数量 |
30 |
消费者数量 |
100 |
缓冲长度 |
200 |
额外等待 |
有 |
CPU占用 |
26% |
||
函数分析情况 |
|||||
% cumulative self self total time seconds seconds calls Ts/call Ts/call name 79.17 10.49 10.49 sell 20.83 13.25 2.76 buy 0.00 13.25 0.00 1 0.00 0.00 Create_Empty_DS Call graph granularity: each sample hit covers 4 byte(s) for 0.08% of 13.25 seconds index % time self children called name <spontaneous> [1] 79.2 10.49 0.00 sell [1] ----------------------------------------------- <spontaneous> [2] 20.8 2.76 0.00 buy [2] ----------------------------------------------- 0.00 0.00 1/1 main [8] [3] 0.0 0.00 0.00 1 Create_Empty_DS [3] ----------------------------------------------- Index by function name [3] Create_Empty_DS [2] buy [1] sell |
|||||
程序运行情况 |
|||||
生产者: #1 成功次数:24555 失败次数:0 生产者: #2 成功次数:24423 失败次数:0 生产者: #3 成功次数:24529 失败次数:0 生产者: #4 成功次数:24428 失败次数:0 ….. 生产者: #8 成功次数:24552 失败次数:0 生产者: #9 成功次数:24467 失败次数:0 生产者: #10 成功次数:24612 失败次数:0 ……………………………. 生产者: #29 成功次数:24439 失败次数:0 生产者: #30 成功次数:24723 失败次数:0 消费者: #1 成功次数:7351 失败次数:16473 消费者: #2 成功次数:6408 失败次数:18192 消费者: #3 成功次数:6725 失败次数:17620 ……. 消费者: #8 成功次数:7830 失败次数:15630 消费者: #9 成功次数:7264 失败次数:16677 ……… 消费者: #97 成功次数:6628 失败次数:17780 消费者: #98 成功次数:7200 失败次数:16692 消费者: #99 成功次数:6921 失败次数:17335 消费者: #100 成功次数:7433 失败次数:16319 |
生产者数量 |
30 |
消费者数量 |
100 |
缓冲长度 |
200 |
额外等待 |
无 |
CPU占用 |
>97% |
||
函数分析情况 |
|||||
% cumulative self self total time seconds seconds calls Ts/call Ts/call name 78.77 2.82 2.82 buy 21.23 3.58 0.76 sell 0.00 3.58 0.00 1 0.00 0.00 Create_Empty_DS Call graph granularity: each sample hit covers 4 byte(s) for 0.28% of 3.58 seconds index % time self children called name <spontaneous> [1] 78.8 2.82 0.00 buy [1] ----------------------------------------------- <spontaneous> [2] 21.2 0.76 0.00 sell [2] ----------------------------------------------- 0.00 0.00 1/1 main [8] [3] 0.0 0.00 0.00 1 Create_Empty_DS [3] ----------------------------------------------- Index by function name [3] Create_Empty_DS [1] buy [2] sell |
|||||
程序运行情况 |
|||||
生产者: #1 成功次数:3600 失败次数:10645017 生产者: #2 成功次数:4000 失败次数:16728291 生产者: #3 成功次数:3200 失败次数:17111918 生产者: #4 成功次数:3400 失败次数:10212130 …… 生产者: #17 成功次数:1400 失败次数:15370944 生产者: #18 成功次数:2200 失败次数:21377626 生产者: #19 成功次数:2600 失败次数:11692857 生产者: #20 成功次数:3800 失败次数:16007285 生产者: #21 成功次数:2200 失败次数:14244794 生产者: #22 成功次数:3600 失败次数:22401337 ….. 生产者: #29 成功次数:4000 失败次数:18425015 生产者: #30 成功次数:4800 失败次数:25033822 消费者: #1 成功次数:1800 失败次数:12785639 消费者: #2 成功次数:2800 失败次数:16116393 消费者: #3 成功次数:2400 失败次数:10309678 消费者: #4 成功次数:800 失败次数:24464415 消费者: #52 成功次数:0 失败次数:13483907 消费者: #53 成功次数:0 失败次数:20152736 消费者: #54 成功次数:0 失败次数:14236228 消费者: #55 成功次数:800 失败次数:21535076 消费者: #56 成功次数:400 失败次数:18554678 消费者: #57 成功次数:200 失败次数:20539844 消费者: #58 成功次数:800 失败次数:12377087 ……. 消费者: #94 成功次数:1600 失败次数:14401603 消费者: #95 成功次数:2000 失败次数:24301257 消费者: #96 成功次数:1800 失败次数:15476220 消费者: #97 成功次数:2600 失败次数:17645168 消费者: #98 成功次数:1000 失败次数:15002154 消费者: #99 成功次数:200 失败次数:14299381 消费者: #100 成功次数:1600 失败次数:21723729 |
生产者数量 |
50 |
消费者数量 |
200 |
缓冲长度 |
500 |
额外等待 |
有 |
CPU占用 |
>97% |
||
函数分析情况 |
|||||
% cumulative self self total time seconds seconds calls Ts/call Ts/call name 98.61 50.26 50.26 sell 1.39 50.97 0.71 buy 0.00 50.97 0.00 1 0.00 0.00 Create_Empty_DS Call graph granularity: each sample hit covers 4 byte(s) for 0.02% of 50.97 seconds index % time self children called name <spontaneous> [1] 98.6 50.26 0.00 sell [1] ----------------------------------------------- <spontaneous> [2] 1.4 0.71 0.00 buy [2] ----------------------------------------------- 0.00 0.00 1/1 main [8] [3] 0.0 0.00 0.00 1 Create_Empty_DS [3] ----------------------------------------------- Index by function name [3] Create_Empty_DS [2] buy [1] sell |
|||||
程序运行情况 |
|||||
生产者: #1 成功次数:14839 失败次数:0 生产者: #2 成功次数:14688 失败次数:0 生产者: #3 成功次数:14838 失败次数:0 生产者: #4 成功次数:14738 失败次数:0 ……….. ….. 生产者: #49 成功次数:14908 失败次数:0 生产者: #50 成功次数:14949 失败次数:0 消费者: #1 成功次数:3602 失败次数:20037 消费者: #2 成功次数:3777 失败次数:19650 消费者: #3 成功次数:3655 失败次数:19907 消费者: #4 成功次数:3909 失败次数:19429 消费者: #5 成功次数:3744 失败次数:19693 ……… 消费者: #200 成功次数:3590 失败次数:19568 |
生产者数量 |
50 |
消费者数量 |
50 |
缓冲长度 |
200 |
额外等待 |
无 |
CPU占用 |
>97% |
||
函数分析情况 |
|||||
% cumulative self self total time seconds seconds calls Ts/call Ts/call name 51.80 1.87 1.87 buy 48.20 3.61 1.74 sell 0.00 3.61 0.00 1 0.00 0.00 Create_Empty_DS Call graph granularity: each sample hit covers 4 byte(s) for 0.28% of 3.61 seconds index % time self children called name <spontaneous> [1] 51.8 1.87 0.00 buy [1] ----------------------------------------------- <spontaneous> [2] 48.2 1.74 0.00 sell [2] ----------------------------------------------- 0.00 0.00 1/1 main [8] [3] 0.0 0.00 0.00 1 Create_Empty_DS [3] ----------------------------------------------- Index by function name [3] Create_Empty_DS [1] buy [2] sell |
|||||
程序运行情况 |
|||||
生产者: #1 成功次数:3800 失败次数:23207623 生产者: #2 成功次数:4000 失败次数:18301181 生产者: #3 成功次数:2000 失败次数:23687122 生产者: #4 成功次数:3200 失败次数:22469725 生产者: #5 成功次数:2400 失败次数:16314631 生产者: #6 成功次数:3400 失败次数:27401390 生产者: #7 成功次数:1200 失败次数:22341461 生产者: #8 成功次数:5000 失败次数:20794031 生产者: #9 成功次数:2400 失败次数:12731020 生产者: #10 成功次数:3600 失败次数:19212245 生产者: #11 成功次数:4800 失败次数:19359142 …… 生产者: #48 成功次数:5000 失败次数:21311750 生产者: #49 成功次数:4000 失败次数:24779388 生产者: #50 成功次数:2400 失败次数:20580230 消费者: #1 成功次数:6000 失败次数:21266466 消费者: #2 成功次数:4200 失败次数:24949360 消费者: #3 成功次数:5200 失败次数:23671124 …. 消费者: #17 成功次数:2800 失败次数:17227389 消费者: #18 成功次数:2800 失败次数:18082421 消费者: #19 成功次数:3000 失败次数:16376502 消费者: #20 成功次数:1000 失败次数:23365359 ….. 消费者: #33 成功次数:2000 失败次数:20199384 消费者: #34 成功次数:1600 失败次数:27577017 消费者: #35 成功次数:1800 失败次数:14283957 消费者: #36 成功次数:1400 失败次数:27276326 消费者: #37 成功次数:3400 失败次数:24800367 消费者: #38 成功次数:3400 失败次数:24015530 …… 消费者: #49 成功次数:3200 失败次数:22219422 消费者: #50 成功次数:2200 失败次数:18238523 |
生产者数量 |
50 |
消费者数量 |
50 |
缓冲长度 |
200 |
额外等待 |
有 |
CPU占用 |
63% |
||
函数分析情况 |
|||||
% cumulative self self total time seconds seconds calls Ts/call Ts/call name 92.32 26.81 26.81 sell 7.68 29.04 2.23 buy 0.00 29.04 0.00 1 0.00 0.00 Create_Empty_DS Call graph granularity: each sample hit covers 4 byte(s) for 0.03% of 29.04 seconds index % time self children called name <spontaneous> [1] 92.3 26.81 0.00 sell [1] ----------------------------------------------- <spontaneous> [2] 7.7 2.23 0.00 buy [2] ----------------------------------------------- 0.00 0.00 1/1 main [8] [3] 0.0 0.00 0.00 1 Create_Empty_DS [3] ----------------------------------------------- Index by function name [3] Create_Empty_DS [2] buy [1] sell |
|||||
程序运行情况 |
|||||
生产者: #1 成功次数:14774 失败次数:12234 生产者: #2 成功次数:14821 失败次数:12209 ….. 生产者: #47 成功次数:14754 失败次数:12265 生产者: #48 成功次数:14895 失败次数:12160 生产者: #49 成功次数:14796 失败次数:12126 生产者: #50 成功次数:14722 失败次数:12149 消费者: #1 成功次数:14816 失败次数:65 消费者: #2 成功次数:14836 失败次数:65 消费者: #3 成功次数:14814 失败次数:58 消费者: #4 成功次数:14804 失败次数:72 ….. 消费者: #26 成功次数:14817 失败次数:62 消费者: #27 成功次数:14851 失败次数:56 消费者: #28 成功次数:14840 失败次数:65 消费者: #29 成功次数:14802 失败次数:56 ….. 消费者: #49 成功次数:14819 失败次数:60 消费者: #50 成功次数:14836 失败次数:59 |
四、结果分析
1、资源消耗的分析
首先,是整体的资源消耗,由于结构问题,生产者和消费者的实质都是while(1)的循环,这意味着如果不插入等待时间的话,任何一种方案,无论生产者消费者的数目,运行起来都可以让CPU的占用达到100%,即使线程的资源开销要小于进程。
另外,就是本题假设的情况,在有等待的情况下,我们假定生产者生产东西需要更多的时间,这个时间为20000的一个空循环,而消费者只需要6000的空循环即可。以3消费者10生产者,有等待这组数据为例,生产者和消费者的CPU占用比例差不多为94:6,即便是3*4:10也只是12:10而已,但是实际上生产者的CPU占用比例很高,这和生产者的操作有关。生产者完成的任务是将一组数据循环写入一个数组内,而这个数组的长度为1024,这本身就需要一定的资源消耗,而消费者只是执行一个strcpy。尽管如此,在总共60秒的运行中,几个函数的CPU占用时间也只是1.6s而已。
接下来,是没有等待的情况。以5生产者,20消费者,40缓冲区的情况为例,从分析报告可以得出,消费者和生产者的CPU占用比大概为4:1,但是从CPU占用时间来看,生产者的3.5秒还比消费者的2.8秒要多,这也说明了生产者实际的工作更占用CPU资源,而绝大多数时间都花在对信号量的等待上。
另外,需要考虑的是互斥操作的开销问题,加锁和解锁都是原子动作,可以互斥完成,因而理论上占用的CPU资源应该非常少。以3消费者10生产者,无等待这组数据的运行结果为例,生产者成功的次数大概在10的3次方级别,而失败的次数确高达10的9次方,从程序运行过程可以得知,失败的操作也包括加锁、访问数量位、解锁这些,从数字的比较我们也可以看出这些原子操作的速度之快。因而,个人认为,在线程数量不是很大的情况下,课件中给的程序的多锁操作并不会对程序的性能造成太过明显的影响。
由于性能原因,并未给出更多数据的测试报告,而影响这个性能的主要原因是大量的printf,包括清屏、光标重定位、显示文字等,这才是影响整个程序性能的真正原因。
2、具体应用情况
生产者-消费者是一个很广泛的问题,代表了多线程互斥访问共享资源,以及线程之间通信的各种问题的总和。这个可以从数据看出,简单有无等待的设置,可以让数据差异变的如此明显。毕竟,在实际应用中,生产者如何生产数据,产生怎样的数据,而消费者又如何拿到数据,是通过管道还是内存共享区,拿到数据之后,是进行处理还是写入磁盘,都有可能,因而,简单通过几组数据来判断哪个进程的效率更高,是不科学的。
我们可以通过两个实际的例子来解释有无等待的情况。有等待更像是在火锅店吃火锅,商家只要把锅准备好,将生菜切好端上来即可,而顾客大量的时间用在食用上,等待的时间也很少,这样资源位置会大量被消耗,但是实际资源消耗的速度很慢,但是由于空间被占用,生产者无法继续生产。而无等待更像是快餐外卖,商家花大量时间将食物准备好,而消费者只需要交钱拿东西走人即可,资源位置空闲很多,消费者等待时间较长,但是处理数据的过程简单又节约资源。
这里还有一个问题,就是资源槽的数量,理论上这个方案应该放弃资源槽,但是从实际实现方法来看,只不过将每个资源槽的标志位移动到资源内部,然后通过读写指针进行查询,实际上这还是资源槽的变相实现方式。但是单就资源槽的数目来看,是个很复杂的问题,要保证在生产者想生产的时候不会没槽位,而消费者查询资源槽状态又不会花费太长时间,这需要具体分析每个函数执行的时间,因而本实验中只是简单设计为消费者数量的1-3倍。
3、线程的调度情况
首先要考虑的是线程的调度顺序。为了方便起见,每个线程在产生的时候都有一个编号,而创建也是按照1-Count这样的顺序for循环创建的。如果线程调度的顺序和线程创建的顺序一样,那么在无等待情况下应该有这样的结果:消费者远多于生产者,这样当生产者生产出数据时,前面几个优先完成等待并读取数据的进程就可以占据先机,拿到数据(这里不考虑数据消费的时间,因为程序的思路和食堂占座差不多,只要把位置占掉就可以了),而后面一些进程则由于位置不佳基本拿不到数据。但是从数据来看并不是这样:
从数据来看,99号消费者只成功拿到200次数据,而100号消费者则拿到了1600次数据,而96号消费者更是拿到了2600次数据。这就说明了线程调度的随机性,线程调度的顺序和线程创建的顺序完全不一样,当然如果利用进程+特殊的框架能做到,比如令牌环作业我用的那套框架,就可以保证每个进程按顺序执行,当然这是使用了其他的进程来辅助调度。
另外一个比较有意思的是,成功次数为什么都是100的整数倍,而失败的次数则可以有很大的差异。除了原子操作占用资源较少,可以很快完成之外,更多的我感觉是因为时间片的轮转。无论时间片是怎么分配的,在一个时间片内必须保证原子操作的完成,且操作完成前不能被终止和剥夺,而其他的函数操作则不能保证其原子性。但是有一点不解,就是这个时间片的长度,从us级等待来看,由于其他进程的自动等待,可以保证消费者公平的获取资源,但是没有等待时,一个时间片至少可以让一个消费者完成200次消费操作,这就不好理解了。
当然,插入等待时间也是一种解决资源分配的方式,这样可以保证每个线程都有机会获取到资源,不至于让某个线程连续的占用资源很长时间。但是在实际情况下,每个线程完成工作不一样,又不可能像实验写出的进程那样无私,拿到资源后就自动等待,这就需要使用线程间通信机制了,让拿不到资源的线程挂起,生产者完成生产后再唤醒这些进程。
4、gprof本身的问题
“gprof是GNU profiler工具。可以显示程序运行的“flat profile”,包括每个函数的调用次数,每个函数消耗的处理器时间。也可以显示“调用图”,包括函数的调用关系,每个函数调用花费了多少时间。还可以显示“注释的源代码”,是程序源代码的一个复本,标记有程序中每行代码的执行次数。
在编译或链接源程序的时候在编译器的命令行参数中加入“-pg”选项,编译时编译器会自动在目标代码中插入用于性能测试的代码片断,这些代码在程序在运行时采集并记录函数的调用关系和调用次数,以及采集并记录函数自身执行时间和子函数的调用时间,程序运行结束后,会在程序退出的路径下生成一个gmon.out文件。这个文件就是记录并保存下来的监控数据。可以通过命令行方式的gprof或图形化的Kprof来解读这些数据并对程序的性能进行分析。另外,如果想查看库函数的profiling,需要在编译是再加入“-lc_p”编译参数代替“-lc”编译参数,这样程序会链接libc_p.a库,才可以产生库函数的profiling信息。如果想执行一行一行的profiling,还需要加入“-g”编译参数。”
这是摘自网上一段关于gprof的介绍,实际应用中,-lc_p参数貌似无法使用,而加上-g之后,并没有多出什么有用的信息。而且,问题并不止这些,从网上介绍来看,貌似gprof是在编译的时候,让gcc为每个函数调用加上一个类似“mount”的函数,来记录函数的调用状况。但是,当函数的执行时间很短时,gprof并不能分析出这些函数,这也就是为什么在代码中为每段代码额外加上一点占用cpu资源的代码,更奇怪的是,这种情况只出现在使用wait的情况,当不wait直接while(1)执行,则完全没有这个问题。
另外,就是根据上文分析,同时根据实践经验可知,大量的IO操作非常占用资源,包括屏幕的擦写,但是gprof并没有分析出这部分的cpu占用,就连Recount这个统计函数都没有出现在函数列表内,这就更诡异了。因而,如果想要完整的分析一个程序的性能,单用gprof肯定是不够的,尤其是对于有应用背景的程序来说。当然,对于完全是CPU占用的程序,比如各种查找、排序算法,使用gprof应该会收到相当好的效果。
5、程序性能
应该说,这个程序的性能只能算一般,当关闭-pg选项时,同时关闭recount的IO操作,在100生产者100消费者,缓冲长度500,增加额外等待时,CPU占用已经接近100%,如果不改变环境变量,在创建160个生产者或者220个消费者时,程序会由于资源分配报错。当然,这样的数目对于一般的应用已经足够了。
6、可能的改进
首先,就是线程调度的方式,忙等肯定不是一个好方法,如果换成线程间通信,轮流挂起或者唤醒某个线程,可能会更好。
其次,是消费者的资源分配问题。程序设计的消费者有一个1024字节的buffer,用来处理接收到的数据,但是这个buffer直到线程消失才被回收,如果加上一些资源回收的机制可能会改善程序的性能。
最后,是对于生产者和消费者具体完成工作的问题,应该说这个实验设计的场景还是很简单,完全的内存拷贝,没有什么复杂运算和文件操作,而且由于手头没有原始版本的代码,无法比较两个代码的性能,整个程序的框架也是重新编写的,这应该也是一点不足。
五、总结体会
通过这次实验的练习,我对linux下的线程、进程等知识有了更直观深入的了解,同时也深感linux下开发程序的不易,通过写这几个程序,发现在linux下,最常见的运行错误莫过于“段错误”,这个错误的范围很广,从访问越界,到数组上下限错误,再到内存或者堆栈溢出,都可能触发这个错误,而如果没有一定的经验这是相当棘手的问题。当然,这个综合练习实验有很好的价值,除了涉及多个知识点之外,自己在做程序的时候也通过解决问题,对linux下一些函数认识的更加透彻,例如对进程间的通信,多线程的安全问题,管道问题都有了进一步的了解,初步学习了一下linux下的编程还是很不错的。