bash并发编程和flock在shell编程中,需要使用并发编程的场景并不多。我们倒是经常会想要某个脚本不要同时出现多次同时执行,比如放在crond中的某个周期任务,如果执行时间较长以至于下次再调度的时间间隔,那么上一个还没执行完就可能又打开一个,这时我们会希望本次不用执行。本质上讲,无论是只保证任何时候系统中只出现一个进程还是多个进程并发,我们需要对进程进行类似的控制。因为并发的时候也会有可能产生竞争条件,导致程序出问题。
我们先来看如何写一个并发的bash程序。在前文讲到作业控制和wait命令使用的时候,我们就已经写了一个简单的并发程序了,我们这次让它变得复杂一点。我们写一个bash脚本,创建一个计数文件,并将里面的值写为0。然后打开100个子进程,每个进程都去读取这个计数文件的当前值,并加1写回去。如果程序执行正确,最后里面的值应该是100,因为每个子进程都会累加一个1写入文件,我们来试试:
[zorro@zorrozou-pc0 bash]$ cat racing.sh #!/bin/bash countfile=/tmp/count if ! [ -f $countfile ] then echo 0 > $countfile fi do_count () { read count < $countfile echo $((++count)) > $countfile } for i in `seq 1 100` do do_count & done wait cat $countfile rm $countfile我们再来看看这个程序的执行结果:
[zorro@zorrozou-pc0 bash]$ ./racing.sh 26 [zorro@zorrozou-pc0 bash]$ ./racing.sh 13 [zorro@zorrozou-pc0 bash]$ ./racing.sh 34 [zorro@zorrozou-pc0 bash]$ ./racing.sh 25 [zorro@zorrozou-pc0 bash]$ ./racing.sh 45 [zorro@zorrozou-pc0 bash]$ ./racing.sh 5多次执行之后,每次得到的结果都不一样,也没有一次是正确的结果。这就是典型的竞争条件引起的问题。当多个进程并发的时候,如果使用的共享的资源,就有可能会造成这样的问题。这里的竞争调教就是:当某一个进程读出文件值为0,并加1,还没写回去的时候,如果有别的进程读了文件,读到的还是0。于是多个进程会写1,以及其它的数字。解决共享文件的竞争问题的办法是使用文件锁。每个子进程在读取文件之前先给文件加锁,写入之后解锁,这样临界区代码就可以互斥执行了:
[zorro@zorrozou-pc0 bash]$ cat flock.sh #!/bin/bash countfile=/tmp/count if ! [ -f $countfile ] then echo 0 > $countfile fi do_count () { exec 3< $countfile #对三号描述符加互斥锁 flock -x 3 read -u 3 count echo $((++count)) > $countfile #解锁 flock -u 3 #关闭描述符也会解锁 exec 3>&- } for i in `seq 1 100` do do_count & done wait cat $countfile rm $countfile [zorro@zorrozou-pc0 bash]$ ./flock.sh 100对临界区代码进行加锁处理之后,程序执行结果正确了。仔细思考一下程序之后就会发现,这里所谓的临界区代码由加锁前的并行,变成了加锁后的串行。flock的默认行为是,如果文件之前没被加锁,则加锁成功返回,如果已经有人持有锁,则加锁行为会阻塞,直到成功加锁。所以,我们也可以利用互斥锁的这个特征,让bash脚本不会重复执行。
我们先来看如何写一个并发的bash程序。在前文讲到作业控制和wait命令使用的时候,我们就已经写了一个简单的并发程序了,我们这次让它变得复杂一点。我们写一个bash脚本,创建一个计数文件,并将里面的值写为0。然后打开100个子进程,每个进程都去读取这个计数文件的当前值,并加1写回去。如果程序执行正确,最后里面的值应该是100,因为每个子进程都会累加一个1写入文件,我们来试试:
[zorro@zorrozou-pc0 bash]$ cat racing.sh #!/bin/bash countfile=/tmp/count if ! [ -f $countfile ] then echo 0 > $countfile fi do_count () { read count < $countfile echo $((++count)) > $countfile } for i in `seq 1 100` do do_count & done wait cat $countfile rm $countfile我们再来看看这个程序的执行结果:
[zorro@zorrozou-pc0 bash]$ ./racing.sh 26 [zorro@zorrozou-pc0 bash]$ ./racing.sh 13 [zorro@zorrozou-pc0 bash]$ ./racing.sh 34 [zorro@zorrozou-pc0 bash]$ ./racing.sh 25 [zorro@zorrozou-pc0 bash]$ ./racing.sh 45 [zorro@zorrozou-pc0 bash]$ ./racing.sh 5多次执行之后,每次得到的结果都不一样,也没有一次是正确的结果。这就是典型的竞争条件引起的问题。当多个进程并发的时候,如果使用的共享的资源,就有可能会造成这样的问题。这里的竞争调教就是:当某一个进程读出文件值为0,并加1,还没写回去的时候,如果有别的进程读了文件,读到的还是0。于是多个进程会写1,以及其它的数字。解决共享文件的竞争问题的办法是使用文件锁。每个子进程在读取文件之前先给文件加锁,写入之后解锁,这样临界区代码就可以互斥执行了:
[zorro@zorrozou-pc0 bash]$ cat flock.sh #!/bin/bash countfile=/tmp/count if ! [ -f $countfile ] then echo 0 > $countfile fi do_count () { exec 3< $countfile #对三号描述符加互斥锁 flock -x 3 read -u 3 count echo $((++count)) > $countfile #解锁 flock -u 3 #关闭描述符也会解锁 exec 3>&- } for i in `seq 1 100` do do_count & done wait cat $countfile rm $countfile [zorro@zorrozou-pc0 bash]$ ./flock.sh 100对临界区代码进行加锁处理之后,程序执行结果正确了。仔细思考一下程序之后就会发现,这里所谓的临界区代码由加锁前的并行,变成了加锁后的串行。flock的默认行为是,如果文件之前没被加锁,则加锁成功返回,如果已经有人持有锁,则加锁行为会阻塞,直到成功加锁。所以,我们也可以利用互斥锁的这个特征,让bash脚本不会重复执行。