【发布时间】:2023-03-10 21:47:01
【问题描述】:
我在设计一个多处理 bash 脚本时遇到了一些问题,该脚本通过网站、跟踪找到的链接并在每个新页面上进行一些处理(它实际上收集电子邮件地址,但这是一个不重要的问题细节)。
脚本应该是这样工作的:
- 下载页面
- 解析所有链接并将它们添加到队列中
- 做一些不重要的处理
- 从队列中弹出一个 URL 并开始于
这本身很容易编程,问题来自两个限制和脚本需要具有的功能。
- 脚本不得两次处理一个 URL
- 脚本必须能够同时处理 n 个(作为参数提供)页面
- 脚本必须符合 POSIX(curl 除外)-> 所以没有花哨的锁
现在,我设法提出了一个实现,它使用两个文件作为队列,其中一个存储所有已处理的 URL,另一个存储已找到但尚未处理的 URL。
主进程简单地生成一堆共享队列文件的子进程,并且(在一个循环中,直到 URLs-to-be-processed-queue 为空)从URLs-to-be-processed-queue 弹出一个 URL,处理页面,尝试将每个新找到的链接添加到URLs-already-processed-queue,如果成功(URL 不存在)也将其添加到URLs-to-be-processed-queue。
问题在于您不能(AFAIK)使队列文件操作原子化,因此锁定是必要的。并且以 POSIX 兼容的方式锁定是......恐怖......缓慢的恐怖。
我的做法如下:
#Pops first element from a file ($1) and prints it to stdout; if file emepty print out empty return 1
fuPop(){
if [ -s "$1" ]; then
sed -nr '1p' "$1"
sed -ir '1d' "$1"
return 0
else
return 1
fi
}
#Appends line ($1) to a file ($2) and return 0 if it's not in it yet; if it, is just return 1
fuAppend(){
if grep -Fxq "$1" < "$2"; then
return 1
else
echo "$1" >> "$2"
return 0
fi
}
#There're multiple processes running this function.
prcsPages(){
while [ -s "$todoLinks" ]; do
luAckLock "$linksLock"
linkToProcess="$(fuPop "$todoLinks")"
luUnlock "$linksLock"
prcsPage "$linkToProcess"
...
done
...
}
#The prcsPage downloads it, does some magic and than calls prcsNewLinks and prcsNewEmails that both get list of new emails / new urls in $1
#$doneEmails, ..., contain file path, $mailLock, ..., contain dir path
prcsNewEmails(){
luAckLock "$mailsLock"
for newEmail in $1; do
if fuAppend "$newEmail" "$doneEmails"; then
echo "$newEmail"
fi
done
luUnlock "$mailsLock"
}
prcsNewLinks(){
luAckLock "$linksLock"
for newLink in $1; do
if fuAppend "$newLink" "$doneLinks"; then
fuAppend "$newLink" "$todoLinks"
fi
done
luUnlock "$linksLock"
}
问题是我的实现很慢(就像真的很慢),几乎慢到使用超过 2 10 (减少锁等待有很大帮助)孩子没有意义过程。您实际上可以禁用锁(只需注释掉 luAckLock 和 luUnlock 位),它工作得很好(而且速度更快),但是seds -i 每隔一段时间就会出现竞争条件,感觉不对.
最糟糕的(如我所见)锁定prcsNewLinks,因为它需要很长时间(基本上大部分时间运行)并且实际上阻止其他进程开始处理新页面(因为它需要弹出新页面来自(当前锁定)$todoLinks 队列的 URL)。
现在我的问题是,如何做得更好、更快、更好?
整个脚本是here(它包含一些信号魔法、大量调试输出,而且通常不是那么好的代码)。
顺便说一句:是的,你是对的,在 bash 中执行此操作 - 更符合 POSIX 的方式 - 太疯狂了!但这是大学作业,所以我不得不这样做
//虽然我觉得我并没有真正期望我解决这些问题(因为竞争条件只有在拥有 25 个以上线程时才会更频繁地出现,这可能不是一个理智的人会测试的)。
代码注释:
- 是的,等待应该有(并且已经有)一个随机时间。此处分享的代码只是在实际分析讲座中编写的概念证明。
- 是的,调试输出的数量及其格式非常糟糕,应该有独立的日志记录功能。不过,这不是我的问题所在。
【问题讨论】:
-
你的
for var in $1是一个非常讨厌的成语。当"$1"包含空格时它会中断,因为您没有引用它。而且根本没有必要使用循环。只需执行var=$1。 (分词和全局扩展不会在作业中发生,因此您不需要引号。但它们不会受到伤害,因此始终在任何地方引用都不是坏习惯,即使在作业中和[[ $var == foo ]]中它们所在的地方也是如此。需要。) -
是的,我知道。我通常引用所有内容,一定忘记了一点:)。感谢您提供不需要循环的提示,它很优雅:)。
-
通常你会在 "$@" 上使用循环,顺便说一句。忘了提那个。同样对于非常短的函数,我有时不会费心将位置参数分配给
local meaningful_name=$1。在 shell 函数中,您应该使用local变量,除非您想与调用者的同名变量进行交互。
标签: multithreading bash posix locks