Ruby and Tk question

Hello everyone,

I've been trying to write a trivial multi-threaded application that has
a GUI (implemented using TkRuby) and which updates various counters on
the GUI. Problem is it deadlocks as soon as I try to update the
graphical part, but I am unsure why. I've included the most stripped
down version of the code here - which will not deadlock - it will just
output to stdout. If you comment in the two obvious lines (commented to
indicate which ones) then it does deadlock.

Am I doing something wrong?
Have I made incorrect assumptions?
How should I do this?
Is it possible to update Tk components from threads other than the main
GUI thread?

Cheers

Stephen

require 'tk' # get widget classes
require 'thread'

class RTVExample < TkRoot
  def initialize(args)
    super(args)

    @countA = 0
    @countB = 0
    @lock1 = Mutex.new

    # create our UI

    @menubar = TkMenubar.new(nil, nil)
    @menubar.pack('side'=>'top', 'fill'=>'x')

    @menubar.add_menu([['File', 0],
                       ['Exit', proc{exitFunc}, 0]])

    @menubar.add_menu([['Thread Test', 0],
                       ['Test 1', proc{testThread1}, 0]])

    @counterLabel = TkLabel.new{text 'Counter' ; pack {padx=30 ; pady=0}}
  end

  # why does this deadlock? - surely it should just run the two threads alongside the GUI thread.

  def testThread1()
    # create two the threads - comment in the #@counter lines to see the deadlock

    tok1 = Thread.new do
      10000.times {
                    @lock1.synchronize { @countA += 1 }
                    puts "A:" + @countA.to_s()

                    # this next line causes the deadlock - why?
                    #@counterLabel.configure(text=>"Counter A: %d" % @countA)
            }
    end
    tok2 = Thread.new do
      10000.times {
                    @lock1.synchronize { @countB += 1 }
                    puts "B:" + @countB.to_s()

                    # this next line causes the deadlock - why?
                    #@counterLabel.configure(text=>"Counter B: %d" % @countB)
            }
          end

    tok1.join
    tok2.join
  end

  def exitFunc()
    TkRoot.destroy()
  end
end

# run the GUI app

gui = RTVExample.new()
Tk.mainloop()

···

--
Stephen Kellett
Object Media Limited http://www.objmedia.demon.co.uk/software.html
Computer Consultancy, Software Development
Windows C++, Java, Assembler, Performance Analysis, Troubleshooting

Deadlocks are usually to do with the OS. Maybe Tk can't cope with
threads. Have you tried this in other Graphical Toolkits, WxRuby is
quite nice and GTK seems quite good.

···

On 29/09/05, Stephen Kellett <snail@objmedia.demon.co.uk> wrote:

Hello everyone,

I've been trying to write a trivial multi-threaded application that has
a GUI (implemented using TkRuby) and which updates various counters on
the GUI. Problem is it deadlocks as soon as I try to update the
graphical part, but I am unsure why. I've included the most stripped
down version of the code here - which will not deadlock - it will just
output to stdout. If you comment in the two obvious lines (commented to
indicate which ones) then it does deadlock.

Am I doing something wrong?
Have I made incorrect assumptions?
How should I do this?
Is it possible to update Tk components from threads other than the main
GUI thread?

Cheers

Stephen

require 'tk' # get widget classes
require 'thread'

class RTVExample < TkRoot
  def initialize(args)
    super(args)

    @countA = 0
    @countB = 0
    @lock1 = Mutex.new

    # create our UI

    @menubar = TkMenubar.new(nil, nil)
    @menubar.pack('side'=>'top', 'fill'=>'x')

    @menubar.add_menu([['File', 0],
                       ['Exit', proc{exitFunc}, 0]])

    @menubar.add_menu([['Thread Test', 0],
                       ['Test 1', proc{testThread1}, 0]])

    @counterLabel = TkLabel.new{text 'Counter' ; pack {padx=30 ; pady=0}}
  end

  # why does this deadlock? - surely it should just run the two threads alongside the GUI thread.

  def testThread1()
    # create two the threads - comment in the #@counter lines to see the deadlock

    tok1 = Thread.new do
      10000.times {
                    @lock1.synchronize { @countA += 1 }
                    puts "A:" + @countA.to_s()

                    # this next line causes the deadlock - why?
                    #@counterLabel.configure(text=>"Counter A: %d" % @countA)
            }
    end
    tok2 = Thread.new do
      10000.times {
                    @lock1.synchronize { @countB += 1 }
                    puts "B:" + @countB.to_s()

                    # this next line causes the deadlock - why?
                    #@counterLabel.configure(text=>"Counter B: %d" % @countB)
            }
          end

    tok1.join
    tok2.join
  end

  def exitFunc()
    TkRoot.destroy()
  end
