Locking a File
Problem
You want to prevent other threads or processes from modifying a file that you're working on.
Solution
Open the file, then lock it with File#flock. There are two kinds of lock; pass in the File constant for the kind you want.
- File::LOCK_EX gives you an exclusive lock, or write lock. If your thread has an exclusive lock on a file, no other thread or process can get a lock on that file. Use this when you want to write to a file without anyone else being able to write to it.
- File::LOCK_SH will give you a shared lock, or read lock. Other threads and processes can get their own shared locks on the file, but no one can get an exclusive lock. Use this when you want to read a file and know that it won't change while you're reading it.
Once you're done using the file, you need to unlock it. Call File#flock again, and pass in File::LOCK_UN as the lock type. You can skip this step if you're running on Windows.
The best way to handle all this is to enclose the locking and unlocking in a method that takes a block, the way open does:
def flock(file, mode) success = file.flock(mode) if success begin yield file ensure file.flock(File::LOCK_UN) end end return success end
This makes it possible to lock a file without having to worry about unlocking it later. Even if your block raises an exception, the file will be unlocked and another thread can use it.
open('output', 'w') do |f| flock(f, File::LOCK_EX) do |f| f << "Kiss me, I've got a write lock on a file!" end end
Discussion
Different operating systems support different ways of locking files. Ruby's flock implementation tries to hide the differences behind a common interface that looks like Unix's file locking interface. In general, you can use flock as though you were on Unix, and your scripts will work across platforms.
On Unix, both exclusive and shared locks work only if all threads and processes play by the rules. If one thread has an exclusive lock on a file, another thread can still open the file without locking it and wreak havoc by overwriting its contents. That's why it's important to get a lock on any file that might conceivably be used by another thread or another process on the system.
Ruby's block-oriented coding style makes it easy to do the right thing with locking. The following shortcut method works with the flock method previously defined. It takes care of opening, locking, unlocking, and closing a file, letting you focus on whatever you want to do with the file's contents.
def open_lock(filename, openmode="r", lockmode=nil) if openmode == 'r' || openmode == 'rb' lockmode ||= File::LOCK_SH else lockmode ||= File::LOCK_EX end value = nil open(filename, openmode) do |f| flock(f, lockmode) do begin value = yield f ensure f.flock(File::LOCK_UN) # Comment this line out on Windows. end end return value end end
This code creates two threads, each of which want to access the same file. Thanks to locks, we can guarantee that only one thread is accessing the file at a time (see Chapter 20 if you're not comfortable with threads).
t1 = Thread.new do puts 'Thread 1 is requesting a lock.' open_lock('output', 'w') do |f| puts 'Thread 1 has acquired a lock.' f << "At last we're alone!" sleep(5) end puts 'Thread 1 has released its lock.' end t2 = Thread.new do puts 'Thread 2 is requesting a lock.' open_lock('output', 'r') do |f| puts 'Thread 2 has acquired a lock.' puts "File contents: #{f.read}" end puts 'Thread 2 has released its lock.' end t1.join t2.join # Thread 1 is requesting a lock. # Thread 1 has acquired a lock. # Thread 2 is requesting a lock. # Thread 1 has released its lock. # Thread 2 has acquired a lock. # File contents: At last we're alone! # Thread 2 has released its lock.
Nonblocking locks
If you try to get an exclusive or shared lock on a file, your thread will block until Ruby can lock the file. But you might be left waiting a long time, perhaps forever. The code that has the file locked may be buggy and in an infinite loop; or it may itself be blocking, waiting to lock a file that you have locked.
You can avoid deadlock and similar problems by asking for a nonblocking lock. When you do, if Ruby can't lock the file for you, File#flock returns false, rather than waiting (possibly forever) for another thread or process to release its lock. If you don't get a lock, you can wait a while and try again, or you can raise an exception and let the user deal with it.
To make a lock into a nonblocking lock, use the OR operator (|) to combine File:: LOCK_NB with either File::LOCK_EX or File::LOCK_SH.
The following code will print "I've got a lock!" if it can get an exclusive lock on the file "output"; otherwise it will print "I couldn't get a lock." and continue:
def try_lock puts "I couldn't get a lock." unless open_lock('contested', 'w', File::LOCK_EX | File::LOCK_NB) do puts "I've got a lock!" true end end try_lock # I've got a lock! open('contested', 'w').flock(File::LOCK_EX) # Get a lock, hold it forever. try_lock # I couldn't get a lock.
See Also
- Chapter 20, especially Recipe 20.11, "Avoiding Deadlock," which covers other types of deadlock problems in a multithreaded environment