ZetCode

Dart FileLock

last modified May 29, 2026

FileLock is a Dart enum that specifies the locking mode when acquiring a lock on an open file via RandomAccessFile.lock(). It is part of the dart:io library and coordinates file access between competing processes or system-level components.

File locks in Dart are advisory on POSIX systems (Linux, macOS) and are implemented using flock(2). On Windows, LockFileEx is used. Locks are tied to an open file description: calling raf.unlock() or closing the RandomAccessFile always releases the lock.

FileLock Enum Values

The FileLock enum has four values that control whether the lock is shared or exclusive and whether the acquisition blocks or fails immediately.

ValueBlockingBehaviour
FileLock.sharedNoShared (read) lock. Multiple shared locks may coexist. Fails immediately if an exclusive lock is held.
FileLock.exclusiveNoExclusive (write) lock. Fails immediately if any lock is held by another open file description.
FileLock.blockingSharedYesShared lock. The returned Future suspends until the lock can be acquired.
FileLock.blockingExclusiveYesExclusive lock. The returned Future suspends until the lock can be acquired.

Basic Exclusive Lock

This example shows the fundamental pattern: open a file, acquire an exclusive lock via raf.lock(), write, then release with raf.unlock().

main.dart
import 'dart:io';
void main() async {
 final file = File('data.txt');
 final raf = await file.open(mode: FileMode.write);
 try {
 await raf.lock(FileLock.exclusive); // acquire lock
 await raf.writeString('Protected content\n');
 print('File written under exclusive lock');
 } finally {
 await raf.unlock(); // always release
 await raf.close();
 }
}

Locking is a separate step from opening: File.open() has no lock parameter. Calling raf.unlock() in a finally block ensures the lock is released even if the write throws.

$ dart main.dart
File written under exclusive lock

Shared Lock for Concurrent Reads

This example demonstrates using a shared lock so multiple readers can access the same file at the same time.

main.dart
import 'dart:io';
import 'dart:convert';
Future<String> readShared(File file) async {
 final raf = await file.open(mode: FileMode.read);
 try {
 await raf.lock(FileLock.shared);
 final length = await raf.length();
 final bytes = await raf.read(length);
 return utf8.decode(bytes);
 } finally {
 await raf.unlock();
 await raf.close();
 }
}
void main() async {
 final file = File('data.txt');
 await file.writeAsString('shared content');
 // Two concurrent shared reads succeed simultaneously
 final results = await Future.wait([
 readShared(file),
 readShared(file),
 ]);
 for (final content in results) {
 print(content.trim());
 }
}

Multiple shared locks coexist freely. Reading the file content through a RandomAccessFile requires raf.read(length) followed by utf8.decode() — there is no readAsString() on RandomAccessFile.

$ dart main.dart
shared content
shared content

Blocking Exclusive Lock

This example shows how FileLock.blockingExclusive serialises concurrent writers without throwing exceptions.

main.dart
import 'dart:io';
Future<void> writeWithBlockingLock(
 File file, String label, String content) async {
 final raf = await file.open(mode: FileMode.append);
 try {
 // Suspends until any existing lock is released
 await raf.lock(FileLock.blockingExclusive);
 print('$label: lock acquired');
 await raf.writeString('$content\n');
 print('$label: write done');
 } finally {
 await raf.unlock();
 await raf.close();
 }
}
void main() async {
 final file = File('log.txt');
 await file.writeAsString(''); // start with empty file
 // Launch two concurrent writers; blocking lock serialises them
 await Future.wait([
 writeWithBlockingLock(file, 'Writer A', 'line from A'),
 writeWithBlockingLock(file, 'Writer B', 'line from B'),
 ]);
 print('Final content:');
 print(await file.readAsString());
}

Future.wait starts both coroutines concurrently, but blockingExclusive queues the second writer until the first finishes, so no data is lost and no exception is thrown.

$ dart main.dart
Writer A: lock acquired
Writer A: write done
Writer B: lock acquired
Writer B: write done
Final content:
line from A
line from B

Lock Conflicts

This example demonstrates what happens when a non-blocking FileLock.exclusive cannot be acquired because another open file description already holds the lock.

main.dart
import 'dart:io';
void main() async {
 final file = File('data.txt');
 await file.writeAsString('content');
 final raf1 = await file.open(mode: FileMode.append);
 await raf1.lock(FileLock.exclusive);
 print('First lock acquired');
 // FileLock.exclusive is non-blocking: throws if lock is already held
 final raf2 = await file.open(mode: FileMode.append);
 try {
 await raf2.lock(FileLock.exclusive);
 print('Second lock acquired'); // not reached
 } on FileSystemException catch (e) {
 print('Lock conflict: ${e.message}');
 } finally {
 await raf2.close(); // close without unlock
 }
 await raf1.unlock();
 await raf1.close();
 print('First lock released');
}

