Problems with my first multithreaded Rupy programm

T

Tassilo Horn

Hi,

I'm trying to get into Ruby and today it's time for
multithreading. Therefore I wanted to write a program which does this:

In a tearoom there are three thirsty drinkers. To cook a tea one
needs water, tea and a cup. Every one of these three drinkers
has one of those items with him.
Every time the waiter is called he puts two items on the
table. Now one drinker has all the items he needs and cooks his
tea. After that he calls the waiter and the fun begins again...

Ok, here's what I wrote so far and what produces this output:

,----
| (%:~/tmp/Teadrinker)- ./TeaRoom.rb
| WAITER: Looking at the table!
| WAITER: Oh, nothing on the table!
| WAITER: Putting 1 and 2 on the table.
| deadlock 0x4023b798: sleep:J(0x4022603c) (main) - ./TeaRoom.rb:94
| deadlock 0x40225e70: sleep:- - /usr/lib/ruby/1.8/thread.rb:96
| deadlock 0x40225efc: sleep:- - /usr/lib/ruby/1.8/thread.rb:96
| deadlock 0x40225f88: sleep:- - /usr/lib/ruby/1.8/thread.rb:96
| deadlock 0x4022603c: sleep:- - ./TeaRoom.rb:31
| ./TeaRoom.rb:31: Thread(0x4022603c): deadlock (fatal)
`----

<----- CODE ----->

#! /usr/bin/env ruby

require 'thread'

# For synchronisation
mutex = Mutex.new
cv = ConditionVariable.new

# items on the table:
# 0: cup
# 1: water
# 2: tea
table = []

waiter = Thread.new {
while true
mutex.synchronize {
puts "WAITER: Looking at the table!"
if table.length == 0
puts "WAITER: Oh, nothing on the table!"
sleep 2
while table[0] == table[1] do
table = [rand(3), rand(3)]
end
$stdout << "WAITER: Putting " << table[0] << " and " << table[1]
<< " on the table.\n"
else
puts "WAITER: Why did you call me???"
end
# Call the waiting tea drinkers
cv.broadcast
Thread.stop
}
end
}

# Mike has a cup (0)
mike = Thread.new {
while true
mutex.synchronize {
cv.wait(mutex)
if table.include?(1) and table.include?(2)
puts "MIKE: Water and tea on table. I'll cook my tea."
table = []
sleep 3
puts "MIKE: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter.wakeup
sleep 3
puts "MIKE: Now I will read my newspaper."
else
puts "MIKE: Not what I need."
end
}
end
}

# Al has water (1)
al = Thread.new {
while true
mutex.synchronize {
cv.wait(mutex)
if table.include?(0) and table.include?(2)
puts "AL: Cup and tea on table. I'll cook my tea."
table = []
sleep 3
puts "AL: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter.wakeup
sleep 3
puts "AL: Now I will read my newspaper."
else
puts "AL: Not what I need."
end
}
end
}

# Ed has tea (2)
ed = Thread.new {
while true
mutex.synchronize {
cv.wait(mutex)
if table.include?(0) and table.include?(1)
puts "ED: Cup and water on table. I'll cook my tea."
table = []
sleep 3
puts "ED: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter.wakeup
sleep 3
puts "ED: Now I will read my newspaper."
else
puts "ED: Not what I need."
end
}
end
}

waiter.join

<----- END OF CODE ----->

What's wrong with these lines?

Much thanks in advance,
Tassilo
 
M

Markus

Try taking the cv.broadcast and the Thread.stop out of the mutex
syncronize block (e.g., let go before you tell the others to start).

-- Markus


Hi,

I'm trying to get into Ruby and today it's time for
multithreading. Therefore I wanted to write a program which does this:

In a tearoom there are three thirsty drinkers. To cook a tea one
needs water, tea and a cup. Every one of these three drinkers
has one of those items with him.
Every time the waiter is called he puts two items on the
table. Now one drinker has all the items he needs and cooks his
tea. After that he calls the waiter and the fun begins again...

Ok, here's what I wrote so far and what produces this output:

,----
| (%:~/tmp/Teadrinker)- ./TeaRoom.rb
| WAITER: Looking at the table!
| WAITER: Oh, nothing on the table!
| WAITER: Putting 1 and 2 on the table.
| deadlock 0x4023b798: sleep:J(0x4022603c) (main) - ./TeaRoom.rb:94
| deadlock 0x40225e70: sleep:- - /usr/lib/ruby/1.8/thread.rb:96
| deadlock 0x40225efc: sleep:- - /usr/lib/ruby/1.8/thread.rb:96
| deadlock 0x40225f88: sleep:- - /usr/lib/ruby/1.8/thread.rb:96
| deadlock 0x4022603c: sleep:- - ./TeaRoom.rb:31
| ./TeaRoom.rb:31: Thread(0x4022603c): deadlock (fatal)
`----