end

# run the GUI app

gui = RTVExample.new()
Tk.mainloop()

--
Stephen Kellett
Object Media Limited http://www.objmedia.demon.co.uk/software.html
Computer Consultancy, Software Development
Windows C++, Java, Assembler, Performance Analysis, Troubleshooting

Message-ID: <ZQx403EhK8ODFwF+@objmedia.demon.co.uk>

I've been trying to write a trivial multi-threaded application that has
a GUI (implemented using TkRuby) and which updates various counters on
the GUI. Problem is it deadlocks as soon as I try to update the
graphical part, but I am unsure why. I've included the most stripped
down version of the code here - which will not deadlock - it will just
output to stdout. If you comment in the two obvious lines (commented to
indicate which ones) then it does deadlock.

Am I doing something wrong?
Have I made incorrect assumptions?
How should I do this?
Is it possible to update Tk components from threads other than the main
GUI thread?

On Ruby/Tk, all operations and callbacks are evaluated on the
eventloop thread. All calls are serialized to avoid conflict of
GUI operation. Therefore, never wait or sleep on the callback
operation!!

Your callback "testThread1" waits two threads.
That is not so serious problem except broken respense of GUI.
However, each of the threads which "testThread1" waits calls
a Tk operation "@counterLabel.configure".
The Tk operation sends a request to the eventloop and waits
for the return value from the eventloop.
It makes deadlock!.

If "testThread1" doen't the created threads, there is no problem
except your typo :wink:

    #@counterLabel.configure(text=>"Counter A: %d" % @countA)

                               ^^^^

    #@counterLabel.configure(text=>"Counter B: %d" % @countB)

                               ^^^^
                           :text or "text"

However, I think, your request is "disable the menu entry between
callback operations are working".
If that is true, Please read the rewrited example at the end of
this mail.

# BTW, TkTimer class may help you sometimes. For example,
# -------------------------------------------------------------
# TkTimer.new(0, 1000){ <operations of each call> }.start
# -------------------------------------------------------------
# This calls the block 1000 times with 0ms (or minimum) interval.
# TkTimer works on the eventloop. So, there is no thread-switching.

---------< example script >--------------------------------------
require 'tk' # get widget classes
require 'thread'

class RTVExample < TkRoot
  def initialize(args)
    super(args)

    @countA = 0
    @countB = 0
    @lock1 = Mutex.new

    # create our UI

    @menubar = TkMenubar.new(nil, nil)
    @menubar.pack('side'=>'top', 'fill'=>'x')

    @menubar.add_menu([['File', 0],
                       ['Exit', proc{exitFunc}, 0]])

    @th_test_mbtn, @th_test_menu =
      @menubar.add_menu([['Thread Test', 0],
                          ['Test 1', proc{testThread1}, 0]])

    @counterLabel = TkLabel.new{text 'Counter' ; pack {padx=30 ; pady=0}}
  end

  # why does this deadlock? - surely it should just run the two threads alongside the GUI thread.

  def testThread1()
    # create two the threads - comment in the #@counter lines to see the deadlock
    Thread.new do
      begin
        @th_test_menu.entryconfigure('Test 1', :state=>:disabled)

        tok1 = Thread.new do
          10000.times {
            @lock1.synchronize { @countA += 1 }
            puts "A:" + @countA.to_s()
            
            # this next line causes the deadlock - why?
            @counterLabel.configure(:text=>"Counter A: %d" % @countA)
            Tk.update_idletasks
          }
        end
        tok2 = Thread.new do
          10000.times {
            @lock1.synchronize { @countB += 1 }
            puts "B:" + @countB.to_s()

            # this next line causes the deadlock - why?
            @counterLabel.configure(:text=>"Counter B: %d" % @countB)
            Tk.update_idletasks
          }
        end

        begin
          tok1.join
        rescue
          # ignore error on Thread
        end
        begin
          tok2.join
        rescue
          # ignore error on Thread
        end
      ensure
        @th_test_menu.entryconfigure('Test 1', :state=>:normal)
      end
    end
  end

  def exitFunc()
    TkRoot.destroy()
  end
