Éviter l'attente occupée dans bash, sans la commande sleep

19

Je sais que je peux attendre une condition pour devenir vrai en bash en faisant:

while true; do
  test_condition && break
  sleep 1
done

Mais il crée 1 sous-processus à chaque itération (sommeil). Je pourrais les éviter en faisant:

while true; do
  test_condition && break
done

Mais il utilise beaucoup de CPU (attente occupée). Pour éviter les sous-processus et les attentes occupées, j'ai trouvé la solution ci-dessous, mais je la trouve moche:

my_tmp_dir=$(mktemp -d --tmpdir=/tmp)    # Create a unique tmp dir for the fifo.
mkfifo $my_tmp_dir/fifo                  # Create an empty fifo for sleep by read.
exec 3<> $my_tmp_dir/fifo                # Open the fifo for reading and writing.

while true; do
  test_condition && break
  read -t 1 -u 3 var                     # Same as sleep 1, but without sub-process.
done

exec 3<&-                                # Closing the fifo.
rm $my_tmp_dir/fifo; rmdir $my_tmp_dir   # Cleanup, could be done in a trap.

Remarque: dans le cas général, je ne peux pas simplement utiliser read -t 1 varsans le fifo, car il consommera stdin et ne fonctionnera pas si stdin n'est pas un terminal ou un pipe.

Puis-je éviter les sous-processus et les attentes occupées de manière plus élégante?

jfg956
la source
1
trueest une fonction intégrée et ne crée pas de sous-processus dans bash. une attente occupée sera toujours mauvaise.
jordanm
@joranm: vous avez raison true, question mise à jour.
jfg956
Pourquoi pas sans fifo? Simplement read -t 1 var.
ott--
@ott: vous avez raison, mais cela consommera stdin. En outre, cela ne fonctionnera pas si stdin n'est pas un terminal ou un canal.
jfg956
Si la maintenabilité est un problème, je suggérerais fortement d'aller avec le sleepcomme dans le premier exemple. Le second, même s'il peut fonctionner, ne sera pas facile pour quiconque de s'adapter à l'avenir. Le code simple a également un plus grand potentiel de sécurité.
Kusalananda

Réponses:

17

Dans les versions plus récentes de bash(au moins v2), les commandes internes peuvent être chargées (via enable -f filename commandname) au moment de l'exécution. Un certain nombre de ces prédéfinis chargeables est également distribué avec les sources bash, et en sleepfait partie. La disponibilité peut différer d'un OS à l'autre (et même d'une machine à l'autre), bien sûr. Par exemple, sur openSUSE, ces commandes internes sont distribuées via le package bash-loadables.

Edit: correction du nom du package, ajout d'une version bash minimale.

Ansgar Esztermann
la source
Wow, c'est ce que je recherche, et j'apprends certainement quelque chose sur le programme intégré chargeable: +1. Je vais essayer cela, et pourtant c'est la meilleure réponse.
jfg956
1
Ça marche ! Sur debian, le paquet est construit par bash. Il ne comprend que les sources et le Makefile doit être modifié, mais j'ai pu l'installer en sleeptant que intégré. Merci.
jfg956
9

La création d'un grand nombre de sous-processus est une mauvaise chose dans une boucle interne. Créer un sleepprocessus par seconde est OK. Il n'y a rien de mal à

while ! test_condition; do
  sleep 1
done

Si vous voulez vraiment éviter le processus externe, vous n'avez pas besoin de garder le fifo ouvert.

my_tmpdir=$(mktemp -d)
trap 'rm -rf "$my_tmpdir"' 0
mkfifo "$my_tmpdir/f"

while ! test_condition; do
  read -t 1 <>"$my_tmpdir/f"
