Share modes, mandatory vs advisory locking, byte-range locks, how .NET maps FileShare on each OS, finding the process holding a file, and safe cross-process coordination.
When you open a file you choose an access (Read, Write) and a share mode (FileShare.None, Read, ReadWrite, Delete) saying what others may do while you hold it.
IOException (Win32 error 32).FileShare.None with an advisory flock, which only other cooperating processes respect.Debug by finding the holder (Process Explorer / handle.exe on Windows, lsof / fuser on Linux). Fix by opening with the right FileShare, disposing handles promptly, retrying transient locks (antivirus, indexers) with backoff, and coordinating writers explicitly instead of relying on lock errors.
new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
// β what I do β what I allow others to do
Both sides must agree. If process A opened with FileShare.Read and process B asks for FileAccess.Write, B fails. If A is writing and B opens for read with FileShare.Read (the default for File.OpenRead), B fails too, because B is saying "nobody else may write" while A already is.
| Opener A holds | B wants | Windows | Linux (.NET) |
|---|---|---|---|
Write, FileShare.None |
Read | Fails | Fails only if B is .NET (advisory flock) |
Write, FileShare.Read |
Read, FileShare.Read |
Fails (B disallows the writer) | Succeeds |
Write, FileShare.Read |
Read, FileShare.ReadWrite |
Succeeds | Succeeds |
| Any | Delete/rename | Fails unless A allowed FileShare.Delete |
Succeeds (unlink) |
FileStream.Lock(position, length) locks part of a file. On Windows it is mandatory for other writers; on Linux it maps to advisory fcntl locks.
A Windows service tailed a log file written by a third-party app and failed every few minutes with the sharing violation. It used File.ReadAllLines, which opens with FileShare.Read, so it refused to share with the writer. Changing to new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete) fixed it. A second, rarer failure came from antivirus scanning the file right after rotation; a short retry with backoff (3 tries, 100β400 ms) handled that.
A very common real-world error. Checks understanding of share modes, OS differences, and practical debugging.
Green flags
Red flags
catch {}.Windows (mandatory) Linux (advisory)
βββββββββββββββ open(W) kernel βββββββββββββββ flock(EX)
β Process A βββshare=NoneβββΊβ β Process A βββββββββββββββΊ lock table
βββββββββββββββ β βββββββββββββββ
βββββββββββββββ open(R) β βββββββββββββββ open(R) ββ no flock call βββΊ OK!
β Process B βββββββββββββββββΊβ β cat / vim β
βββββββββββββββ sharing βββββββββββββββ
violation
On Linux, any program that does not call flock simply ignores the lock. .NET applies the emulation to its own FileStreams, and it can be turned off with the DOTNET_SYSTEM_IO_DISABLEFILELOCKING environment variable (useful on some network file systems where flock misbehaves).
FileShare.Delete. This is why log rotation and deployments on Windows hit "file in use" errors.FileStream, Image.FromFile (keeps the file open until disposed), XmlReader without using.app_offline.htm or shadow copying).| OS | Tool | Command |
|---|---|---|
| Windows | Process Explorer | Find β Find Handle or DLL β file name |
| Windows | Sysinternals handle | handle.exe C:\data\report.csv |
| Windows | Resource Monitor | CPU β Associated Handles β search |
| Linux | lsof | lsof /data/report.csv |
| Linux | fuser | fuser -v /data/report.csv |
| Linux | locks | cat /proc/locks, lslocks |
Locks on files are a weak coordination tool, especially on network shares (SMB/NFS lock semantics vary). Better options:
Mutex or Semaphore on one machine.FileShare.None and keep it open. In a second app, try File.ReadAllText. Run on Windows and Linux; then on Linux try cat on the same file.handle.exe to see who holds it.// Read a file that another process is actively writing (log tailing)
await using var fs = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite | FileShare.Delete); // allow writer and rotation
using var reader = new StreamReader(fs);
while (await reader.ReadLineAsync(ct) is { } line)
Process(line);
// Retry transient sharing violations (antivirus, indexer) with backoff
const int SharingViolation = unchecked((int)0x80070020);
const int LockViolation = unchecked((int)0x80070021);
static async Task<T> WithFileRetryAsync<T>(Func<Task<T>> action, CancellationToken ct)
{
for (int attempt = 1; ; attempt++)
{
try { return await action(); }
catch (IOException ex) when (attempt < 4 &&
(ex.HResult == SharingViolation || ex.HResult == LockViolation))
{
await Task.Delay(TimeSpan.FromMilliseconds(100 * Math.Pow(2, attempt - 1)), ct);
}
}
}
// Release image files immediately: copy into memory instead of Image.FromFile holding the file
byte[] bytes = await File.ReadAllBytesAsync(path, ct);
using var ms = new MemoryStream(bytes);
using var image = SixLabors.ImageSharp.Image.Load(ms);
Mistake: Reading a file with the default share mode while another process writes it.
Why it is wrong: File.OpenRead and File.ReadAllText use FileShare.Read, which means "nobody may write", so the open fails while a writer holds the file.
Instead: Open with FileShare.ReadWrite, plus FileShare.Delete if the file is rotated.
Mistake: Assuming Linux enforces the same locks as Windows.
Why it is wrong: Linux locks are advisory, so cat, vim or a Python script will read and write a file your app holds with FileShare.None.
Instead: Do not rely on file locks for correctness on Linux; coordinate writers explicitly.
Mistake: Blaming another process when your own code holds the handle.
Why it is wrong: Undisposed streams, readers or Image.FromFile keep the file open inside your own process.
Instead: Check which PID holds it with Process Explorer or lsof, and wrap every stream in using.
Mistake: Using file locks on a network share to coordinate several servers. Why it is wrong: SMB and NFS lock behaviour differs by protocol version and client caching, and a crashed client can leave a stale lock that blocks everyone. Instead: Use a database row lock, an Azure Blob lease or a single-writer service.
Mistake: Retrying forever, or treating every IOException as a lock.
Why it is wrong: Infinite retries hide real conflicts, and access-denied or disk-full errors will never succeed on retry.
Instead: Retry only sharing and lock violations (by HResult) a few times with backoff, then fail with a message naming the file.
I treat file locks as a platform detail to work around, not a coordination mechanism to build on. The Windows versus Linux difference means code that works on one platform can silently corrupt data on the other.
| Coordination option | Scope | Reliability | Notes |
|---|---|---|---|
FileShare / flock |
One machine | Mandatory on Windows, advisory on Linux | Fine for avoiding accidental clashes |
Named Mutex or Semaphore |
One machine | Good | Cross-process on the same host |
| Unique temp file + atomic rename | One volume | Good | Readers never see partial files; no lock needed |
Database row lock or sp_getapplock |
Many servers | Good | Uses infrastructure you already have |
| Blob lease (Azure Storage) | Many servers | Good, with lease expiry | Expiry handles crashed holders |
| Single-writer service or queue | Many servers | Best | Removes contention by design |
In cloud and container setups, shared files on Azure Files or NFS are a common source of these errors, so I prefer object storage plus messaging over many instances writing the same file. When diagnosing, I always identify the actual holder process first; guessing wastes hours.
A file descriptor (Linux, a small integer) or handle (Windows, an opaque value) is the process's reference to a kernel object: an open file, socket, pipe, event, mutex, process or thread. The kernel owns the object, and the process holds an index into its own table.
They are limited (e.g. ulimit -n often 1,024 soft by default on Linux shells, much higher in many containers and under systemd), so leaks eventually break the app.
In .NET, SafeHandle wraps the raw value: it releases it in a critical finaliser if you forget, guarantees release even under thread aborts or async exceptions, and prevents handle recycling bugs by reference counting during P/Invoke calls. You still call Dispose (using) to release deterministically.
Data passes through several buffers: your own buffer β StreamWriter/FileStream buffer (4 KiB default for FileStream) β the OS page cache β the disk's own cache β the storage media.
Flush() moves data from the .NET buffer into the OS. It survives a process crash, not a power loss or kernel crash.Flush(flushToDisk: true) calls fsync/FlushFileBuffers, asking the OS and device to persist it.Flush(true), then rename over the target (File.Move(temp, path, overwrite: true)), so readers see either the old or the new file, never half of one.Buffers exist for speed: many small writes become a few large system calls and disk operations.
How to tell it (STAR)
**S:** Our [monitoring agent] kept missing log lines because it failed to open the vendor's log file. **T:** I needed reliable shipping without changing the vendor app. **A:** `handle.exe` showed the vendor writing the file, and our `ReadAllLines` call refused to share with a writer. I opened the file with `FileShare.ReadWrite | FileShare.Delete`, tracked the offset, and added a short retry for antivirus scans. **R:** Sharing errors dropped to [zero] and we stopped losing log lines.
Last updated 23 Oct 2026.