end

# run the GUI app

gui = RTVExample.new()
Tk.mainloop()

···

From: Stephen Kellett <snail@objmedia.demon.co.uk>
Subject: Ruby and Tk question
Date: Thu, 29 Sep 2005 19:41:45 +0900
-----------------------------------------------------------------
--
Hidetoshi NAGAI (nagai@ai.kyutech.ac.jp)

Cheers

Stephen

require 'tk' # get widget classes
require 'thread'

class RTVExample < TkRoot
  def initialize(args)
    super(args)

    @countA = 0
    @countB = 0
    @lock1 = Mutex.new

    # create our UI

    @menubar = TkMenubar.new(nil, nil)
    @menubar.pack('side'=>'top', 'fill'=>'x')

    @menubar.add_menu([['File', 0],
                       ['Exit', proc{exitFunc}, 0]])

    @menubar.add_menu([['Thread Test', 0],
                       ['Test 1', proc{testThread1}, 0]])

    @counterLabel = TkLabel.new{text 'Counter' ; pack {padx=30 ; pady=0}}
  end

  # why does this deadlock? - surely it should just run the two threads alongside the GUI thread.

  def testThread1()
    # create two the threads - comment in the #@counter lines to see the deadlock

    tok1 = Thread.new do
      10000.times {
                    @lock1.synchronize { @countA += 1 }
                    puts "A:" + @countA.to_s()

                    # this next line causes the deadlock - why?
                    #@counterLabel.configure(text=>"Counter A: %d" % @countA)
            }
    end
    tok2 = Thread.new do
      10000.times {
                    @lock1.synchronize { @countB += 1 }
                    puts "B:" + @countB.to_s()

                    # this next line causes the deadlock - why?
                    #@counterLabel.configure(text=>"Counter B: %d" % @countB)
            }
          end

    tok1.join
    tok2.join
  end

  def exitFunc()
    TkRoot.destroy()
  end
end

# run the GUI app

gui = RTVExample.new()
Tk.mainloop()

--
Stephen Kellett
Object Media Limited http://www.objmedia.demon.co.uk/software.html
Computer Consultancy, Software Development
Windows C++, Java, Assembler, Performance Analysis, Troubleshooting

I haven't tried other GUI toolkits (I wasn't particularly aware of them - the PickAxe book talks about Tk). The equivalent code with Python and Tk runs just fine.

Stephen

···

In message <9b84274f050929034642084d93@mail.gmail.com>, Daniel Lewis <danieljohnlewis@gmail.com> writes

Deadlocks are usually to do with the OS. Maybe Tk can't cope with
threads. Have you tried this in other Graphical Toolkits, WxRuby is
quite nice and GTK seems quite good.

--
Stephen Kellett
Object Media Limited http://www.objmedia.demon.co.uk/software.html
Computer Consultancy, Software Development
Windows C++, Java, Assembler, Performance Analysis, Troubleshooting

Thank you for answer. Most helpful.

Stephen

On Ruby/Tk, all operations and callbacks are evaluated on the
eventloop thread. All calls are serialized to avoid conflict of
GUI operation. Therefore, never wait or sleep on the callback
operation!!

....

···

In message <20050930.115615.74743836.nagai@ai.kyutech.ac.jp>, Hidetoshi NAGAI <nagai@ai.kyutech.ac.jp> writes
--
Stephen Kellett
Object Media Limited http://www.objmedia.demon.co.uk/software.html
Computer Consultancy, Software Development
Windows C++, Java, Assembler, Performance Analysis, Troubleshooting

Awesome Hidetoshi! Thanks

···

On 9/30/05, Stephen Kellett <snail@objmedia.demon.co.uk> wrote:

In message <20050930.115615.74743836.nagai@ai.kyutech.ac.jp>, Hidetoshi > NAGAI <nagai@ai.kyutech.ac.jp> writes

Thank you for answer. Most helpful.

Stephen

>On Ruby/Tk, all operations and callbacks are evaluated on the
>eventloop thread. All calls are serialized to avoid conflict of
>GUI operation. Therefore, never wait or sleep on the callback
>operation!!

....
--
Stephen Kellett
Object Media Limited http://www.objmedia.demon.co.uk/software.html
Computer Consultancy, Software Development
Windows C++, Java, Assembler, Performance Analysis, Troubleshooting