done
Gilles 'SO- arrête d'être méchant'
la source
Vous avez raison sur un processus par seconde étant des arachides (mais ma question était de trouver un moyen de le supprimer). À propos de la version plus courte, elle est plus agréable que la mienne, donc +1 (mais j'ai supprimé le mkdircomme c'est fait par mktemp(sinon, c'est une condition de course)). C'est également vrai pour ce while ! test_condition;qui est plus agréable que ma solution initiale.
jfg956
7

J'ai récemment eu besoin de faire ça. J'ai proposé la fonction suivante qui permettra à bash de dormir indéfiniment sans appeler de programme externe:

snore()
{
    local IFS
    [[ -n "${_snore_fd:-}" ]] || { exec {_snore_fd}<> <(:); } 2>/dev/null ||
    {
        # workaround for MacOS and similar systems
        local fifo
        fifo=$(mktemp -u)
        mkfifo -m 700 "$fifo"
        exec {_snore_fd}<>"$fifo"
        rm "$fifo"
    }
    read ${1:+-t "$1"} -u $_snore_fd || :
}

REMARQUE: j'ai déjà publié une version de ceci qui ouvrirait et fermerait le descripteur de fichier à chaque fois, mais j'ai constaté que sur certains systèmes, le faire des centaines de fois par seconde finirait par se bloquer. Ainsi, la nouvelle solution conserve le descripteur de fichier entre les appels à la fonction. Bash le nettoiera quand même à la sortie.

Cela peut être appelé comme / bin / sleep, et il dormira pendant la durée demandée. Appelé sans paramètres, il se bloque pour toujours.

snore 0.1  # sleeps for 0.1 seconds
snore 10   # sleeps for 10 seconds
snore      # sleeps forever

Il y a un article avec des détails excessifs sur mon blog ici

boulon
la source
1
Excellente entrée de blog. Cependant, je suis allé là-bas à la recherche d'une explication pourquoi read -t 10 < <(:)revient immédiatement en read -t 10 <> <(:)attendant les 10 secondes, mais je ne comprends toujours pas.
Amir
En read -t 10 <> <(:)quoi ça veut dire <>?
CodeMedic
<> ouvre le descripteur de fichier pour la lecture et l'écriture, même si la substitution de processus sous-jacente <(:) autorise uniquement la lecture. C'est un hack qui fait que Linux, et Linux en particulier, suppose que quelqu'un pourrait y écrire, donc la lecture se bloquera en attendant une entrée qui n'arrivera jamais. Il ne le fera pas sur les systèmes BSD, auquel cas la solution de contournement se déclenchera.
boulon
3

Dans ksh93or mksh, sleepest un shell intégré, donc une alternative pourrait être d'utiliser ces shells à la place de bash.

zsha également un zselectintégré (chargé avec zmodload zsh/zselect) qui peut dormir pendant un nombre donné de centièmes de secondes avec zselect -t <n>.

Scrutinisateur
la source
2

Comme l' a dit l' utilisateur yoi , si dans votre script est ouvert stdin , alors au lieu de dormir 1, vous pouvez simplement utiliser:

read -t 1 3<&- 3<&0 <&3

Dans Bash version 4.1 et plus récente, vous pouvez utiliser un nombre flottant, par exemple read -t 0.3 ...

Si dans un script stdin est fermé (le script est appelé my_script.sh < /dev/null &), alors vous devez utiliser un autre descripteur ouvert, qui ne produit pas de sortie lorsque la lecture est exécutée, par exemple. stdout :

read -t 1 <&1 3<&- 3<&0 <&3

Si dans un script tous les descripteurs sont fermés ( stdin , stdout , stderr ) (par exemple parce qu'il est appelé comme démon), alors vous devez trouver tout fichier existant qui ne produit pas de sortie:

read -t 1 </dev/tty10 3<&- 3<&0 <&3
Mysak
la source
read -t 1 3<&- 3<&0 <&3est le même que read -t 0. C'est juste la lecture de stdin avec timeout.
Stéphane Chazelas
1

Cela fonctionne à partir d'un shell de connexion ainsi que d'un shell non interactif.

#!/bin/sh

# to avoid starting /bin/sleep each time we call sleep, 
# make our own using the read built in function
xsleep()
{
  read -t $1 -u 1
}

# usage
xsleep 3
Omar
la source
Cela a également fonctionné sur Mac OS X v10.12.6
b01
1
Ce n'est pas recommandé. Si plusieurs scripts l'utilisent en même temps, ils obtiennent tous SIGSTOP car ils essaient tous de lire stdin. Votre stdin est bloqué pendant que cela attend. N'utilisez pas stdin pour cela. Vous voulez de nouveaux descripteurs de fichiers différents.
Normadize
1
@Normadize Il y a une autre réponse ici ( unix.stackexchange.com/a/407383/147685 ) qui traite du souci d'utiliser des descripteurs de fichiers gratuits. Sa version minimale est read -t 10 <> <(:).
Amir
0

Avez-vous vraiment besoin d'un fifo? La redirection de stdin vers un autre descripteur de fichier devrait également fonctionner.

{
echo line | while read line; do
   read -t 1 <&3
   echo "$line"
done
} 3<&- 3<&0

Inspiré par: Lire l'entrée dans bash à l'intérieur d'une boucle while

yoi
la source
1
Cela ne fait pas de sommeil, cela consomme toujours stdin du terminal.
jfg956
0

Une légère amélioration par rapport aux solutions mentionnées ci-dessus (sur lesquelles je me suis basé).

bash_sleep() {
    read -rt "${1?Specify sleep interval in seconds}" -u 1 <<<"" || :;
}

# sleep for 10 seconds
bash_sleep 10

Réduit le besoin d'un fifo et donc pas de nettoyage à faire.

CodeMedic
la source
1
Ce n'est pas recommandé. Si plusieurs scripts l'utilisent en même temps, ils obtiennent tous SIGSTOP car ils essaient tous de lire stdin. Votre stdin est bloqué pendant que cela attend. N'utilisez pas stdin pour cela. Vous voulez de nouveaux descripteurs de fichiers différents.
Normadize
@Normadize Je n'ai jamais pensé à ça; s'il vous plaît pouvez-vous élaborer ou me diriger vers une ressource où je peux en savoir plus à ce sujet.
CodeMedic
@CodeMedic Il y a une autre réponse ici ( unix.stackexchange.com/a/407383/147685 ) qui traite du souci d'utiliser des descripteurs de fichiers gratuits. Sa version minimale est read -t 10 <> <(:).
Amir