I'm working on my first attempts on Sockets and Threading and am running into an issue where I believe I am hitting a thread cap, but not entirely sure.
Basically I have a server.rb which opens a TCPServer and a game.rb which connects to the server. On the server, when the user connects I output some general information and then want to start reading data that is passed through from the game.rb. I believe the problem I am running into is I am just creating a new thread everytime the loop executes and I think I'm meeting the cap. I am also not sure if this is best practice. I wanted to avoid using other APIs until I got a decent grasp on the basics, so I tried to follow the code from https://github.com/sausheong/tanks.
server.rb
require 'socket'
# lsof -i :2000
# kill ID
class Server
def initialize(host, port)
puts "starting arena"
#server = TCPServer.open(host, port)
#sprites = Hash.new
#players = Hash.new
handle_connection(#server.accept)
end
def handle_connection(socket)
_, port, host = socket.peeraddr
user = "#{host}:#{port}"
puts "#{user} has joined!"
puts "------"
loop do
thread = Thread.start(socket) do |client|
puts 'starting new thread'
data = socket.readpartial(4096)
data_array = data.split("\n")
if data_array && data_array.any?
begin
data_array.each do |row|
message = row.split("|")
puts message
end
rescue Exception => exception
puts "exception happened?"
puts exception.inspect
end
end
end
end # end loop
rescue EOFError => err
puts "error"
puts "Closing the connection. Bye!"
socket.close
puts err
end # handle_connection end
end # class end
Server.new('localhost', 2000)
game.rb
# create a server
# join server
# move Player
# communicate to server that player moved
# update players position for server?
require 'gosu'
require 'socket'
class Player
def initialize
#image = Gosu::Image.new("media/starfighter.bmp")
#x = #y = #vel_x = #vel_y = #angle = 0.0
#score = 0
end
def warp(x, y)
#x, #y = x, y
end
def turn_left
#angle -= 4.5
end
def turn_right
#angle += 4.5
end
def accelerate
#vel_x += Gosu.offset_x(#angle, 0.5)
#vel_y += Gosu.offset_y(#angle, 0.5)
end
def move
#x += #vel_x
#y += #vel_y
#x %= 640
#y %= 480
#vel_x *= 0.95
#vel_y *= 0.95
end
def draw
#image.draw_rot(#x, #y, 1, #angle)
end
end
class Client
def initialize(host, port)
#client = TCPSocket.open(host, port)
send_data("321|123")
end
def send_data(data)
#client.write(data)
end
# puts "enter your name:"
# client.write gets
#
# while line = client.gets
# puts line.chop
# end
end
class Tutorial < Gosu::Window
def initialize(server, port)
super 640, 480
self.caption = "Tutorial Game"
#client = Client.new(server, port)
#background_image = Gosu::Image.new("media/space.png", :tileable => true)
#player = Player.new
#player.warp(320, 240)
end
def update
if Gosu.button_down? Gosu::KB_LEFT or Gosu::button_down? Gosu::GP_LEFT
#player.turn_left
#client.send_data("left")
end
if Gosu.button_down? Gosu::KB_RIGHT or Gosu::button_down? Gosu::GP_RIGHT
#player.turn_right
end
if Gosu.button_down? Gosu::KB_UP or Gosu::button_down? Gosu::GP_BUTTON_0
#player.accelerate
end
#player.move
end
def draw
#player.draw
#background_image.draw(0, 0, 0)
end
def button_down(id)
if id == Gosu::KB_ESCAPE
close
else
super
end
end
end
Tutorial.new('localhost', 2000).show
Your loop: (simplified)
socket = #server.accept
loop do
Thread.start(socket) do |client|
# ...
end
end
accepts a single connection and then creates new threads as fast as it can, passing each thread the same socket object, so all of the threads start reading data from the same socket concurrently.
To avoid this, you have to move the accept call into the loop:
loop do
socket = #server.accept
Thread.start(socket) do |client|
# ...
end
end
Or simply:
loop do
Thread.start(#server.accept) do |client|
# ...
end
end
accept immediately blocks to wait for a new connection, effectively pausing the loop. Once a connection is established, it creates a new thread to handle that connection and the loop starts over.
A similar example can be found in the documentation for TCPServer.
Related
I am building a Ruby application which consists of the code responsible for the logic of the program and the one for GUI. Both parts of the code are split in classes and run in separate threads.
Ruby Gtk library is very poorly documented. I want to know how to update specific Gtk elements in real time (for instance, text in a label, which is in a window). I want to update a specific element every second.
I also want to find out how can threads exchange data. I have tried using Queue library. When I use it, I run into an error in the console:
undefined local variable or method `queue' for #<TimerWindow:0xa36d634 ptr=0xb4201178>
Program code:
require_relative 'notifications'
require_relative 'settings_reader'
require 'gtk3'
require "thread"
q = Queue.new
class TimerWindow < Gtk::Window
def initialize
#label = ""
super
init_ui
end
def init_ui
fixed = Gtk::Fixed.new
add fixed
button = Gtk::Button.new :label => "Quit"
button.set_size_request 80, 35
button.signal_connect "clicked" do
#label.set_text q.pop
end
fixed.put button, 50, 50
set_title "Tomatono"
signal_connect "destroy" do
Gtk.main_quit
end
set_border_width 10
#label = Gtk::Label.new "HEY"
fixed.put #label, 20, 20
set_default_size 250, 200
set_window_position :center
show_all
end
end
class Timer
def initialize
# Current time in seconds
#time = 0
settings = Settings_Reader.new
#work_time = Integer(settings.work_time) * 60
#break_time = Integer(settings.break_time) * 60
#work_text = settings.work_text
#return_text = settings.return_text
#break_text = settings.break_text
#work_notif_header = settings.work_notif_header
#break_notif_header = settings.break_notif_header
#status_notif_header = settings.status_notif_header
#work_status = settings.work_status
#break_status = settings.break_status
end
def launch
while true
work_time()
break_time()
end
end
def work_time()
puts #work_text
notification = Notif.new(#work_notif_header, #work_text)
notification.post
#time = 0
sleep(1)
while #time < #work_time
#time += 1
puts "#{min_remaining()} minutes remaining" if (#time % 60) == 0
if (#time % 60) == 0
notification = Notif.new(#work_notif_header, "#{#work_status} #{#time / 60} minutes.")
notification.post
end
q << #time
sleep(1)
end
end
def break_time
puts #break_text
#time = 0
sleep(1)
while #time < #break_time
#time += 1
puts "#{min_remaining()} minutes remaining" if (#time % 60) == 0
notification = Notif.new(#break_notif_header, "#{#break_status} #{#time / 60} minutes.")
notification.post
q << #time
sleep(1)
end
end
def reset
end
def stop_time
end
def min_remaining()
(1500 - #time) / 60
end
end
app = Thread.new {
timer = Timer.new
timer.launch
}
gui = Thread.new {
Gtk.init
window = TimerWindow.new
#window.update
Gtk.main
}
app.join
gui.join
Whenever I press the "Quit" button, I want the label text to change to the value set in the q variable, set in the Timer class (in the while loop). But it throws out an error that variable does not exist. Should it not be global?
No It is a local variable:
myglobal = "toto"
class Myclass
def initialize
#myvar = myglobal
end
def print
puts #myvar
end
end
an_instance = Myclass.new
an_instance.print
throw this error:
global_and_class.rb:5:in `initialize': undefined local variable or method `myglobal' for #<Myclass:0x00000000a74ce8> (NameError)
But it works if you specify myglobal as a global variable:
$myglobal = "toto"
class Myclass
def initialize
#myvar = $myglobal
end
def print
puts #myvar
end
end
an_instance = Myclass.new
an_instance.print
But you should be carefull with the use of global variable. Why not use the Queue instance as an argument for the initialize method ?
** Edit **
First of all here is a simple example that works with just a local variable:
#!/usr/bin/env ruby
require "gtk3"
label = Gtk::Label.new("test")
othert = Thread.new {
loop {
puts 'thread running';
label.text = Time.now.to_s; sleep 1 }
}
maint = Thread.new {
win = Gtk::Window.new
win.set_default_size 100, 30
win.add(label)
win.show_all
win.signal_connect("destroy") {othert.kill;Gtk.main_quit}
Gtk.main
}
maint.join
othert.join
Maybe you should start from this and see how to create you classes.
Edit 2
class TimerWindow < Gtk::Window
def initialize(label)
super()
add(label)
end
end
alabel = Gtk::Label.enw("test")
othert = Thread.new {
loop {
puts 'thread running';
label.text = Time.now.to_s; sleep 1 }
}
maint = Thread.new {
win = TimerWindow.new(alabel)
win.set_default_size 100, 30
win.show_all
win.signal_connect("destroy") {othert.kill;Gtk.main_quit}
Gtk.main
}
maint.join
othert.join
So witness and observe the following code, my questions is why do i never make it to the on_connect after starting the cool.io loop in send_to_server, the l.run should fire off the request as per the documented example on the github, and how the code handles incoming connections in module Server #socket.attach(l)
l.run
which does work and accepts the incoming data and sends it to my parser, which does work and fires off all the way up until the aforementioned send_to_server. So what is going on here?
require 'cool.io'
require 'http/parser'
require 'uri'
class Hash
def downcase_key
keys.each do |k|
store(k.downcase, Array === (v = delete(k)) ? v.map(&:downcase_key) : v)
end
self
end
end
module ShadyProxy
extend self
module ClientParserCallbacks
extend self
def on_message_complete(conn)
lambda do
puts "on_message_complete"
PluginHooks.before_request_to_server(conn)
end
end
def on_headers_complete(conn)
lambda do |headers|
conn.headers = headers
end
end
def on_body(conn)
lambda do |chunk|
conn.body << chunk
end
end
end
module PluginHooks
extend self
def before_request_to_server(conn)
# modify request here
conn.parser.headers.delete "Proxy-Connection"
conn.parser.headers.downcase_key
send_to_server(conn)
end
def send_to_server(conn)
parser = conn.parser
uri = URI::parse(parser.request_url)
l = Coolio::Loop.default
puts uri.scheme + "://" + uri.host
c = ShadyHttpClient.connect(uri.scheme + "://" + uri.host,uri.port).attach(l)
c.connection_reference = conn
c.request(parser.http_method,uri.request_uri)
l.run
end
def before_reply_to_client(conn)
end
end
class ShadyHttpClient < Coolio::HttpClient
def connection_reference=(conn)
puts "haz conneciton ref"
#connection_reference = conn
end
def connection_reference
#connection_reference
end
def on_connect
super
#never gets here
#headers = nil
#body = ''
#buffer = ''
end
def on_connect_failed
super
# never gets here either
end
def on_response_header(header)
#headers = header
end
def on_body_data(data)
puts "on data?"
#body << data
STDOUT.write data
end
def on_request_complete
puts "Headers"
puts #headers
puts "Body"
puts #body
end
def on_error(reason)
STDERR.puts "Error: #{reason}"
end
end
class ShadyProxyConnection < Cool.io::TCPSocket
attr_accessor :headers, :body, :buffer, :parser
def on_connect
#headers = nil
#body = ''
#buffer = ''
#parser = Http::Parser.new
#parser.on_message_complete = ClientParserCallbacks.on_message_complete(self)
#parser.on_headers_complete = ClientParserCallbacks.on_headers_complete(self)
#parser.on_body = ClientParserCallbacks.on_body(self)
end
def on_close
puts "huh?"
end
def on_read(data)
#buffer << data
#parser << data
end
end
module Server
def run(opts)
begin
# Start our server to handle connections (will raise things on errors)
l = Coolio::Loop.new
#socket = Cool.io::TCPServer.new(opts[:host],opts[:port], ShadyProxy::ShadyProxyConnection)
#socket.attach(l)
l.run
# Handle every request in another thread
loop do
Thread.new s = #socket.accept
end
# CTRL-C
rescue Interrupt
puts 'Got Interrupt..'
# Ensure that we release the socket on errors
ensure
if #socket
#socket.close
puts 'Socked closed..'
end
puts 'Quitting.'
end
end
module_function :run
end
end
ShadyProxy::Server.run(:host => '0.0.0.0',:port => 1234)
Okay, so I'm building a server to play the game of war as an assignment. My server needs to get input from two different sockets, and that's where I'm stuck. I've stored my sockets as keys and my players as values in a hash, but I can't get the sockets in my main program.
I can puts the hash in my main program, but I can't just get ONE socket so that I can tell my program to read from it. Here's my classes and the test I'm running:
require 'minitest/autorun'
require 'socket'
require_relative 'WarGame_Class.rb'
require_relative 'ModifiedPlayer_Class.rb'
require_relative 'DeckClass.rb'
class WarServer
def initialize(host, port)
#socket_server = TCPServer.new(host, port)
#players = [Player.new, Player.new]
#deck = CardDeck.new
#deck.deal_cards(#players[0].cards, #players[1].cards)
game = WarGame.new
#clients = {} # keys are sockets, values are players
end
def read_client_keys
#clients.key[0] #does not work
end
def close
#socket_server.close
end
def capture_input(player) ##input client to get what they wrote
#input = #clients.keys[0].read_nonblock(1000) # arbitrary max number of bytes
end
def accept_client
#Hash here to link client to player? (or game?)
client = #socket_server.accept
#clients[client] = #players[#clients.size]
# puts "clients key 0: #{#clients.keys[0]}"
puts
# puts "clients values: #{#clients.values}"
if #clients.size == 2
start_game#####################!!!! Starts game if two clients can put client messages in start game
end
end
def start_game ##############!!!
#clients.keys[0].puts "Welcome to War. Please press enter to play your card"
#clients.keys[1].puts "Welcome to War. Please press enter to play your card"
end
end
class MockWarClient
def initialize
#socket = TCPSocket.new('localhost', 2012)
end
def output
#output
end
def capture_output #need to add (socket)? How else read from specific socket?
#output = #socket.read_nonblock(1000) # arbitrary max number of bytes
rescue
#output = "capture_output error."
end
def capture_input
end
end
class WarServerTest < MiniTest::Unit::TestCase
def setup #This would be like our INITIALIZE Function
#anything is available through out all tests (i.e., instance vars)
#war_server = WarServer.new('localhost', 2012)
end
def teardown
#war_server.close
end
def test_server_capture_output_from_client
client_1 = MockWarClient.new
#war_server.accept_client
client_2 = MockWarClient.new
#war_server.accept_client
#can output #war_server.read_client_keys, though, if I take out the argument to pass in.
#puts "Test_Server_output #client keys #{#war_server.read_client_keys(player)}" #cient_1?
puts "Test_Server_output #client keys #{#war_server.read_client_keys}"
# I NEED TO JUST BE ABLE TO GET *ONE* KEY SO I CAN READ FROM THE SOCKET
#warserver.capture_input
refute(#war_server.input.empty)
end
end
You must use #keys[0] (or #keys.first) to select the first key of the Hash.
The function #key(val) does something different, returning the key corresponding to an occurrence of val in the hash.
http://www.ruby-doc.org/core-2.0.0/Hash.html#method-i-key-3F
def read_client_keys
#clients.keys[0] # this should work
end
I am attempting to write a chat server with EventMachine. How do I pass a message from one EventMachine connection, to another, in a thread-safe manner?
I see a messaging protocol (Stomp) being supported but I can't figure out how to use it. Any help is appreciated.
Stomp in EventMachine - http://eventmachine.rubyforge.org/EventMachine/Protocols/Stomp.html
See http://eventmachine.rubyforge.org/EventMachine/Channel.html
you can try something in these lines:
require 'eventmachine'
class Chat < EventMachine::Connection
def initialize channel
#channel = channel
end
def post_init
send_data 'Hello'
#sid = #channel.subscribe do |msg|
send_data msg
end
end
def receive_data(msg)
#channel.push msg
end
def unbind
#channel.unsubscribe #sid
end
end
EM.run do
#channel = EventMachine::Channel.new
EventMachine.start_server '127.0.0.1', 8081, Chat, #channel
end
EDIT: also check out https://github.com/eventmachine/eventmachine/tree/master/examples/guides/getting_started - there is a nice chatroom example
Try starting out with an in memory message dispatcher.
require 'thread'
class Room
def initialize
#users = []
end
def join(user)
#users << user
end
def leave(user)
#user.delete(user)
end
def broadcast(message)
#users.each do |user|
user.enqueue(message)
end
end
end
class User
def initialize
#mutex = Mutex.new
#queued_messages = []
end
def enqueue(message)
#mutex.synchronize do
#queued_message << message
end
end
def get_new_messages
#mutex.synchronize do
output = #queued_messages
#queued_messages = []
end
return output
end
end
UPDATE
ROOM = Room.new
class Connection
def user_logged_in
# #user = ...
ROOM.join(#user)
end
def received_message(message)
ROOM.broadcast(message)
end
def receive_send_more_messages_request(req)
messages = #user.get_new_messages
# write messages
end
def connection_closed
ROOM.leave(#user)
end
end
I couldn't find a decent ThreadPool implementation for Ruby, so I wrote mine (based partly on code from here: http://web.archive.org/web/20081204101031/http://snippets.dzone.com:80/posts/show/3276 , but changed to wait/signal and other implementation for ThreadPool shutdown. However after some time of running (having 100 threads and handling about 1300 tasks), it dies with deadlock on line 25 - it waits for a new job there. Any ideas, why it might happen?
require 'thread'
begin
require 'fastthread'
rescue LoadError
$stderr.puts "Using the ruby-core thread implementation"
end
class ThreadPool
class Worker
def initialize(callback)
#mutex = Mutex.new
#cv = ConditionVariable.new
#callback = callback
#mutex.synchronize {#running = true}
#thread = Thread.new do
while #mutex.synchronize {#running}
block = get_block
if block
block.call
reset_block
# Signal the ThreadPool that this worker is ready for another job
#callback.signal
else
# Wait for a new job
#mutex.synchronize {#cv.wait(#mutex)} # <=== Is this line 25?
end
end
end
end
def name
#thread.inspect
end
def get_block
#mutex.synchronize {#block}
end
def set_block(block)
#mutex.synchronize do
raise RuntimeError, "Thread already busy." if #block
#block = block
# Signal the thread in this class, that there's a job to be done
#cv.signal
end
end
def reset_block
#mutex.synchronize {#block = nil}
end
def busy?
#mutex.synchronize {!#block.nil?}
end
def stop
#mutex.synchronize {#running = false}
# Signal the thread not to wait for a new job
#cv.signal
#thread.join
end
end
attr_accessor :max_size
def initialize(max_size = 10)
#max_size = max_size
#workers = []
#mutex = Mutex.new
#cv = ConditionVariable.new
end
def size
#mutex.synchronize {#workers.size}
end
def busy?
#mutex.synchronize {#workers.any? {|w| w.busy?}}
end
def shutdown
#mutex.synchronize {#workers.each {|w| w.stop}}
end
alias :join :shutdown
def process(block=nil,&blk)
block = blk if block_given?
while true
#mutex.synchronize do
worker = get_worker
if worker
return worker.set_block(block)
else
# Wait for a free worker
#cv.wait(#mutex)
end
end
end
end
# Used by workers to report ready status
def signal
#cv.signal
end
private
def get_worker
free_worker || create_worker
end
def free_worker
#workers.each {|w| return w unless w.busy?}; nil
end
def create_worker
return nil if #workers.size >= #max_size
worker = Worker.new(self)
#workers << worker
worker
end
end
Ok, so the main problem with the implementation is: how to make sure no signal is lost and avoid dead locks ?
In my experience, this is REALLY hard to achieve with condition variables and mutex, but easy with semaphores. It so happens that ruby implement an object called Queue (or SizedQueue) that should solve the problem. Here is my suggested implementation:
require 'thread'
begin
require 'fasttread'
rescue LoadError
$stderr.puts "Using the ruby-core thread implementation"
end
class ThreadPool
class Worker
def initialize(thread_queue)
#mutex = Mutex.new
#cv = ConditionVariable.new
#queue = thread_queue
#running = true
#thread = Thread.new do
#mutex.synchronize do
while #running
#cv.wait(#mutex)
block = get_block
if block
#mutex.unlock
block.call
#mutex.lock
reset_block
end
#queue << self
end
end
end
end
def name
#thread.inspect
end
def get_block
#block
end
def set_block(block)
#mutex.synchronize do
raise RuntimeError, "Thread already busy." if #block
#block = block
# Signal the thread in this class, that there's a job to be done
#cv.signal
end
end
def reset_block
#block = nil
end
def busy?
#mutex.synchronize { !#block.nil? }
end
def stop
#mutex.synchronize do
#running = false
#cv.signal
end
#thread.join
end
end
attr_accessor :max_size
def initialize(max_size = 10)
#max_size = max_size
#queue = Queue.new
#workers = []
end
def size
#workers.size
end
def busy?
#queue.size < #workers.size
end
def shutdown
#workers.each { |w| w.stop }
#workers = []
end
alias :join :shutdown
def process(block=nil,&blk)
block = blk if block_given?
worker = get_worker
worker.set_block(block)
end
private
def get_worker
if !#queue.empty? or #workers.size == #max_size
return #queue.pop
else
worker = Worker.new(#queue)
#workers << worker
worker
end
end
end
And here is a simple test code:
tp = ThreadPool.new 500
(1..1000).each { |i| tp.process { (2..10).inject(1) { |memo,val| sleep(0.1); memo*val }; print "Computation #{i} done. Nb of tasks: #{tp.size}\n" } }
tp.shutdown
You can try the work_queue gem, designed to coordinate work between a producer and a pool of worker threads.
I'm slightly biased here, but I would suggest modelling this in some process language and model check it. Freely available tools are, for example, the mCRL2 toolset (using a ACP-based language), the Mobility Workbench (pi-calculus) and Spin (PROMELA).
Otherwise I would suggest removing every bit of code that is not essential to the problem and finding a minimal case where the deadlock occurs. I doubt that it the 100 threads and 1300 tasks are essential to get a deadlock. With a smaller case you can probably just add some debug prints which provide enough information the solve the problem.
Ok, the problem seems to be in your ThreadPool#signal method. What may happen is:
1 - All your worker are busy and you try to process a new job
2 - line 90 gets a nil worker
3 - a worker get freed and signals it, but the signal is lost as the ThreadPool is not waiting for it
4 - you fall on line 95, waiting even though there is a free worker.
The error here is that you can signal a free worker even when nobody is listening. This ThreadPool#signal method should be:
def signal
#mutex.synchronize { #cv.signal }
end
And the problem is the same in the Worker object. What might happen is:
1 - The Worker just completed a job
2 - It checks (line 17) if there is a job waiting: there isn't
3 - The thread pool send a new job and signals it ... but the signal is lost
4 - The worker wait for a signal, even though it is marked as busy
You should put your initialize method as:
def initialize(callback)
#mutex = Mutex.new
#cv = ConditionVariable.new
#callback = callback
#mutex.synchronize {#running = true}
#thread = Thread.new do
#mutex.synchronize do
while #running
block = get_block
if block
#mutex.unlock
block.call
#mutex.lock
reset_block
# Signal the ThreadPool that this worker is ready for another job
#callback.signal
else
# Wait for a new job
#cv.wait(#mutex)
end
end
end
end
end
Next, the Worker#get_block and Worker#reset_block methods should not be synchronized anymore. That way, you cannot have a block assigned to a worker between the test for a block and the wait for a signal.
Top commenter's code has helped out so much over the years. Here it is updated for ruby 2.x and improved with thread identification. How is that an improvement? When each thread has an ID, you can compose ThreadPool with an array which stores arbitrary information. Some ideas:
No array: typical ThreadPool usage. Even with the GIL it makes threading dead easy to code and very useful for high-latency applications like high-volume web crawling,
ThreadPool and Array sized to number of CPUs: easy to fork processes to use all CPUs,
ThreadPool and Array sized to number of resources: e.g., each array element represents one processor across a pool of instances, so if you have 10 instances each with 4 CPUs, the TP can manage work across 40 subprocesses.
With these last two, rather than thinking about threads doing work think about the ThreadPool managing subprocesses that are doing the work. The management task is lightweight and when combined with subprocesses, who cares about the GIL.
With this class, you can code up a cluster based MapReduce in about a hundred lines of code! This code is beautifully short although it can be a bit of a mind-bend to fully grok. Hope it helps.
# Usage:
#
# Thread.abort_on_exception = true # help localize errors while debugging
# pool = ThreadPool.new(thread_pool_size)
# 50.times {|i|
# pool.process { ... }
# or
# pool.process {|id| ... } # worker identifies itself as id
# }
# pool.shutdown()
class ThreadPool
require 'thread'
class ThreadPoolWorker
attr_accessor :id
def initialize(thread_queue, id)
#id = id # worker id is exposed thru tp.process {|id| ... }
#mutex = Mutex.new
#cv = ConditionVariable.new
#idle_queue = thread_queue
#running = true
#block = nil
#thread = Thread.new {
#mutex.synchronize {
while #running
#cv.wait(#mutex) # block until there is work to do
if #block
#mutex.unlock
begin
#block.call(#id)
ensure
#mutex.lock
end
#block = nil
end
#idle_queue << self
end
}
}
end
def set_block(block)
#mutex.synchronize {
raise RuntimeError, "Thread is busy." if #block
#block = block
#cv.signal # notify thread in this class, there is work to be done
}
end
def busy?
#mutex.synchronize { ! #block.nil? }
end
def stop
#mutex.synchronize {
#running = false
#cv.signal
}
#thread.join
end
def name
#thread.inspect
end
end
attr_accessor :max_size, :queue
def initialize(max_size = 10)
#process_mutex = Mutex.new
#max_size = max_size
#queue = Queue.new # of idle workers
#workers = [] # array to hold workers
# construct workers
#max_size.times {|i| #workers << ThreadPoolWorker.new(#queue, i) }
# queue up workers (workers in queue are idle and available to
# work). queue blocks if no workers are available.
#max_size.times {|i| #queue << #workers[i] }
sleep 1 # important to give threads a chance to initialize
end
def size
#workers.size
end
def idle
#queue.size
end
# are any threads idle
def busy?
# #queue.size < #workers.size
#queue.size == 0 && #workers.size == #max_size
end
# block until all threads finish
def shutdown
#workers.each {|w| w.stop }
#workers = []
end
alias :join :shutdown
def process(block = nil, &blk)
#process_mutex.synchronize {
block = blk if block_given?
worker = #queue.pop # assign to next worker; block until one is ready
worker.set_block(block) # give code block to worker and tell it to start
}
end
end