<----- CODE ----->

#! /usr/bin/env ruby

require 'thread'

# For synchronisation
mutex = Mutex.new
cv = ConditionVariable.new

# items on the table:
# 0: cup
# 1: water
# 2: tea
table = []

waiter = Thread.new {
while true
mutex.synchronize {
puts "WAITER: Looking at the table!"
if table.length == 0
puts "WAITER: Oh, nothing on the table!"
sleep 2
while table[0] == table[1] do
table = [rand(3), rand(3)]
end
$stdout << "WAITER: Putting " << table[0] << " and " << table[1]
<< " on the table.\n"
else
puts "WAITER: Why did you call me???"
end
# Call the waiting tea drinkers
cv.broadcast
Thread.stop
}
end
}

# Mike has a cup (0)
mike = Thread.new {
while true
mutex.synchronize {
cv.wait(mutex)
if table.include?(1) and table.include?(2)
puts "MIKE: Water and tea on table. I'll cook my tea."
table = []
sleep 3
puts "MIKE: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter.wakeup
sleep 3
puts "MIKE: Now I will read my newspaper."
else
puts "MIKE: Not what I need."
end
}
end
}

# Al has water (1)
al = Thread.new {
while true
mutex.synchronize {
cv.wait(mutex)
if table.include?(0) and table.include?(2)
puts "AL: Cup and tea on table. I'll cook my tea."
table = []
sleep 3
puts "AL: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter.wakeup
sleep 3
puts "AL: Now I will read my newspaper."
else
puts "AL: Not what I need."
end
}
end
}

# Ed has tea (2)
ed = Thread.new {
while true
mutex.synchronize {
cv.wait(mutex)
if table.include?(0) and table.include?(1)
puts "ED: Cup and water on table. I'll cook my tea."
table = []
sleep 3
puts "ED: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter.wakeup
sleep 3
puts "ED: Now I will read my newspaper."
else
puts "ED: Not what I need."
end
}
end
}

waiter.join

<----- END OF CODE ----->

What's wrong with these lines?

Much thanks in advance,
Tassilo
 
S

SER

A couple of things.

1) Create the Waiter last. He's got the lock on the mutex, so none of
the other threads can enter their synchronized sections, so they can't
wait. They're simply blocking while waiting to enter the protected
section. It doesn't matter that the waiter is sleep()ing, because he
still has the lock. If you create the waiter last, then everybody else
has a chance to enter a synchronized section and wait (releasing the
lock) before the waiter does his thing.

2) Don't use Thread.stop. It isn't doing what I think you think it is
doing.

3) Create another CV just for the waiter, so you can send signals
directly to him

4) Wrap your "whiles" in the synchronized sections, not the other way
around. For one thing, entering synchronized sections are costly; you
want to do it as rarely as you can. For another, when you wait(), you
release the lock, allowing others to get at the lock, so it is OK to
put the while inside the synchronized section.

5) If I was going to clean up the code for you, I'd recommend doing
something like this:

Here's a version that works:

<code lang='ruby'>
#!/usr/bin/env ruby

require 'thread'


# For synchronisation
mutex = Mutex.new
cv = ConditionVariable.new
waiter_cv = ConditionVariable.new


# items on the table:
# 0: cup
# 1: water
# 2: tea
table = []

