nullprogram.com/blog/2010/12/21/
I've learned how to use the
curses/ncurses
library recently, and I decided to experiment with threading while
using ncurses. This posed another learning opportunity: a chance to
learn more about GNU
Pth, a non-preemptive, userspace threading library. It's a really
cool trick to get threading into any C program on any platform. I've
used POSIX threads, Pthreads, before, so Pth is totally new to me.
The idea was this: a ticking timestamp string that I can move around
the screen with the arrow keys. One thread will keep the clock up to
date, and the other listens for user input and changes the clock
coordinates.
The ncurses function for getting a key from the user
is getch()
. By default, it blocks waiting for the user to
press a key, which is returned. Unfortunately, this doesn't interact
with Pth well at all.
Pth threads are userspace threads rather than system threads. This
means the operating system kernel is completely unaware of them, so
they are managed by a userspace scheduler and the Pth threads all run
inside a single system thread. Because of this, Pth threads can never
take advantage of hardware parallelism. This disadvantage comes with
the advantage of portability (hence the name "portable threads"). It
can be used on systems that provide no threading support.
Pth threads are also non-preemptive. This means the thread currently in
control must eventually choose to yield control to other threads,
cooperating with them. Preemptive threads take control from
each other, so they never have to yield. Fortunately, the Pth library
sneaks in implicit yielding, so you usually don't have to worry about
this when using Pth. You can generally treat the Pth threads as if
they were preemptive.
As I was saying, Pth wasn't behaving well with ncurses. When I
called getch()
my entire program, all threads, were
getting blocked too, which defeats the whole purpose of threading. I
switched to Pthreads to see if it was a mistake on my part, or an
issue with Pth. Pthreads was working just fine, so I had to figure out
what I was doing wrong with Pth.
I did manage to get Pth to behave the same was as the Pthreads, but it
took two significant extra changes. Here's a code listing. There's
also a mutex synchronized draw_clock()
function not shown
here. I've written it so that I could easily switch back and forth
between the two threading libraries.
Notice the two (bolded) differences in the code between Pth and
Pthreads, in the USE_PTH
sections. The Pth implementation
uses the halfdelay()
function, which
tells getch()
to be less blocking. The argument tells it
to return with an error if nothing happens within one tenth of a
second. This means our main polling loop will execute about 10 times a
second when nothing is happening.
The second change is an explicit yield, because Pth doesn't place any
implicit yields in the loop. Without the yield the same problem
remains.
I'm not happy with either of these
changes. Making getch()
behave like non-blocking input is
very hackish. If I extend my program I'll always have to be careful
when I call getch()
, since it's (essentially)
non-blocking. I'll also have to make sure I always yield when polling
with getch()
. So how do I fix this? First, let's see why
we need that explicit yield. Pth is supposed to be hiding that from
me.
How does Pth insert implicit yielding? I've been aware of Pth for
years, and I've always wondered about this, but never bothered to
look. I dug into the Pth sources.
Pth inserts yields before some of the common blocking operations. It
has its own definitions for functions such as read()
,
write()
, fork()
,
and system()
. This is where the implicit yielding is
injected. It steals your calls to these functions, using its own
functions. Here's the relevant section of pth.h
, where
the "soft" version of this happens (the "hard" version
uses syscall()
, operating at a lower level),
Its own functions wrap the real deal, but suspend themselves on an
awaiting event and yield. Any time the scheduler runs, it polls for
these events, using select()
in the case of input/output,
and will wake these threads back up if the required event occurs.
The problem with getch()
is that Pth doesn't know about
it, so it doesn't get a chance to handle it properly. After taking a
look at the implementation for pth_read()
, I
fixed getch()
for my program,
It tells the Pth scheduler that the thread wants to be suspended until
there's something to read on stdin
(file descriptor
0
). This prevents the system thread that everyone is
counting on from blocking. With this redefinition I went back and
removed the two Pth additions, halfdelay()
and the yield,
and it now behaves exactly the same way as the Pthreads version. Fixed!
If you use any other libraries, you'll need to do this for any
long-blocking function that Pth doesn't already catch.
If you want to see this in action, here's the full
source: clock.c
. You can
choose the threading library to use,
gcc -lncurses -lpth -DUSE_PTH clock.c -o clock_pth
gcc -lncurses -pthread clock.c -o clock_pthread
Update: I've just discovered that Debian and Debian-based
systems
have implicit yeilding disabled by default. You may have needed to
add this before the pth.h
include.
This is now in the linked source.