FileLock.exclusive throws a FileSystemException immediately when the lock cannot be granted. Use FileLock.blockingExclusive to wait instead of failing.

$ dart main.dart
First lock acquired
Lock conflict: Failed to lock file
First lock released

File Lock with Timeout

This example implements a try-lock loop that retries the non-blocking FileLock.exclusive with a short back-off until a deadline is reached.

main.dart
import 'dart:io';
import 'dart:async';
/// Tries to acquire an exclusive lock within [timeout].
/// Returns the locked [RandomAccessFile], or null on timeout.
Future<RandomAccessFile?> tryLockWithTimeout(
 File file, Duration timeout) async {
 final deadline = DateTime.now().add(timeout);
 while (DateTime.now().isBefore(deadline)) {
 final raf = await file.open(mode: FileMode.append);
 try {
 await raf.lock(FileLock.exclusive);
 return raf; // acquired
 } on FileSystemException {
 await raf.close(); // busy, back off
 await Future.delayed(const Duration(milliseconds: 100));
 }
 }
 return null;
}
void main() async {
 final file = File('data.txt');
 await file.writeAsString('hello');
 // Simulate a process holding the lock for the full timeout period
 final holder = await file.open(mode: FileMode.append);
 await holder.lock(FileLock.exclusive);
 print('Holder acquired lock');
 final raf = await tryLockWithTimeout(file, const Duration(seconds: 2));
 if (raf != null) {
 print('Lock acquired within timeout');
 await raf.unlock();
 await raf.close();
 } else {
 print('Timed out waiting for lock');
 }
 await holder.unlock();
 await holder.close();
 print('Holder released lock');
}

The 100 ms back-off avoids busy-waiting. Because the holder never releases while tryLockWithTimeout is running, the loop exhausts the deadline and returns null.

$ dart main.dart
Holder acquired lock
Timed out waiting for lock
Holder released lock

Byte-Range Locking

raf.lock() accepts optional start and end byte parameters so you can lock only a specific region of a file. This lets concurrent operations work on non-overlapping sections simultaneously.

main.dart
import 'dart:io';
import 'dart:typed_data';
const recordSize = 16; // bytes per fixed-size record
Future<void> updateRecord(File file, int index, Uint8List data) async {
 final start = index * recordSize;
 final end = start + recordSize;
 final raf = await file.open(mode: FileMode.append);
 try {
 await raf.lock(FileLock.exclusive, start, end); // range-lock
 print('Record $index locked (bytes $start-${end - 1})');
 await raf.setPosition(start);
 await raf.writeFrom(data);
 print('Record $index updated');
 } finally {
 await raf.unlock(start, end); // release same range
 await raf.close();
 }
}
void main() async {
 final file = File('records.bin');
 // Initialise 3 blank records
 await file.writeAsBytes(Uint8List(recordSize * 3));
 // Records 0 and 2 are non-overlapping, so both locks succeed concurrently
 await Future.wait([
 updateRecord(file, 0, Uint8List.fromList(List.filled(recordSize, 0xAA))),
 updateRecord(file, 2, Uint8List.fromList(List.filled(recordSize, 0xBB))),
 ]);
 final bytes = await file.readAsBytes();
 print('Record 0, byte 0: 0x${bytes[0].toRadixString(16).padLeft(2, '0')}');
 print('Record 2, byte 32: 0x${bytes[32].toRadixString(16).padLeft(2, '0')}');
}

The same [start, end) range must be passed to both lock() and unlock(). Locks on non-overlapping ranges are independent, so records 0 and 2 can be updated at the same time.

$ dart main.dart
Record 0 locked (bytes 0-15)
Record 2 locked (bytes 32-47)
Record 0 updated
Record 2 updated
Record 0, byte 0: 0xaa
Record 2, byte 32: 0xbb

Safe Read-Modify-Write

A common concurrency hazard is a read-modify-write cycle where two processes read the same old value before either has written back. An exclusive lock over the entire cycle prevents this.

