On Tuesday, May 27, 2003, at 07:32 PM, Jonathan Scott Duff wrote:
On Tue, May 27, 2003 at 02:05:57PM -0700, Michael Lazzaro wrote:
If we could think about "threads" not in terms of forkyness, but simply
in terms of coroutines that can be called in parallel, it should be
possible to create an implementation of "threading" that had to do a
whole heck-of-a-lot less duplication of state, etc.

See, this where I start to feel all Cozeny and wonder what the heck we're doing even thinking about how it's implemented. What I want to know is how it looks from the user perspective.

Sorry, yes, I'm not talking at all about implementation. I'm just talking about syntax/semantics of invoking them. Underneath, threads and coroutines may well be implemented by giant, man-eating woodchucks battling to the death on a rainy day under a popsicle-stick bridge, for all I care. :-)



If, in order to
understand threads, I have to first understand coroutines, I think
that's a loss because it throws away (or at least morphs into an
unrecognizable form) all of collect CS knowledge of what "threading"
usually means.  In other words, I think the idea of fork-like
behaviour is important to threads.

[Philosophy]


Here's the possible defect in my brain that started my train of thought.

I hadn't used coroutines in a long while, so when the topic came up I had to do a little reviewing to make sure I was on the same page. Reviewing done, I started thinking about how coroutines were a different concept from continuations, which were different from threads, and we already had closures, and at what point was the aggregation of all those things going to cause potential Perl6 converts to look at our hypothetical table-o-contents and say "WTF -- exactly how much 'Perl' am I gonna have to learn to be considered a 'good' Perl programmer?"

Because _all_ those things are useful concepts, but in spite of the fact that they address very similar problem spaces -- allowing "program state" to be saved and resumed in less-than-strictly-linear ways -- they historically approach it in rather different ways. Whether you're sucking in a concept from a well-worn but sortof-crazy-aunt language like Lisp, or from a possibly-created-as-the-result-of-a-wager language like Icon, each concept largely reached its current fruition independent of the others. No currently-popular(?) language uses _all_ of them at the same time, or to be more precise, you don't see many programmers using _all_ those concepts simultaneously within a given application (and my gut tells me I'd not be keen to debug it if I saw it.)

OK. So I get to thinking about coroutines. Coroutines are one of those things that Freak People Out, but once you get to use them, they're quite easy to understand, and damn useful. They're like blocking threads; you call them like a subroutine, they (eventually) return a result to you, and you go on your merry way. They are used _exactly_ as subroutines are used... the only difference is that, like a thread, you can _resume_ the subroutine from the point you last yield()ed, and continue on your way within the subroutine as if that yield() never happened. Again, like a suspended and resumed thread.

True threads, on the other hand, are one of those things that everyone *thinks* they know, but which have so many issues with scheduling, blocking, data sharing and hiding, and general wiggyness that it's a lot harder to implement a well-engineered group of threads than most people are willing to admit. Much of that is intrinsic, by definition, to _any_ attempt at parallelization. But how much?

Threads are a low-level interface to parallelization. But realistically, people use threads for a *very* narrow set of real-world concepts:

(Concept 1) Creating event loops, listeners, etc., in which an event spawns an "action", which runs in parallel to anything else which is running and is independent of all those other things -- so whether it succeeds, fails, or just never ends, it doesn't affect the rest of the program. We'll call this the "subprocess" model.

(Concept 2) Splitting multiple "lengthy" tasks to run in parallel, to allow calculations on one task to proceed even if another task is in a blocked (waiting) state, as frequently happens in I/O, or to allow multiple calculations to be run on multiple processors. We'll call this the "parallelization" model.


Current popular thread semantics, I would argue, are entirely designed around (Concept 1), and are designed to be very similar to actual _process_ forking. Which at first glance is great, because if you understand process fork()ing you understand threading, but there are two problems with that. First, threads aren't processes, so the analogy breaks down upon more complicated inspection -- if you think of threads as processes, you're going to very quickly get burned by the fact that they aren't. Second, you don't _want_ them to be like processes, because you often don't _want_ your entire program to be duplicated as with fork(), to run off on its own like some goateed Kirk/Spock/whoever from any given "transporter malfunctioning and/or parallel universe" Star Trek episode -- often, the threads represent small snippets of much narrower functionality. This model is therefore rather unhelpful for the narrower "parallel task" cases of (Concept 2).


If you look at (Concept 1) vs. (Concept 2), they're pretty significantly different. An interface that best supports (Concept 1) is not going to well support (Concept 2), and vice versa -- I would argue it can't be done. They're at least as different as the concepts of continuation vs. coroutine, for example!

So how would you design code parallelization -- multiple pieces of code running at the same time, independent of each other, if you were approaching it from the direction of solving (Concept 2)? A pretty easy question, since other languages have already answered it. In general, you'd want to call the code, and return a result, and the caller wouldn't give a flying damn about whether that call resulted in a parallel process or not, so long as the result was obtained before the caller needed it:

     my $result = big_long_calculation();
     ...
     print $result;

You wouldn't want the caller to have to specify that a thread was being fork()ed and joined back up again later -- the caller doesn't care. It's up to big_long_calculation() to run in parallel, if it was specifically designed to allow it. MUCH LIKE COROUTINES, you don't want to be exposed to the "how" of the guts of big_long_calculation... you should just know that it works.

But if, on the other hand, you _want_ to explicitly say that two tasks can run in parallel, you could have a syntax that could do that:

my @results = parallel( get_slow_data($a), get_slow_data($b) );

Again, you don't really give a fuzz about _how_ that's happening, so long as the results get to you by the time you need them.

How, then, would you design an interface around (Concept 1) -- the listener/eventloop/subprocess model? Pseudocode for a top-level event loop might be something like:

    loop {
        sleep($fraction), next
          unless $e = get_event();

        process_event($e);
    }

... where process_event implicitly does so in a parallel thread. Note that you're not using the return value of process_event, so there's no "result" to wait for -- the loop simply continues to the next event.

The pseudocode implies the ability to mark process_event() as allowed to "automatically" be parallelized when called:

    thread process_event (...args...) { ... }
or
    sub process_event (...args...) is threaded { ... }

The point being, you would _still_ be calling process_event() as a normal subroutine; you wouldn't particularly care about how it did whatever it did, as long as it did it.

And the larger point being, if you were to design semantics around easy support for the basic things threads are actually used for in the real world, it probably wouldn't be the fork()-like threading semantics we so often see. It'd look, conceptually, a lot more like subroutines or coroutines.

:-/

MikeL


[*] Note, as an aside, that another aspect of this "coroutine" version of threads is that you get to choose whether or not exceptions in your "threads" propagate to the caller. By default, they would, because you'd want to know about them:


my $result = big_long_calculation();

but you would also want to be able to prevent that from happening, which you easily could using obvious, already-familiar constructs:

    thread process_event () {
        ...
        try { stuff } catch Exception { }
    }

You're also allowed to meaningfully have subthreads within threads, thread "groups" controlled by different schedulers, etc, as you would expect. Note also that coroutine-like semantics for threads might imply that shared data between threads would largely be a matter of variable scope, as in Austin's previous threads proposal. Which could be a lot more intuitive, if done right.



Reply via email to