Beefy Boxes and Bandwidth Generously Provided by pair Networks
Welcome to the Monastery
 
PerlMonks  

Semaphores failing to prevent race condition

by Llew_Llaw_Gyffes (Scribe)
on Mar 12, 2011 at 21:18 UTC ( [id://892863]=perlquestion: print w/replies, xml ) Need Help??

Llew_Llaw_Gyffes has asked for the wisdom of the Perl Monks concerning the following question:

Brethren,

I have a Perl application (it's an ICB client, but that doesn't really matter for this purpose) written with multiple threads (I wrote it partly to learn to use threads) and a character-mode Curses interface implemented directly with Curses.pm. The interface is nothing fancy - no boxes, menus, borders, anything like that; just a an input pane occupying (by default, but configurable) the bottom five lines of the terminal, a single status line above it, and the rest of the window above that a scrolling display pane. (Think of ICB as IRC Lite on standalone servers.) There are five threads in all: the master thread; an output thread that handles incoming messages, processes hooks and triggers, and updates the display pane; an input thread that handles key events in a readline-like manner, handles input editing and history, and sends messages; a status thread that maintains the status line; and a logging thread that listens to 9and, when activated, logs to disk) both incoming and outgoing messages.

Naturally, with five threads, three of which can update the terminal, there is a need to make sure only one of them is updating it at once. Therefore, I have a semaphore:

my $output_sem : shared = Thread::Semaphore->new();

And every operation that updates the terminal is wrapped in calls to $output_sem->down() and $output_sem->up(). For instance, an example from within the talk() function of the input thread:

if ($success) { $output_sem->down(); erase($input_window); addstr($input_window, $buffer); refresh($input_window); $ptr = length($buffer); curs_to_ptr($input_window, $ptr); $output_sem->up(); }

Or. a little further down in talk():

elsif ($key eq chr(1)) # home { $output_sem->down(); $ptr = 0; curs_to_ptr($input_window, $ptr); $output_sem->up(); }

(Here, curs_to_ptr() is a function that calculates from the length of the input buffer where in the input pane the cursor should be, and moves it there.)

Meanwhile, all output to the display pane is happening via this function:

sub icb_print { my ($window, $color, $fmt, @args) = @_; my $buf = sprintf ($fmt, @args); $output_sem->down(); if ($use_color) { attron ($window, COLOR_PAIR($colors{$color})) if ($colors{$col +or}); attron ($window, A_BOLD) if ($attr{$color} & 1); attron ($window, A_REVERSE) if ($attr{$color} & 2); } addstr($window, $buf); if ($use_color) { attroff ($window, COLOR_PAIR($colors{$color})) if ($colors{$co +lor}); attroff ($window, A_BOLD) if ($attr{$color} & 1); attroff ($window, A_REVERSE) if ($attr{$color} & 2); } refresh($window); log_send($buf) if ($log_sem); if (chomp($buf)) { if ($page[0]) { $page[1]++ unless ($page[2]); # count newlines if ($page[1] > $page[0]) # maintain paused flag { unless ($page[2]) { $page[2] = 1; icb_print ($window, 'more', "%s\n", " ---- PAUSED +: Press Enter to continue ---- "); } } } } $output_sem->up(); }

While the status line is maintained by a similar but separate function (necessarily, because otherwise, as a result of how the status line update function works, I'd have to have nested $output_sem->down() calls, which wouldn't work too well), also wrapped between $output_sem->down() and $output_sem->up() calls.

Now, one would normally expect that this ought to work pretty well, and do a good job of keeping one thread from writing into another's area of the display. And, as long as I was on a single-core machine, it mostly did, though there was occasional display corruption that I never managed to isolate. But about four or five months ago now, I built myself a new, much faster workstation with six cores, and all hell broke loose. The lower portion of the screen should look something like this. And most of the time it does. But sometimes, something steps on something else, and something like this happens. (There's a full-window example of a particularly egregious corruption incident here.) Sorry to make you follow links; I'd have inlined the first two images if permitted, they're small GIFs and only a few K each.

As far as I can tell, I've taken every possible precaution. But I cannot eliminate this problem, or even isolate exactly how or why it is happening. This leads me to have to ask whether anyone can answer the following questions:

— Are Curses.pm and Thread::Semaphore known for certain to be thread-safe on multi-core processors? (I'm running with Perl 5.12.2, Curses.pm 1.28, threads 1.82, threads::shared 1.36, Threads::Semaphore 2.12.)

— Is there anything that immediately leaps out that I have conspicuously overlooked in my efforts to avoid a race condition?

— Is there any likely cause I may have overlooked for this display corruption, and if so, what can I do about it?

Replies are listed 'Best First'.
Re: Semaphores failing to prevent race condition
by BrowserUk (Patriarch) on Mar 12, 2011 at 21:54 UTC

    I written a lot of threaded perl code over the last 8 or 9 years, and I've never had occasion to use Thread::Semaphore. To me it seems to be a very complicated (and apparently broken) way to do something very simple.

    I would ditch that module in favour of a very simple mechanism.

    my $outputSem :shared; ... if ($success) { lock $outputSem; erase($input_window); addstr($input_window, $buffer); refresh($input_window); $ptr = length($buffer); curs_to_ptr($input_window, $ptr); } ... sub icb_print { my ($window, $color, $fmt, @args) = @_; my $buf = sprintf ($fmt, @args); lock $outputSem; ... }

    No guarantees given I cannot see never mind run most of your code, but that simple change will probably fix your problems.


    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.
      I think I'm going to try this first, given that at this point I'm rather suspicious of Thread::Semaphore.

        Not conclusive yet, as the client's only been running about three days, but it looks as though this simple fix solved the problem.

        The answer seems to be that Thread::Semaphore is, ironically, not thread-safe.

Re: Semaphores failing to prevent race condition
by roboticus (Chancellor) on Mar 12, 2011 at 21:52 UTC

    Llew_Llaw_Gyffes:

    Generally I don't let multiple threads talk to the terminal, mostly because of the problems you're experiencing right now. Instead, I tend to let asynchronous tasks add their complete messages to a queue, and have one thread (usually the one starting all the others) poll the queues and print messages.

    The primary difficulties of sharing a terminal with multiple writers is due to (1) buffering (see suffering from buffering), and (2) you can task switch in the *middle* of an escape sequence. So you get text out of the expected sequence, and in the wrong locations and colors due to the broken escape sequences.

    If you give each thread an array that they can put complete messages into, and have one thread grabbing distinct messages and printing them, you can more easily manage your terminal.

    ...roboticus

    When your only tool is a hammer, all problems look like your thumb.

    Update: Corrected name.

      So possibly a single screen-interaction thread with messages passed to it via Thread::Queue, then?

        Sure thing.

        ...roboticus

        When your only tool is a hammer, all problems look like your thumb.

      Oh, BTW, buffering isn't going to help in this case because there is no I/O direct to a filehandle. All output is sent to Curses window objects as addstr(), erase(), etc.
Re: Semaphores failing to prevent race condition
by Anonymous Monk on Mar 12, 2011 at 22:03 UTC
    — Are Curses.pm and Thread::Semaphore known for certain to be thread-safe on multi-core processors?

    My understanding is, there is only thread-safe or not thread-safe, and since Curses doesn't go so far as to mention thread safety, its safe to assume it isn't thread-safe.

    Thread safety isn't that important, if you stick to the rule of thumb to only require the unsafe modules you need in child threads.

    The other rule of thumb is to only have one thread doing GUI stuff, and have all the other threads signal it.

      Thread safety isn't that important, if you stick to the rule of thumb to only require the unsafe modules you need in child threads.

      You're confusing threads and processes here. Perl code is shared across all of the process's threads. It doesn't matter when you require the module.

      My understanding is, there is only thread-safe or not thread-safe

      I usually classify as:

      • Safe to use from multiple threads.
      • Safe to use from just one of the threads.
      • Not safe to use when threads are used.
        You're confusing threads and processes here.

        No I am not. If you use Tk; or some other module, and then you use threads, instead of using require Tk;, you'll run into segfaults, etc etc. Using require Tk; avoids the issue completely.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://892863]
Approved by GrandFather
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others having a coffee break in the Monastery: (4)
As of 2024-03-28 08:10 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found