Sockets and threads


#1

Hello,

I observed that even if every thread in a program creates it’s own
Socket, but all to the same Server, then this is not thread-safe.

It is the same situation as if every thread shared the same Socket
without any mutexes. I observed this with TCPSockets, UNIXSockets ans
UDPSockets.

I run Kernel 2.6.14.2, ruby 1.8.4 (2005-12-24) [i686-linux]

The short source code below produces the following output on the server.
If I enabled the mutex (the 3 lines commented out) it would work better.

Got: thread[16] says 65
Got: thread[19] says 68thread[17] says 20
Got:
Got: thread[24] says 97
Got: thread[8] says 64thread[26] says 17thread[13] says 43
Got:
Got:
Got: thread[7] says 84
Got: thread[0] says 78thread[18] says 37
Got: thread[27] says 98
Got:
Got: thread[18] says 13
Got: thread[11] says 19
Got: thread[1] says 91thread[26] says 18
Got:
Got: thread[1] says 53
Got: thread[19] says 42

First start the server with:

ruby tcpsockets.rb server

Then start the client which spawns several threads:

ruby tcpsockets.rb

require ‘socket’
require ‘monitor’

if ARGV[0] == ‘server’
threads = []
server = TCPServer.new ‘localhost’, 8889
putslock = Monitor.new
while socket = server.accept
t = Thread.new(socket) do |sock|
while str = sock.gets
putslock.synchronize do
puts “Got: #{str}”
end
end
end
threads << t
end
threads.each do |t| t.join end
else
Thread.abort_on_exception = true
#socketlock = Monitor.new
threads = []
30.times do |i|
t = Thread.new(i) do |ii|
count = 0
socket = TCPSocket.new ‘localhost’, 8889
puts “Using socket #{socket.object_id}”
while count < 10
#socketlock.synchronize do
socket.puts “thread[#{ii}] says #{Kernel.rand 100}”
socket.flush
#end
sleep Kernel.rand * 0.3
count += 1
end
end
threads << t
end
threads.each do |t| t.join end
end

Is this known/intended behavior?

thanks and a good day!
Dominik


#2

Hello,

I found the bug: In the client code the variable “socket” was already
known to the interpreter, but anyway not in the same scope, so every
thread overwrote the socket and in the end all used the same
connection…

I didn’t know that

if false
yo = 123
else
puts yo.class.name
end

would output “NilClass”…

bye!
Dominik


#3

Dominik Werder wrote:

else
puts yo.class.name
end

would output “NilClass”…

I’m getting more and more defensive about this kind of bug. I usually
just do

Thread.new(…) { |…| some_method(…)}

and then I can code some_method without worrying about local variable
sharing.

Another example is when threads are created in a loop. Even if the
thread code is very small, local vars with shared binding can be
devastating:

a = []
i = 0
(0…100).map do
Thread.new {sleep 0.001; a << i}
i += 1
end
sleep 1
p a.uniq.size

Output is unpredictable, 74, 86, etc. (YMMV)

Obviously, this could be solved by making i local to the block, or
passing it to Thread.new. Without the sleep 0.001, I don’t seem to get
this behavior (the output is 100), but I wouldn’t rely on that: the ruby
thread scheduler could still reasonably schedule threads so that two or
more of them saw the same value of i. So it’s still best to pass
arguments to Thread.new.