情景
shell腳本的執行效率雖高,但當任務量巨大時仍然需要較長的時間,尤其是需要執行一大批的命令時。因為默認情況下,shell腳本中的命令是串行執行的。如果這些命令相互之間是獨立的,則可以使用“並發”的方式執行這些命令,這樣可以更好地利用系統資源,提升運行效率,縮短腳本執行的時間。如果命令相互之間存在交互,則情況就復雜了,那么不建議使用shell腳本來完成多線程的實現。
為了方便闡述,使用一段測試代碼。在這段代碼中,通過seq
命令輸出1到10,使用for...in
語句產生一個執行10次的循環。每一次循環都執行sleep 1
,並echo
出當前循環對應的數字。
注意:
- 真實的使用場景下,循環次數不一定等於10,或高或低,具體取決於實際的需求。
- 真實的使用場景下,循環體內執行的語句往往比較耗費系統資源,或比較耗時等。
請根據真實場景的各種情況理解本文想要表達的內容。
$ cat test1.sh
#/bin/bash all_num=10 a=$(date +%H%M%S) for num in `seq 1 ${all_num}` do sleep 1 echo ${num} done b=$(date +%H%M%S) echo -e "startTime:\t$a" echo -e "endTime:\t$b"
通過上述代碼可知,為了體現執行的時間,將循環體開始前后的時間打印了出來。
運行結果:
$ sh test1.sh
1 2 3 4 5 6 7 8 9 10 startTime: 193649 endTime: 193659
10次循環,每次sleep 1秒,所以總執行時間10s。
方案
方案1:使用"&"使命令后台運行
在linux中,在命令的末尾加上&
符號,則表示該命令將在后台執行,這樣后面的命令不用等待前面的命令執行完就可以開始執行了。示例中的循環體內有多條命令,則可以以{}
括起來,在大括號后面添加&
符號。
$ cat test2.sh
#/bin/bash all_num=10 a=$(date +%H%M%S) for num in `seq 1 ${all_num}` do { sleep 1 echo ${num} } & done b=$(date +%H%M%S) echo -e "startTime:\t$a" echo -e "endTime:\t$b"
運行結果:
sh test2.sh
startTime: 194147 endTime: 194147 [j-tester@merger142 ~/bin/multiple_process]$ 1 2 3 4 5 6 7 8 9 10
通過結果可知,程序沒有先打印數字,而是直接輸出了開始和結束時間,然后顯示出了命令提示符[j-tester@merger142 ~/bin/multiple_process]$
(出現命令提示符表示腳本已運行完畢),然后才是數字的輸出。這是因為循環體內的命令全部進入后台,所以均在sleep了1秒以后輸出了數字。開始和結束時間相同,即循環體的執行時間不到1秒鍾,這是由於循環體在后台執行,沒有占用腳本主進程的時間。
方案2:命令后台運行+wait
命令
解決上面的問題,只需要在上述循環體的done語句后面加上wait
命令,該命令等待當前腳本進程下的子進程結束,再運行后面的語句。
$ cat test3.sh
#/bin/bash all_num=10 a=$(date +%H%M%S) for num in `seq 1 ${all_num}` do { sleep 1 echo ${num} } & done wait b=$(date +%H%M%S) echo -e "startTime:\t$a" echo -e "endTime:\t$b"
運行結果:
$ sh test3.sh
1 2 3 4 5 6 7 9 8 10 startTime: 194221 endTime: 194222
但這樣依然存在一個問題:
因為&
使得所有循環體內的命令全部進入后台運行,那么倘若循環的次數很多,會使操作系統在瞬間創建出所有的子進程,這會非常消耗系統的資源。如果循環體內的命令又很消耗系統資源,則結果可想而知。
最好的方法是並發的進程是可配置的。
方案3:使用文件描述符控制並發數
$ cat test4.sh
#/bin/bash all_num=10 # 設置並發的進程數 thread_num=5 a=$(date +%H%M%S) # mkfifo tempfifo="my_temp_fifo" mkfifo ${tempfifo} # 使文件描述符為非阻塞式 exec 6<>${tempfifo} rm -f ${tempfifo} # 為文件描述符創建占位信息 for ((i=1;i<=${thread_num};i++)) do { echo } done >&6 # for num in `seq 1 ${all_num}` do { read -u6 { sleep 1 echo ${num} echo "" >&6 } & } done wait # 關閉fd6管道 exec 6>&- b=$(date +%H%M%S) echo -e "startTime:\t$a" echo -e "endTime:\t$b"
運行結果:
$ sh test4.sh
1 3 2 4 5 6 7 8 9 10 startTime: 195227 endTime: 195229
方案4:使用xargs -P
控制並發數
xargs命令有一個-P
參數,表示支持的最大進程數,默認為1。為0時表示盡可能地大,即方案2
的效果。
$ cat test5.sh
#/bin/bash all_num=10 thread_num=5 a=$(date +%H%M%S) seq 1 ${all_num} | xargs -n 1 -I {} -P ${thread_num} sh -c "sleep 1;echo {}" b=$(date +%H%M%S) echo -e "startTime:\t$a" echo -e "endTime:\t$b"
運行結果:
$ sh test5.sh
1 2 3 4 5 6 8 7 9 10 startTime: 195257 endTime: 195259
方案5:使用GNU parallel
命令控制並發數
GNU parallel
命令是非常強大的並行計算命令,使用-j
參數控制其並發數量。
$ cat test6.sh
#/bin/bash all_num=10 thread_num=6 a=$(date +%H%M%S) parallel -j 5 "sleep 1;echo {}" ::: `seq 1 10` b=$(date +%H%M%S) echo -e "startTime:\t$a" echo -e "endTime:\t$b"
運行結果:
$ sh test6.sh
1 2 3 4 5 6 7 8 9 10 startTime: 195616 endTime: 195618
總結
“多線程”的好處不言而喻,雖然shell中並沒有真正的多線程,但上述解決方案可以實現“多線程”的效果,重要的是,在實際編寫腳本時應有這樣的考慮和實現。
另外:
方案3、4、5雖然都可以控制並發數量,但方案3顯然寫起來太繁瑣。
方案4和5都以非常簡潔的形式完成了控制並發數的效果,但由於方案5的parallel命令非常強大,所以十分建議系統學習下。
方案3、4、5設置的並發數均為5,實際編寫時可以將該值作為一個參數傳入。
參考文章
- http://blog.csdn.net/qq_34409701/article/details/52488964
- https://www.codeword.xyz/2015/09/02/three-ways-to-script-processes-in-parallel/
- http://www.gnu.org/software/parallel/
相關知識點
- wait命令
&
后台運行- 文件描述符、mkfifo等
- xargs命令
- parallel命令
鏈接:https://www.cnblogs.com/signjing/p/7074778.html