Ho un po 'di problemi con la progettazione di uno script bash multiprocesso che va attraverso i siti Web, segue i collegamenti trovati e esegue l'elaborazione su ogni nuova pagina (raccoglie effettivamente e-mail indirizzi ma questo è un dettaglio non importante del problema).Come implementare l'accesso multithreading alla coda basata su file nello script bash
si suppone Lo script di lavorare in questo modo:
- scarica una pagina
- analizza tutti i collegamenti e li aggiunge alla coda
- Fa un po 'di elaborazione senza importanza
- Pops un URL da coda e a partire da
Che di per sé sarebbe piuttosto semplice programmare, il problema deriva da due restrizioni e una caratteristica che lo script deve avere.
- Lo script non deve elaborare un URL due volte
- Lo script deve essere in grado di elaborare n (fornito come argomento) pagine contemporaneamente
- Lo script deve essere POSIX complient (con l'eccezione del riccio) - > in modo che nessun serrature fantasia
Ora, sono riuscito a venire con un'implementazione che usa due file per le code, uno dei quali memorizza tutti gli URL che erano già stati elaborati, e un altro gli URL che erano stati trovati, ma non ancora elaborato.
I principali proces genera semplicemente un gruppo di processi figlio che condividono tutti i file di coda e (in un ciclo fino URL-to-be-lavorati-coda è vuota) si apre in alto un URL da URLs-to-be-processed-queue
, processo nella pagina, prova per aggiungere ogni nuovo collegamento trovato a URLs-already-processed-queue
e se riesce (l'URL non è già lì) aggiungerlo a URLs-to-be-processed-queue
pure.
Il problema sta nel fatto che non è possibile (AFAIK) rendere le operazioni del file di coda atomiche e quindi il blocco è necessario. E bloccare in modo POSIX complient è ... terror ... slow terror.
Il modo in cui lo faccio è la seguente:
#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"
}
Il problema è che la mia esecuzione è rallentata (piace molto lento), quasi così lento che non ha senso utilizzare più di 2
10 (riduzione del blocco in attesa aiuta molto) i processi figli. È possibile disabilitare i blocchi (basta commentare i bit luAckLock e luUnlock) e funziona abbastanza bene (e molto più velocemente) ma ci sono delle condizioni di gara ogni tanto con lo seds -i
e non sembra giusto.
Il peggiore (secondo me) è bloccaggio in prcsNewLinks
quanto richiede parecchio tempo (la maggior parte del tempo-run fondamentalmente) e praticamente impedisce altri processi avvio per elaborare una nuova pagina (in quanto richiede poping nuovo URL da (attualmente bloccata) $todoLinks
coda).
Ora la mia domanda è: come fare meglio, più veloce e più bello?
L'intero script è here (contiene alcuni segnali magici, molte uscite di debug e non un buon codice in genere).
BTW: Sì, hai ragione, a fare questo in bash - e per di più in modo conforme a POSIX - è folle! Ma è un compito universitario quindi devo farlo
// Anche se sento che non mi aspetto realmente da me risolvere questi problemi (poiché le condizioni di gara si verificano più frequentemente solo quando si hanno più di 25 thread che probabilmente non sono qualcosa a la persona sana testerebbe).
Note per il codice:
- Sì, l'attesa dovrebbe avere (e ha già) un tempo casuale. Il codice condiviso qui era solo una dimostrazione del concetto scritta durante una lezione di analisi reale.
- Sì, il numero di debug ouptuts e la loro formattazione è terribile e dovrebbe esserci una funzione di registrazione autonoma. Questo, tuttavia, non è il punto del mio problema.
Il tuo 'per var in $ 1' è un idioma piuttosto sgradevole. Si interrompe quando '" $ 1 "' contiene spazi, dal momento che non lo citate. E non ha senso usare un loop. Basta fare 'var = $ 1'. (la suddivisione di parole e l'espansione globale non avvengono in compiti, quindi non hai bisogno di virgolette, ma non fanno male, quindi non è una cattiva pratica indicare SEMPRE ovunque, anche nei compiti e all'interno di [[$ var == foo]] 'dove non sono necessari.) –
Sì, lo so. Di solito cito tutto, devo averlo dimenticato un po ':). E grazie per la punta che non ha bisogno di loop, è elegante :). – Petrroll
Normalmente si usa un ciclo su "$ @", BTW. Ho dimenticato di dirlo. Anche per funzioni veramente brevi, a volte non mi preoccupo di assegnare i parametri posizionali a 'meaningful_name locale = $ 1'. Nelle funzioni di shell, dovresti usare le variabili 'local' a meno che tu non voglia interagire con le variabili del chiamante con lo stesso nome. –