When cron jobs overlap, when multiple deploy agents run simultaneously, when parallel workers all try to migrate the same database — concurrency bugs can corrupt data or cause double-processing. Shell scripts have three built-in locking primitives: flock, PID files, and directory atomicity. Knowing which to use and when is essential production knowledge.
1
flock — kernel-level file locking
BASH
# flock acquires a kernel file lock — cleaned up automatically on process exit
# even if the script crashes or is killed with SIGKILL
# ── Simplest form: wrap entire script ─────────────────────
flock -n /var/lock/myapp-backup.lock ./backup.sh
# -n: non-blocking — exit immediately if lock is held
# Without -n: block (wait) until lock is released
# ── With timeout ──────────────────────────────────────────
flock -w 10 /var/lock/myapp.lock ./migrate.sh
# -w 10: wait up to 10 seconds for the lock, then exit
# ── Inside a script using a file descriptor ───────────────
exec 200>/var/lock/myapp.lock
if ! flock -n 200; then
echo "Another instance is running. Exiting." >&2
exit 1
fi
# ... protected critical section here ...
# Lock released automatically when fd 200 closes (script exits)
# ── Self-locking script header pattern ────────────────────
[ "${FLOCKER}" != "$0" ] && \
exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :
# Place at top of any script — it re-executes itself with a lock on itself
# ── Flock with cleanup notification ───────────────────────
exec 9>/var/lock/daily_report.lock
flock -w 5 9 || { echo "Cannot acquire lock"; exit 1; }
trap 'flock -u 9; rm -f /var/lock/daily_report.lock' EXIT
echo "Lock acquired by PID $$ at $(date)" >&9
2
PID files — traditional locking
BASH
#!/usr/bin/env bash
# PID file locking — checks if the previous instance is still alive
PID_FILE="/var/run/myapp-worker.pid"
acquire_pidlock() {
if [[ -f "${PID_FILE}" ]]; then
OLD_PID=$(cat "${PID_FILE}")
if kill -0 "${OLD_PID}" 2>/dev/null; then
echo "Already running as PID ${OLD_PID}" >&2
return 1
fi
# Stale PID file — previous run crashed
echo "Removing stale PID file (PID ${OLD_PID} no longer running)"
fi
echo "$$" > "${PID_FILE}"
trap 'rm -f "${PID_FILE}"' EXIT INT TERM
}
acquire_pidlock || exit 1
echo "Worker running as PID $$"
# ... do work ...
# ── Check if another instance is running ───────────────────
is_running() {
[[ -f "${PID_FILE}" ]] && kill -0 "$(cat "${PID_FILE}")" 2>/dev/null
}
if is_running; then
echo "Service is running (PID $(cat "${PID_FILE}"))"
else
echo "Service is NOT running"
fi
3
Parallel workers with semaphores
BASH
# ── Process a list with N workers in parallel ─────────────
MAX_PARALLEL=4
process_item() {
local item="${1}"
echo "Processing: ${item}"
sleep 1 # simulate work
}
# Method 1: xargs parallel ────────────────────────────────
cat items.txt | xargs -P "${MAX_PARALLEL}" -I{} bash -c 'process_item "$@"' -- {}
# Method 2: background jobs with wait ─────────────────────
running=0
while read -r item; do
process_item "${item}" &
(( ++running ))
(( running >= MAX_PARALLEL )) && { wait -n 2>/dev/null || wait; (( running-- )); }
done < items.txt
wait # wait for remaining jobs
# Method 3: GNU parallel (most powerful) ──────────────────
parallel -j4 process_item {} ::: $(cat items.txt)
cat items.txt | parallel -j4 './worker.sh {}'
# ── Semaphore with flock ───────────────────────────────────
# Allow at most N simultaneous processes using numbered lock files
run_with_semaphore() {
local sem_dir="/tmp/myapp-sem"
local max="${1}"; shift
mkdir -p "${sem_dir}"
for i in $(seq 1 "${max}"); do
if flock -n "${sem_dir}/slot_${i}" "$@"; then
return 0
fi
done
echo "Semaphore full — waiting for slot"
flock "${sem_dir}/slot_1" "$@"
}
vriddh@prod-01:~/scripts$flock -n /var/lock/backup.lock ./backup.sh & flock -n /var/lock/backup.lock ./backup.sh
[1] 18421 Backup started...
flock: /var/lock/backup.lock: already locked by another process
vriddh@prod-01:~/scripts$cat items.txt | xargs -P4 -I{} ./process.sh {}
Processing item-01 (PID 18501)
Processing item-02 (PID 18502)
Processing item-03 (PID 18503)
Processing item-04 (PID 18504)
█
✔ Locking rules — Prefer
flock over PID files for single-machine locking — it is automatically released even on SIGKILL. Use PID files when you also need to send signals to the running process. Use flock -n for cron jobs that should skip if already running, and flock -w N for jobs that should wait a bounded time. Always pair flock with trap ... EXIT to clean up lock files. For distributed locking across machines, use Redis SET NX EX (page 59).