Menjalankan loop tepat sekali per detik

33

Saya menjalankan loop ini untuk memeriksa dan mencetak beberapa hal setiap detik. Namun, karena perhitungan mungkin memerlukan beberapa ratus milidetik, waktu yang dicetak terkadang melompati satu detik.

Apakah ada cara untuk menulis perulangan sehingga saya dijamin akan mendapatkan hasil cetak setiap detik? (Asalkan, tentu saja, bahwa perhitungan dalam loop memakan waktu kurang dari satu detik :))

while true; do
  TIME=$(date +%H:%M:%S)
  # some calculations which take a few hundred milliseconds
  FOO=...
  BAR=...
  printf '%s  %s  %s\n' $TIME $FOO $BAR
  sleep 1
done
sebagainya
sumber
Mungkin bermanfaat: unix.stackexchange.com/q/60767/117549
Jeff Schaller
26
Perhatikan bahwa " tepat sekali per detik" secara harfiah tidak mungkin dalam kebanyakan kasus karena Anda (biasanya) berjalan di userspace di atas kernel multitasking yang preemptively yang akan menjadwalkan kode Anda sesuai keinginan (sehingga Anda mungkin tidak mendapatkan kembali kontrol segera setelah tidur) berakhir, misalnya). Kecuali jika Anda menulis kode C yang memanggil sched(7)API (POSIX: lihat <sched.h>dan halaman-halaman yang ditautkan dari sana), Anda pada dasarnya tidak dapat memiliki jaminan real-time dari formulir ini.
Kevin
Hanya untuk mendukung apa yang dikatakan @Kevin, menggunakan sleep () untuk mencoba dan mendapatkan segala waktu yang tepat akan gagal, itu hanya menjamin SETIDAKNYA 1 detik tidur. Jika Anda benar - benar membutuhkan pengaturan waktu yang tepat, Anda perlu melihat jam sistem (lihat CLOCK_MONOTONIC) dan memicu berbagai hal berdasarkan waktu-sejak-peristiwa-terakhir + 1, dan pastikan Anda tidak tersandung dengan mengambil> 1sec untuk dijalankan, menghitung waktu berikutnya setelah beberapa operasi, dll.
John U
hanya akan meninggalkan ini di sini falsehoodsabouttime.com
alo Malbarez
Tepat sekali satu detik = gunakan VCXO. Solusi khusus perangkat lunak hanya akan membuat Anda "cukup baik", tetapi tidak tepat.
Ian MacDonald

Jawaban:

65

Untuk sedikit lebih dekat dengan kode asli, yang saya lakukan adalah:

while true; do
  sleep 1 &
  ...your stuff here...
  wait # for sleep
done

Ini sedikit mengubah semantik: jika barang Anda membutuhkan waktu kurang dari satu detik, itu hanya akan menunggu detik penuh berlalu. Namun, jika barang Anda membutuhkan waktu lebih dari satu detik untuk alasan apa pun, itu tidak akan terus menelurkan lebih banyak lagi subproses tanpa pernah ada akhirnya.

Jadi barang Anda tidak pernah berjalan secara paralel, dan tidak di latar belakang, sehingga variabel berfungsi seperti yang diharapkan juga.

Perhatikan bahwa jika Anda memulai tugas latar belakang tambahan juga, Anda harus mengubah waitinstruksi untuk hanya menunggu sleepproses secara khusus.

Jika Anda membutuhkannya agar lebih akurat, Anda mungkin hanya perlu menyinkronkannya ke jam sistem dan tidur ms bukannya detik penuh.


Bagaimana cara menyinkronkan ke jam sistem? Tidak tahu benar, upaya bodoh:

Default:

while sleep 1
do
    date +%N
done

Output: 003511461 010510925 016081282 021643477 028504349 03 ... (terus berkembang)

Disinkronkan:

 while sleep 0.$((1999999999 - 1$(date +%N)))
 do
     date +%N
 done

Output: 002648691 001098397 002514348 001293023 001679137 00 ... (tetap sama)

frostschutz
sumber
9
Trik tidur / tunggu ini sangat pintar!
philfr
Saya bertanya-tanya apakah semua implementasi sleepmenangani pecahan detik?
jcaron
1
@ jcaron tidak semuanya. tapi itu berfungsi untuk gnu sleep dan busybox sleep sehingga tidak eksotis. Anda mungkin dapat melakukan fallback sederhana seperti sleep 0.9 || sleep 1parameter yang tidak valid cukup satu-satunya alasan untuk tidur gagal.
frostschutz
@ frostschutz Saya berharap sleep 0.9akan ditafsirkan sebagai sleep 0implementasi naif (mengingat itulah yang atoiakan dilakukan). Tidak yakin apakah itu benar-benar menghasilkan kesalahan.
jcaron
1
Saya senang melihat pertanyaan ini memicu banyak minat. Saran dan jawaban Anda sangat bagus. Tidak hanya tetap dalam detik, ia juga menempel sedekat mungkin dengan seluruh detik. Impresif! (PS! Sebagai catatan, seseorang harus menginstal GNU Coreutils dan menggunakan gdatepada macOS untuk membuat date +%Npekerjaan.)
sebagainyarin
30

