Hi all,
I’ve not done a lot of multi-threaded programming before, and I now find
myself in the middle of writing a multi-threaded application. I’d
appreciate someone who knows more about thread-safety than me to do a
review of the code below and tell me where it falls short in that
respect (as I’m sure it does). I can’t immediately see anywhere in the
code which is likely to cause problems and I’ve tried to write it so
that it doesn’t have problems, but I haven’t used Mutex or any other
library traditionally associated with thread-safety so I’m feeling a bit
nervous about it. I learn best by getting my hands dirty, so I’ve given
it a shot and I’d like someone to point out my mistakes.
Some background: the application (known as SAMS) is essentially no more
than a database-backed DRb server. DRb takes care of most of the
threading, by starting a new thread for each connection for me. Rather
than starting a separate database connection for each thread, or trying
to share one connection between all threads, I’ve tried to write a
connection pooling class. The other application code generally requests
a connection from the pool immediately before performing its queries and
releases it immediately after, so unless the queries go for quite some
time no one thread will be hanging onto a connection for more than a
second or so.
Below is the code I have written. The idea is that other application
code calls SAMS::Database.get_handle{ |handle| handle.execute… } and
this module takes care of the rest. Also, the application’s shutdown
code will call SAMS::Database.destroy to cleanly disconnect all the handles.
require ‘dbi’
module SAMS
module Database
# These values will eventually be taken from a configuration file
MAX_FREE_HANDLES = 10
MAX_HANDLES = 20
@handles = []
@free_handles = []
@stopped = false
class << self
def new_handle
# The DB connection parameters will eventually be read from a
# configuration file somewhere
h = DBI.connect('DBI:pg:sams', 'samsd', 'foo')
h['AutoCommit'] = false
@handles << h
h
end
def destroy_handle
h = @free_handles.shift
if h
h.disconnect
@handles.delete h
end
end
def allocate_handle
if @free_handles.empty?
if @handles.length < MAX_HANDLES
return new_handle
else
while @free_handles.empty?
sleep(1)
end
return allocate_handle
end
else
return @free_handles.shift
end
end
private :new_handle, :destroy_handle, :allocate_handle
def get_handle
return nil if @stopped
begin
h = allocate_handle
yield h
ensure # Make sure the handle gets put back in the list
@free_handles << h
end
while @free_handles.length > MAX_FREE_HANDLES
destroy_handle
end
end
def destroy
# Don't allocate any more handles
@stopped = true
# Destroy all handles, waiting for them to be released first
while @handles.length > 0
if @free_handles.length > 0
destroy_handle
else
# Wait for a handle to be released
sleep(1)
end
end
end
end
end
end
···
–
Tim Bates
tim@bates.id.au