代碼規范中不允許遞歸調用,實際開發中應該盡量避免對遞歸的使用,究其原因主要是以下兩點:
1. 嵌套深度上會存在一定風險,遞歸層數過多,不斷壓棧,可能會引起棧溢出的問題;
2. 代碼可讀性,不太容易被后面維護的人理解;
但是,凡事總有例外。
比如要有一種需求場景,需要遍歷一個目錄下的所有文件,包括其中子目錄中的文件,然后將滿足一定條件的文件篩選出來,
你會發現,用遞歸去設計反而會比較簡單。
對於解決一些包含重復類似邏輯的問題,遞歸對於開發人員來說是一個反而比較清晰的選擇。
本文主要介紹,不得不使用遞歸時,針對上述第一個風險,如何評估棧空間是否足夠。
評估思路:
1. 確認當前線程棧空間限制cur_stack_size是多少?
2. 遞歸調用n次,分析n次壓棧后棧空間的損耗cost_size大約大少?
3. 結合業務評估,預估一個最大可能的遞歸調用次數max
4. (max*cost_size/n) 如果大於或已經接近 cur_stack_size, 表示存在棧越界風險,需要放大棧空間或者做功能規格約束
具體以一個例子來說明,main函數中啟動一個線程,線程棧大小可配,線程中遞歸計算一個階乘:
1 #include <stdio.h> 2 #include <string.h> 3 #include <stdlib.h> 4 #include <pthread.h> 5 #include <errno.h> 6 #include <limits.h> 7 #include <sys/types.h> 8 #include <sys/stat.h> 9 #include <fcntl.h> 10 11 static char* stack_begin = NULL; 12 static char* stack_end = NULL; 13 static int totalnum = 0; 14 static const char* filepath = "datafile.txt"; 15 16 static unsigned long factor(int n) 17 { 18 unsigned long ulRet = 0; 19 20 if (n == totalnum) 21 { 22 stack_begin = (char*)(&ulRet); 23 } 24 if (1 == n) 25 { 26 stack_end =(char*)(&ulRet); 27 ulRet = 1; 28 } 29 else 30 { 31 32 ulRet = n*factor(n-1); 33 } 34 #if 0 35 printf("[%5d], begin:%p,end:%p, &n:%p\n", n, stack_begin,stack_end, &n); 36 #endif 37 return ulRet; 38 } 39 40 41 static int traverse_test(int test_num, int stack_size) 42 { 43 int i = 0; 44 int fd = -1; 45 int num = 0; 46 int iret = 0; 47 int stacksize = 0; 48 long theoretical_max = 0; 49 float percost = 0.0; 50 float stackcost = 0.0; 51 pthread_t thread_id; 52 pthread_attr_t attr; 53 char info[256]; 54 55 num = test_num; 56 stacksize = stack_size * 1024; 57 printf("--------- num :%d, stacksize:%d ---------\n",test_num,stack_size); 58 59 fd = open(filepath,O_CREAT|O_RDWR|O_APPEND,0777); 60 if (0 > fd) 61 { 62 printf("open file failed, err:%d,%s\n",errno, strerror(errno)); 63 return -1; 64 } 65 else 66 { 67 (void)truncate(filepath,0); 68 lseek(fd, 0, SEEK_SET); 69 } 70 71 memset(info,0,sizeof(info)); 72 snprintf(info, sizeof(info),"%s %s %s %s %s\n","num", "stacksize(KB)","percost(KB)","stackcost(KB)","maxNum"); 73 (void)write(fd, info, strnlen(info, sizeof(info))); 74 75 if (0 != pthread_attr_init(&attr)) 76 { 77 printf("pthread attr init err, errno:%d!!!!\n",errno); 78 iret = -1; 79 goto err_exit; 80 } 81 if (0 != pthread_attr_setstacksize(&attr, stacksize)) 82 { 83 printf("pthread set stack err, min[%d],set[%d],errno:%d,err:%s!!!!\n", 84 PTHREAD_STACK_MIN,stacksize,errno,strerror(errno)); 85 iret = -1; 86 goto err_exit; 87 } 88 89 for (i = 2; i <= num; i++) 90 { 91 /*start a pthread to call recursive*/ 92 memset(&thread_id,0,sizeof(thread_id)); 93 stack_begin = 0; 94 stack_end = 0; 95 totalnum = i; 96 if (0 != pthread_create(&thread_id,&attr, factor,i)) 97 { 98 printf("pthread create err, errno:%d!!!!\n",errno); 99 iret = -1; 100 goto err_exit; 101 } 102 if (0 != pthread_join(thread_id, NULL)) 103 { 104 printf("pthread join err, errno:%d!!!!\n",errno); 105 iret = -1; 106 goto err_exit; 107 } 108 109 percost = (float)(stack_begin - stack_end)/(float)i; 110 stackcost = (float)(stack_begin - stack_end)/1024.0; 111 theoretical_max = (stacksize*i)/(stack_begin - stack_end); 112 memset(info,0,sizeof(info)); 113 snprintf(info, sizeof(info),"%d %d %.2f %.2f %ld\n", i, stack_size, percost,stackcost,theoretical_max); 114 (void)write(fd, info, strnlen(info, sizeof(info))); 115 116 if (1 == i || 0 == i % 10) 117 { 118 printf("testnum[%d], stacksize[%d]KB, percost[%.2f]Byte, stackcost:[%.2f]KB, max maybe:[%ld],!!!!\n", 119 i, stack_size,percost,stackcost,theoretical_max); 120 } 121 } 122 iret = 0; 123 124 err_exit: 125 if (0 != pthread_attr_destroy(&attr)) 126 { 127 printf("pthread attr destroy err, errno:%d!!!!\n",errno); 128 } 129 close(fd); 130 return iret; 131 } 132 133 int main(int argc, char* argv[]) 134 { 135 int num = 0; 136 int stacksize = 0; 137 138 num = atoi(argv[1]); 139 stacksize = atoi(argv[2]); 140 141 if (0 != traverse_test(num,stacksize)) 142 { 143 printf("err happen!!!\n"); 144 return -1; 145 } 146 147 return 0; 148 }
編譯運行, 測試10的階乘,線程棧配置為16KB:
gcc -g test.c -pthread -o test;./test 10 16
結果如下:
10次遞歸,棧開銷大約: 0x7feef0a2bebc – 0x7feef0a2bbec = 0x2d0,, 720字節
進一步增大迭代次數和線程棧(計算4000的階乘,棧空間定位256KB),將得到的數據繪制分析曲線如下:
(X軸是階乘計算的總數n,Y軸是平均每次階乘棧的開銷KB):
可以看出來,每次平均每次棧的開銷值並非線性增長,所以評估時注意使用最終持平的那個單次開銷去估算。
保險期間,實際項目中用最高約束規格去做一把驗證性測試。