Slash Boxes
NOTE: use Perl; is on undef hiatus. You can read content, but you can't post it. More info will be forthcoming forthcomingly.

All the Perl that's Practical to Extract and Report

use Perl Log In

Log In

[ Create a new account ]

tsee (4409)

  {smueller} {at} {}

You can find most of my Open Source Perl software in my CPAN directory [].

Journal of tsee (4409)

Saturday July 04, 2009
06:29 AM

In defense of Perl ithreads

[ #39225 ]

People like to point out the problems with Perl's threading, say they're simply the Windows fork-emulation ported to other operating systems and conclude that they're of no use otherwise. They generally omit mentioning the cases in which Perl ithreads are the only viable solution for concurrency in Perl.

First, you have to understand the i in ithreads. Read: interpreter threads. Each ithread in your Perl program has its own copy of the whole interpreter. Nothing is shared between the interpreters by default*. Most other threading implementations work the other way around: By default they share everything and the user has to deal with locking of any shared resources. This has many advantages over ithreads. Most obviously, an ithread takes a lot more memory. Furthermore, passing data between ithreads is rather painful and very, very slow. But there is, unfortunately, a big downside to shared-by-default:

Real concurrency (i.e. multiple threads executing concurrently on multiple CPUs) doesn't seem to be feasible with the shared-by-default approach in a language such as Perl. This is because almost all operations -- including those who seem to be entirely read-only -- can potentially modify the data structures. Use a scalar that contains an integer in a string-context and the scalar will be modified to contain a char*. Marc Lehmann explained this in more detail in his talk "Why so-called Perl threads should die" at the German Perl Workshop 2009. (Couldn't find his slides online, sorry.) As far as I know, the typcial dynamic programming languages other than Perl only have (non-concurrent) cooperative multi-threading to start with.

Now, some will be quick to point out that ithreads are a mere fork() reimplementation with quite a few disadvantages. For a real fork, the kernel can do COW and non-thread-safe XS modules aren't a problem. But if your software has to run on Windows, the fork's a non-starter. As mentioned earlier, threads are used for the emulation of fork() on Windows. That means if you use fork(), you'll get multiple processes on systems which support it natively and multiple (i)threads on Windows with all the associated problems regarding memory use and thread-safety. If you're writing software predominantly on Linux, would you rather debug problems in your development environment or on your customer's (or generally user's) machine? I thought so. There is a case to be made for consistency.

The other big contender is the excellent Coro module (or an event loop). I suggest you have a look at its documentation to understand what it does exactly**. The downside? It's cooperative multi-threading. It doesn't really run concurrently. The code in each Coro has to cede control to the other coroutines regularly. If there is some code that's not directly under your control and takes a long time, your GUI or what not will be blocked. If you think about it a bit, you'll realize that heavy re-use of code from CPAN and cooperative multi-threading is a non-starter. In my case, I needed to parse Perl code using PPI. That can take seconds...

I'm all ears for suggestions on how to do concurrency right in a dynamic language. (here: Perl, but if another language does it better, that'd be interesting, too.)

The requirements are:

  • Real concurrency, i.e. using multiple cores.
  • Non-cooperative multi-threading due to code that is not under my control
  • Portability
  • A point I haven't touched in my rant: Ability to recover from crashes. You can recover from crashing ithreads.

* This statement may become painfully untrue if you use a non-thread-safe XS module, unfortunately.

** I'm not restating what it does in my own words because I'd expect them to be slightly inaccurate thus provoking eternal damnation.

The Fine Print: The following comments are owned by whoever posted them. We are not responsible for them in any way.
More | Login | Reply
Loading... please wait.
  • I’m still waiting for the point where you show why ithreads aren’t merely a bad emulation of fork() for Windows. Why would someone who is not personally interested in deploying to fork()-enabled systems ever choose ithreads over fork()? I don’t see any good reason, particularly since a threaded perl is ~20% slower (and somewhat more memory hungry) across the board – no matter whether it’s ever going to run any threaded code or not. And if you exclude Windows from the portabilit

    • If you want to pass non-trivial data around between multiple threads of execution, (i)threads are a far more convenient solution than implementing your own marshalling/serialization code and using fork().

    • Apart from Corion's convenience argument, I don't know a good reason why ithreads are much better than fork() for non-portable code. Maybe that managing a bunch of threads seems a bit easier to me than managing a bunch of processes (no zombies).

      Maybe I wasn't clear enough on some things: I don't particularly like ithreads. It's just that they're the best solution (known to me) in some cases.

  • ithreads have two problems:

    1. When you spawn a thread, it copies the whole environment from the parent thread, variables and code. This results in poor performance when a lot of libraries are loaded.

    2. You can't pass a file descriptor from one thread to the other. So you can't create a network server that spawns a thread for each client.

    This makes perl one of the only scripting language you can't use to build a multi-threaded network server, without using fancy stuff like Coro, AnyEvent or POE. Those are gr

    • C:\Projekte\App-CPANr>perl -Mthreads -e "open my $fh, '<', '/boot.ini'; for (async { print for <$fh>}, async{sleep 10}) { $_->join; }"

      [boot loader]
      [opera ting systems]
      multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="Microsoft Windows XP Professional"
      /fastdetect /NoExecute=OptIn

      • Works great for that. How about starting ten threads, then passing them a filehandle to work on? That's what I was talking about.

        Beside, if you have a lot of stuff loaded (like POE), instead of this trivial exemple, each thread spawn is rather costly.

    • 1. When you spawn a thread, it copies the whole environment from the parent thread, variables and code. This results in poor performance when a lot of libraries are loaded.

      The worst part of this is not just the poor performance but its unpredictability and that the effect is not local but global.

      For instance, to implement a CPAN module doing something specific (as downloading a bunch of files from the Internet in parallel) that you want to be usable from any script, if the module were launching, say, 10

  • You could also consider writing a backend server that your frontend talks to with TCP/IP. I did this with IO::All so that I could have a perlcritic backend that doesn't slow down my client so much.
  • Yes, currently you will have to explicitly cede control to the other coroutines regularly. However, Yuval is currently trying to get his patch [] implementing preemptive coros merged into Coro. Those allow control to be automatically passed to other Coros using different strategies like opcode counting and fixed timeslices. That'd at least solve one of the problems you're describing.

    • That would totally make me reconsider things. Thanks for pointing this out!

    • Of course, by making coroutines actually execute in parallel, you give up the one thing that Coro provides over threads - you invite back in the daemons of locking and deadlocks.

      • Luckily, preemptive coros don't do that. Everything still runs in one process, one coro at a time. Just passing control from one coro to another can happen implicitly, so blocking code that isn't under your control is not a problem anymore.

    • Excuse my gross ignorance, but would this remove the one-core limit?

      Could we then use even half of my four core machine?

      • This would totally not remove the one-core limit. But it would fix essentially all other problems. For multi-core usage, one would have to resort to an extra process.