Jika Anda dapat menyusun ulang loop Anda menjadi skrip / oneliner maka cara paling sederhana untuk melakukannya adalah dengan watchdan preciseopsinya.

Anda dapat melihat efeknya dengan watch -n 1 sleep 0.5- ini akan menunjukkan hitungan detik, tetapi kadang-kadang akan melewatkan satu detik. Menjalankannya watch -n 1 -p sleep 0.5akan menghasilkan dua kali per detik, setiap detik, dan Anda tidak akan melihat ada lompatan.

Maelstrom
sumber
11

Menjalankan operasi dalam subkulit yang berjalan sebagai pekerjaan latar belakang akan membuat mereka tidak terlalu mengganggu sleep.

while true; do
  (
    TIME=$(date +%T)
    # some calculations which take a few hundred milliseconds
    FOO=...
    BAR=...
    printf '%s  %s  %s\n' "$TIME" "$FOO" "$BAR"
  ) &
  sleep 1
done

Satu-satunya waktu "dicuri" dari satu detik adalah waktu yang dibutuhkan untuk meluncurkan subkulit, sehingga akhirnya akan melewatkan satu detik, tetapi mudah-mudahan lebih jarang daripada kode aslinya.

Jika kode dalam subkulit berakhir menggunakan lebih dari satu detik, loop akan mulai mengumpulkan pekerjaan latar belakang dan akhirnya kehabisan sumber daya.

Kusalananda
sumber
9

Alternatif lain (jika Anda tidak dapat menggunakan, misalnya, watch -pseperti yang disarankan Maelstrom) adalah sleepenh[ manpage ], yang dirancang untuk ini.

Contoh:

#!/bin/sh

t=$(sleepenh 0)
while true; do
        date +'sec=%s ns=%N'
        sleep 0.2
        t=$(sleepenh $t 1)
done

Perhatikan sleep 0.2di sana simulasi melakukan tugas memakan waktu makan sekitar 200 ms. Meskipun demikian, output nanodetik tetap stabil (well, menurut standar OS non-realtime) - ini terjadi sekali per detik:

sec=1533663406 ns=840039402
sec=1533663407 ns=840105387
sec=1533663408 ns=840380678
sec=1533663409 ns=840175397
sec=1533663410 ns=840132883
sec=1533663411 ns=840263150
sec=1533663412 ns=840246082
sec=1533663413 ns=840259567
sec=1533663414 ns=840066687

Itu di bawah 1ms berbeda, dan tidak ada tren. Itu cukup bagus; Anda harus mengharapkan memantul setidaknya 10ms jika ada beban pada sistem - tetapi masih tidak ada penyimpangan dari waktu ke waktu. Yaitu, Anda tidak akan kehilangan satu detik.

derobert
sumber
7

Dengan zsh:

n=0
typeset -F SECONDS=0
while true; do
  date '+%FT%T.%2N%z'
  ((++n > SECONDS)) && sleep $((n - SECONDS))
done

Jika tidur Anda tidak mendukung detik titik mengambang, Anda dapat menggunakan zshkata zselectganti (setelah a zmodload zsh/zselect):

zmodload zsh/zselect
n=0
typeset -F SECONDS=0
while true; do
  date '+%FZ%T.%2N%z'
  ((++n > SECONDS)) && zselect -t $((((n - SECONDS) * 100) | 0))
done

Itu tidak boleh melayang selama perintah dalam loop membutuhkan waktu kurang dari satu detik untuk dijalankan.

Stéphane Chazelas
sumber
0

Saya memiliki persyaratan yang sama persis untuk skrip shell POSIX, di mana semua pembantu (usleep, GNUsleep, sleepenh, ...) tidak tersedia.

lihat: https://stackoverflow.com/a/54494216

#!/bin/sh

get_up()
{
        read -r UP REST </proc/uptime
        export UP=${UP%.*}${UP#*.}
}

wait_till_1sec_is_full()
{
    while true; do
        get_up
        test $((UP-START)) -ge 100 && break
    done
}

while true; do
    get_up; START=$UP

    your_code

    wait_till_1sec_is_full
done
Bastian Bittorf
sumber