main.dart
import 'dart:io';
import 'dart:convert';
Future<int> incrementCounter(File file) async {
 final raf = await file.open(mode: FileMode.append);
 try {
 await raf.lock(FileLock.blockingExclusive);
 // Read current value
 await raf.setPosition(0);
 final length = await raf.length();
 final raw = utf8.decode(await raf.read(length)).trim();
 final count = int.tryParse(raw) ?? 0;
 // Write incremented value
 final next = count + 1;
 await raf.truncate(0);
 await raf.setPosition(0);
 await raf.writeString('$next');
 return next;
 } finally {
 await raf.unlock();
 await raf.close();
 }
}
void main() async {
 final file = File('counter.txt');
 if (!await file.exists()) await file.writeAsString('0');
 for (var i = 0; i < 3; i++) {
 final value = await incrementCounter(file);
 print('Counter is now: $value');
 }
}

FileMode.append opens the file for reading and writing without truncating it. raf.truncate(0) clears the file before writing the new value so no stale bytes remain. The full read-modify-write cycle is atomic because blockingExclusive keeps other callers waiting.

$ dart main.dart
Counter is now: 1
Counter is now: 2
Counter is now: 3

Lock Guard Helper

Repeating the open/lock/unlock/close boilerplate at every call site is error-prone. A generic helper wraps it and guarantees the lock is always released.

main.dart
import 'dart:io';
// Serialises same-isolate callers; POSIX fcntl locks are per-process and
// therefore do not block multiple descriptors opened by the same process.
Future<void> _queue = Future.value();
/// Opens [file], acquires a blocking exclusive lock, runs [action],
/// then unlocks and closes — even if [action] throws.
///
/// Calls within the same isolate are serialised via [_queue]; the OS lock
/// guards concurrent access from other processes.
Future<T> withExclusiveLock<T>(
 File file,
 Future<T> Function(RandomAccessFile raf) action) {
 final task = _queue.then((_) async {
 final raf = await file.open(mode: FileMode.append);
 try {
 await raf.lock(FileLock.blockingExclusive);
 return await action(raf);
 } finally {
 await raf.unlock();
 await raf.close();
 }
 });
 // Advance the queue; swallow errors so one failure doesn't stall later calls.
 _queue = task.then((_) {}, onError: (_) {});
 return task;
}
void main() async {
 final logFile = File('app.log');
 await logFile.writeAsString(''); // clear log
 Future<void> appendLog(String message) =>
 withExclusiveLock(logFile, (raf) async {
 final pos = await raf.length();
 await raf.setPosition(pos);
 await raf.writeString(
 '${DateTime.now().toIso8601String()} $message\n');
 });
 // Concurrent log writes are serialised by the blocking lock
 await Future.wait([
 appendLog('Server started'),
 appendLog('Connection accepted'),
 appendLog('Request processed'),
 ]);
 print(await logFile.readAsString());
}

The higher-order function withExclusiveLock eliminates the possibility of forgetting unlock() — the finally block runs even if the action throws. Each appendLog call is one line, and all lock bookkeeping is hidden in the helper.

There is a subtlety on Linux: Dart's FileLock.blockingExclusive uses POSIX fcntl record locks, which are per-process. Multiple file descriptors opened by the same process therefore never block each other, so the OS lock alone cannot serialise concurrent calls within one isolate. The module-level _queue future chain fills that gap: each call appends itself to the chain and only starts after the previous one completes. The OS lock still matters — it prevents races with other processes that open the same file.

$ dart main.dart
2026年05月29日T10:00:00.000 Server started
2026年05月29日T10:00:00.001 Connection accepted
2026年05月29日T10:00:00.002 Request processed

Best Practices

  • Call lock() after open(): File.open() has no lock parameter; always acquire the lock as a separate step with raf.lock().
  • Always unlock in finally: Place raf.unlock() and raf.close() in a finally block so they run even when an exception is thrown.
  • Non-blocking vs blocking: Use shared / exclusive (non-blocking) when you want to fail fast and handle contention yourself. Use blockingShared / blockingExclusive when contention is rare and waiting is acceptable.
  • Lock the full operation: For read-modify-write cycles, keep the lock held for the entire sequence — not just the write — to prevent lost updates.
  • Byte-range granularity: For structured binary files, lock only the record or section being modified so other parts of the file remain accessible.
  • Consistent lock order: When acquiring locks on multiple files, always acquire them in the same order across all code paths to avoid deadlocks.
  • Use a helper: A generic withExclusiveLock wrapper makes call sites concise and prevents unlock omissions.

Source

Dart FileLock Documentation

This tutorial covered Dart's FileLock enum with practical examples: acquiring exclusive and shared locks, using blocking locks to serialise concurrent writers, detecting and handling lock conflicts, implementing a timeout loop, locking specific byte ranges, performing safe read-modify-write cycles, and building a reusable lock-guard helper.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.

AltStyle によって変換されたページ (->オリジナル) /