# Mike has a cup (0)
mike = Thread.new {
mutex.synchronize {
while true
cv.wait(mutex)
if table.include?(1) and table.include?(2)
puts "MIKE: Water and tea on table. I'll cook my tea."
table = []
#sleep 3
puts "MIKE: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter_cv.signal
#sleep 3
puts "MIKE: Now I will read my newspaper."
else
puts "MIKE: Not what I need."
end
end
}

}

# Al has water (1)
al = Thread.new {
mutex.synchronize {
while true
cv.wait(mutex)
if table.include?(0) and table.include?(2)
puts "AL: Cup and tea on table. I'll cook my tea."
table = []
#sleep 3
puts "AL: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter_cv.signal
#sleep 3
puts "AL: Now I will read my newspaper."
else
puts "AL: Not what I need."
end
end
}
}

# Ed has tea (2)
ed = Thread.new {
mutex.synchronize {
while true
cv.wait(mutex)
if table.include?(0) and table.include?(1)
puts "ED: Cup and water on table. I'll cook my tea."
table = []
#sleep 3
puts "ED: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter_cv.signal
#sleep 3
puts "ED: Now I will read my newspaper."
else
puts "ED: Not what I need."
end
end
}
}

waiter = Thread.new {
mutex.synchronize {
while true
puts "#"*80
puts "WAITER: Looking at the table!"
if table.length == 0
puts "WAITER: Oh, nothing on the table!"
#sleep 2
while table[0] == table[1] do
table = [rand(3), rand(3)]
end
$stdout << "WAITER: Putting " << table[0] << " and " <<
table[1] << " on the table.\n"
else
puts "WAITER: Why did you call me???"
end
# Call the waiting tea drinkers
puts "WAITER: Broadcast"
cv.broadcast
puts "WAITER: Wait"
waiter_cv.wait(mutex)
end
}
}

waiter.join
</code>


And here's a cleaner, shorter version:

<code lang='ruby'>
#!/usr/bin/env ruby

require 'thread'

# For synchronisation
mutex = Mutex.new
cv = ConditionVariable.new
waiter_cv = ConditionVariable.new


# items on the table:
# 0: cup
# 1: water
# 2: tea
table = []

items = %w{ Cup Water Tea }
[ [:Mike,1,2], [:Al,0,2], [:Ed,0,1] ].each do |guy, *needs|
Thread.new do
puts "Doing #{guy}"
mutex.synchronize do
while true
cv.wait(mutex)
if (table & needs).size == 2
puts "#{guy}: #{items[table[0]]} and #{items[table[1]]} on
table. "+
"I'll cook my tea."
table = []
puts "#{guy}: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter_cv.signal
puts "#{guy}: Now I will read my newspaper."
else
puts "#{guy}: Not what I need."
end
end
end
end
end

waiter = Thread.new do
mutex.synchronize do
while true
puts "#"*80
puts "WAITER: Looking at the table!"
if table.length == 0
puts "WAITER: Oh, nothing on the table!"
while table[0] == table[1] do
table = [rand(3), rand(3)]
end
puts "WAITER: Putting #{items[table[0]]} and #{items[table[1]]}
on the table."
else
puts "WAITER: Why did you call me???"
end
# Call the waiting tea drinkers
puts "WAITER: Broadcast"
cv.broadcast
puts "WAITER: Wait"
waiter_cv.wait(mutex)
end
end
end

waiter.join
</code>

--- SER
 
S

SER

A couple of things.

1) Create the Waiter last. He's got the lock on the mutex, so none of
the other threads can enter their synchronized sections, so they can't
wait. They're simply blocking while waiting to enter the protected
section. It doesn't matter that the waiter is sleep()ing, because he
still has the lock. If you create the waiter last, then everybody else
has a chance to enter a synchronized section and wait (releasing the
lock) before the waiter does his thing.

2) Don't use Thread.stop. It isn't doing what I think you think it is
doing.

3) Create another CV just for the waiter, so you can send signals
directly to him

4) Wrap your "whiles" in the synchronized sections, not the other way
around. For one thing, entering synchronized sections are costly; you
want to do it as rarely as you can. For another, when you wait(), you
release the lock, allowing others to get at the lock, so it is OK to
put the while inside the synchronized section.

5) If I was going to clean up the code for you, I'd recommend doing
something like this:

Here's a version that works:

<code lang='ruby'>
#!/usr/bin/env ruby

require 'thread'


# For synchronisation
mutex = Mutex.new
cv = ConditionVariable.new
waiter_cv = ConditionVariable.new


# items on the table:
# 0: cup
# 1: water
# 2: tea
table = []

# Mike has a cup (0)
mike = Thread.new {
mutex.synchronize {
while true
cv.wait(mutex)
if table.include?(1) and table.include?(2)
puts "MIKE: Water and tea on table. I'll cook my tea."
table = []
#sleep 3
puts "MIKE: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter_cv.signal
#sleep 3
puts "MIKE: Now I will read my newspaper."
else
puts "MIKE: Not what I need."
end
end
}

}

# Al has water (1)
al = Thread.new {
mutex.synchronize {
while true
cv.wait(mutex)
if table.include?(0) and table.include?(2)
puts "AL: Cup and tea on table. I'll cook my tea."
table = []
#sleep 3
puts "AL: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter_cv.signal
#sleep 3
puts "AL: Now I will read my newspaper."
else
puts "AL: Not what I need."
end
end
}
}

# Ed has tea (2)
ed = Thread.new {
mutex.synchronize {
while true
cv.wait(mutex)
if table.include?(0) and table.include?(1)
puts "ED: Cup and water on table. I'll cook my tea."
table = []
#sleep 3
puts "ED: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter_cv.signal
#sleep 3
puts "ED: Now I will read my newspaper."
else
puts "ED: Not what I need."
end
end
}
}

waiter = Thread.new {
mutex.synchronize {
while true
puts "#"*80
puts "WAITER: Looking at the table!"
if table.length == 0
puts "WAITER: Oh, nothing on the table!"
#sleep 2
while table[0] == table[1] do
table = [rand(3), rand(3)]
end
$stdout << "WAITER: Putting " << table[0] << " and " <<
table[1] << " on the table.\n"
else
puts "WAITER: Why did you call me???"
end
# Call the waiting tea drinkers
puts "WAITER: Broadcast"
cv.broadcast
puts "WAITER: Wait"
waiter_cv.wait(mutex)
end
}
}

waiter.join
</code>


And here's a cleaner, shorter version:

<code lang='ruby'>
#!/usr/bin/env ruby

require 'thread'

# For synchronisation
mutex = Mutex.new
cv = ConditionVariable.new
waiter_cv = ConditionVariable.new


# items on the table:
# 0: cup
# 1: water
# 2: tea
table = []

items = %w{ Cup Water Tea }
[ [:Mike,1,2], [:Al,0,2], [:Ed,0,1] ].each do |guy, *needs|
Thread.new do
puts "Doing #{guy}"
mutex.synchronize do
while true
cv.wait(mutex)
if (table & needs).size == 2
puts "#{guy}: #{items[table[0]]} and #{items[table[1]]} on
table. "+
"I'll cook my tea."
table = []
puts "#{guy}: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter_cv.signal
puts "#{guy}: Now I will read my newspaper."
else
puts "#{guy}: Not what I need."
end
end
end
end
end

waiter = Thread.new do
mutex.synchronize do
while true
puts "#"*80
puts "WAITER: Looking at the table!"
if table.length == 0
puts "WAITER: Oh, nothing on the table!"
while table[0] == table[1] do
table = [rand(3), rand(3)]
end
puts "WAITER: Putting #{items[table[0]]} and #{items[table[1]]}
on the table."
else
puts "WAITER: Why did you call me???"
end
# Call the waiting tea drinkers
puts "WAITER: Broadcast"
cv.broadcast
puts "WAITER: Wait"
waiter_cv.wait(mutex)
end
end
end

waiter.join
</code>

--- SER
 
T

Tassilo Horn

Markus said:
Try taking the cv.broadcast and the Thread.stop out of the mutex
syncronize block (e.g., let go before you tell the others to start).

But then the waiter puts two item on the table but no drinker takes them
away.

The output is then:

,----
| WAITER: Looking at the table!
| WAITER: Oh, nothing on the table!
| WAITER: Putting 0 and 1 on the table.
| WAITER: Looking at the table!
| WAITER: Why did you call me???
| WAITER: Looking at the table!
| WAITER: Why did you call me???
| ...
`----

But it should work like this:

The waiter puts two items on the table and falls asleep.
The drinker who needs those two items wakes up, consumes them,
drinks his tea and wakes up the waiter.
The waiter puts two new items on the table.
The drinker who needs those two items wakes up, ...

I hope I didn't understand you wrong.

Regards,
Tassilo
 
S

SER

A couple of things.

1) Create the Waiter last. He's got the lock on the mutex, so none of
the other threads can enter their synchronized sections, so they can't
wait. They're simply blocking while waiting to enter the protected
section. It doesn't matter that the waiter is sleep()ing, because he
still has the lock. If you create the waiter last, then everybody else
has a chance to enter a synchronized section and wait (releasing the
lock) before the waiter does his thing.

2) Don't use Thread.stop. It isn't doing what I think you think it is
doing.

3) Create another CV just for the waiter, so you can send signals
directly to him

4) Wrap your "whiles" in the synchronized sections, not the other way
around. For one thing, entering synchronized sections are costly; you
want to do it as rarely as you can. For another, when you wait(), you
release the lock, allowing others to get at the lock, so it is OK to
put the while inside the synchronized section.

5) If I was going to clean up the code for you, I'd recommend doing
something like this:

Here's a version that works:

<code lang='ruby'>
#!/usr/bin/env ruby

require 'thread'

# For synchronisation
mutex = Mutex.new
cv = ConditionVariable.new
waiter_cv = ConditionVariable.new

# items on the table:
# 0: cup
# 1: water
# 2: tea
table = []

# Mike has a cup (0)
mike = Thread.new {
mutex.synchronize {
while true
cv.wait(mutex)
if table.include?(1) and table.include?(2)
puts "MIKE: Water and tea on table. I'll cook my tea."
table = []
#sleep 3
puts "MIKE: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter_cv.signal
#sleep 3
puts "MIKE: Now I will read my newspaper."
else
puts "MIKE: Not what I need."
end
end
}
}

# Al has water (1)
al = Thread.new {
mutex.synchronize {
while true
cv.wait(mutex)
if table.include?(0) and table.include?(2)
puts "AL: Cup and tea on table. I'll cook my tea."
table = []
#sleep 3
puts "AL: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter_cv.signal
#sleep 3
puts "AL: Now I will read my newspaper."
else
puts "AL: Not what I need."
end
end
}
}

# Ed has tea (2)
ed = Thread.new {
mutex.synchronize {
while true
cv.wait(mutex)
if table.include?(0) and table.include?(1)
puts "ED: Cup and water on table. I'll cook my tea."
table = []
#sleep 3
puts "ED: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter_cv.signal
#sleep 3
puts "ED: Now I will read my newspaper."
else
puts "ED: Not what I need."
end
end
}
}

waiter = Thread.new {
mutex.synchronize {
while true
puts "#"*80
puts "WAITER: Looking at the table!"
if table.length == 0
puts "WAITER: Oh, nothing on the table!"
#sleep 2
while table[0] == table[1] do
table = [rand(3), rand(3)]
end
$stdout << "WAITER: Putting " << table[0] << " and " <<
table[1] << " on the table.\n"
else
puts "WAITER: Why did you call me???"
end
# Call the waiting tea drinkers
puts "WAITER: Broadcast"
cv.broadcast
puts "WAITER: Wait"
waiter_cv.wait(mutex)
end
}
}

waiter.join
</code>


And here's a cleaner, shorter version:

<code lang='ruby'>
#!/usr/bin/env ruby

require 'thread'

# For synchronisation
mutex = Mutex.new
cv = ConditionVariable.new
waiter_cv = ConditionVariable.new


# items on the table:
# 0: cup
# 1: water
# 2: tea
table = []

items = %w{ Cup Water Tea }
[ [:Mike,1,2], [:Al,0,2], [:Ed,0,1] ].each do |guy, *needs|
Thread.new do
puts "Doing #{guy}"
mutex.synchronize do
while true
cv.wait(mutex)
if (table & needs).size == 2
puts "#{guy}: #{items[table[0]]} and #{items[table[1]]} on
table. "+
"I'll cook my tea."
table = []
puts "#{guy}: Drinking my tea. Calling the waiter."
# Wake up the waiter-Thread
waiter_cv.signal
puts "#{guy}: Now I will read my newspaper."
else
puts "#{guy}: Not what I need."
end
end
end
end
end

waiter = Thread.new do
mutex.synchronize do
while true
puts "#"*80
puts "WAITER: Looking at the table!"
if table.length == 0
puts "WAITER: Oh, nothing on the table!"
while table[0] == table[1] do
table = [rand(3), rand(3)]
end
puts "WAITER: Putting #{items[table[0]]} and #{items[table[1]]}
on the table."
else
puts "WAITER: Why did you call me???"
end
# Call the waiting tea drinkers
puts "WAITER: Broadcast"
cv.broadcast
puts "WAITER: Wait"
waiter_cv.wait(mutex)
end
end
end

waiter.join
</code>

--- SER
 
S

SER

Wow. Google groups really screws up the formatting of code. Sorry
about that. I hope you have a good editor that'll re-indent code for
you.

--- SER
 
T

Tassilo Horn

SER said:
A couple of things.

Perfect. I'm willing to learn.
1) Create the Waiter last. He's got the lock on the mutex, so none of
the other threads can enter their synchronized sections, so they can't
wait. They're simply blocking while waiting to enter the protected
section. It doesn't matter that the waiter is sleep()ing, because he
still has the lock. If you create the waiter last, then everybody else
has a chance to enter a synchronized section and wait (releasing the
lock) before the waiter does his thing.

Ah, I understand.
2) Don't use Thread.stop. It isn't doing what I think you think it is
doing.

I was quite sure it's not doing what I wanted it to do but had no better
idea how to do what I want it to do. ;-)
3) Create another CV just for the waiter, so you can send signals
directly to him

And that is the solution for 2) which I didn't realize.
4) Wrap your "whiles" in the synchronized sections, not the other way
around. For one thing, entering synchronized sections are costly; you
want to do it as rarely as you can. For another, when you wait(), you
release the lock, allowing others to get at the lock, so it is OK to
put the while inside the synchronized section.
Ok.

5) If I was going to clean up the code for you, I'd recommend doing
something like this:

Oh, thanks. I hope I'll be disciplinated enough to first implementing
your tips on my own before reading you code...

Much thanks for your great help. But _one_ posting would have been
enough! ;-)

Regards,
Tassilo
 
M

Markus

But then the waiter puts two item on the table but no drinker takes them
away.

I hope I didn't understand you wrong.

I fear you might have; I didn't mean to eliminate them, simply to
move them down to right after the '}' instead of right before it. It
seemed to work for me (I just tried it).

-- Markus
 
T

Tassilo Horn

Markus said:
I fear you might have; I didn't mean to eliminate them, simply to
move them down to right after the '}' instead of right before it. It
seemed to work for me (I just tried it).

Ah, that's ok. But there were other things skewed up in my code. I
refactored my code obeying SER's tips and now it works like a charm.

But thanks for your fast reply.

Regards,
Tassilo
 
S

SER

Much thanks for your great help. But _one_ posting would have been
enough! ;-)

Google Groups (beta). I try to not complain too much, since it is a
free service, but it is the only way I have of reading usenet from
behind a firewall, and in addition to really munging posts, it often
returns server errors while trying to post. Since the errors are
always varied and new, when they look really bad, I resubmit the post.
That's what happened here.

Google Groups (nonbeta) is even worse, in that posting can take six
hours to propegate to usenet.

Sorry about that.

--- SER
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,744
Messages
2,569,483
Members
44,901
Latest member
Noble71S45

Latest Threads

Top