Register Now | Sign In
FeedGhost Logo

RSS feed Stu Smith: Making It Up As I Go Along

My life working for BinaryComponents, coding, design, and other stuff.


As I mentioned in a previous post, I've been reading through "Beautiful Code: Leading Programmers Explain How They Think", and one chapter that really caught my attention was about Software Transactional Memory. What I'm about to present in this mini-series therefore is merely my take on an idea that greater minds than mine have come up with.

 

The Problems with Locking

I'm a C# / .NET programmer, and the staple multi-threaded construct is the lock block (which is basically a monitor or mutex):

lock( _lockObject )
{
  SharedResource.Foo(); // Access a shared resource.
}

All fine and good, and you certainly can't do without them, but there's a bunch of problems with them:

 

1. Locks Aren't Enforced

Since there's no necessary connection between the lock and the resource, it's possible to forget to lock:

Thread_1
{
  lock( _lock )
  {
    SharedResource.Foo(); // Access a shared resource.
  }
}
 
Thread_2
{
  SharedResource.Foo(); // Access a shared resource.
  // Oops!
}

There's not even a runtime error here. That program is wrong, silently wrong, and difficult to debug.

 

2. Locks Don't Combine

Let's say we have a bank account class:

class BankAccount
{
  public void Deposit( long change )
  {
    lock( _lock )
    {
      _amount += change;
    }
  }

  private long _amount;
  private object _lock = new object();
}

Then how do we write a thread-safe transfer method?

public static void Transfer( BankAccount a, BankAccount b, long amount )
{
  // This is incorrect:
  a.Deposit( -amount );
  b.Deposit( amount );
}

What can I lock on to make this function correct?

 

3. Locks Can Deadlock

If you're using more than one lock, you'd better get them in the right order, otherwise you're jammed. Getting into deadlock is easy; getting out is near-impossible.

 

The Cliff-Hanger

So locks are evil then. A necessary evil, but evil nonetheless. But we can hide them away...

As a parting teaser before the following parts, here's my implementation of the Dining Philosophers problem, written without any (obvious) locks:

using System;

using System.Threading;

 

namespace ConsoleTests

{

  public static class DiningPhilosophers

  {

    public static void Run()

    {

      //

      // Create fork values.

 

      for( var i = 0; i < _numPhilosophers; ++i )

      {

        _forks[i] = new BinaryComponents.TransactionalMemory.Values.Boolean( false );

      }

 

      //

      // Create a thread for each philosopher.

 

      for( var i = 0; i < _numPhilosophers; ++i )

      {

        var pts = new ParameterizedThreadStart( RunPhilosopher );

        var thread = new Thread( pts );

 

        thread.Start( i );

      }

    }

 

    private static void RunPhilosopher( object data )

    {

      var n = (int) data;

      var rnd = new Random();

      var leftFork = _forks[n];

      var rightFork = _forks[( n + 1 ) % _numPhilosophers];

 

      while( true )

      {

        Console.WriteLine( string.Format( "Philosopher {0} is hungry...", n ) );

 

        //

        // Try to grab the forks.

 

        BinaryComponents.TransactionalMemory.Manager.Execute( delegate

        {

          //

          // If either fork we want is in use, then retry.

 

          if( leftFork.Value || rightFork.Value )

          {

            return false;

          }

 

          //

          // Otherwise, grab them.

 

          leftFork.Value = true;

          rightFork.Value = true;

 

          return true;

        } );

 

        //

        // Eat for a while.

 

        Console.WriteLine( string.Format( "Philosopher {0} is eating.", n ) );

 

        Thread.Sleep( rnd.Next( 5000 ) );

 

        Console.WriteLine( string.Format( "Philosopher {0} puts down the forks and starts thinking.", n ) );

 

        //

        // Drop the forks.

 

        BinaryComponents.TransactionalMemory.Manager.Execute( delegate

        {

          leftFork.Value = false;

          rightFork.Value = false;

 

          return true;

        } );

 

        //

        // Think for a while.

 

        Thread.Sleep( rnd.Next( 5000 ) );

      }

    }

 

    private const int _numPhilosophers = 5;

 

    private static BinaryComponents.TransactionalMemory.Values.Boolean[] _forks = new BinaryComponents.TransactionalMemory.Values.Boolean[_numPhilosophers];

  }

}

FeedGhost - Professional RSS Reading



re: Easier Threading: Software Transactional Memory: Part 1
05 Sep 2007 05:42 by James Holderness
I'm not very familiar with the workings of transactional memory, but if my understanding is correct, it seems to me you're almost guaranteed to deadlock immediately on a multi-core system.

If each thread is on a separate core, so they're genuinely running at the same time, each philosopher will try to start eating at exactly the same time. Because of the way transactional memory works, all the forks appear to be free, so they all happily grab two forks. It's only at the end of the transaction that they figure out there's been a conflict (for all of them), and all the transactions have to be rolled back. They're now back where they started, with nobody having a fork and everybody wanting to eat. The deadlock cycle begins again.

Or is that not how transactional memory works?
re: Easier Threading: Software Transactional Memory: Part 1
05 Sep 2007 10:40 by Stu Smith
Hi James,

hopefully the next couple of articles in the series will explain further, but here's my quick understanding/explanation of what happens.

1. Each philosopher starts eating at the same time: that is true. The system I've built is more like "optimistic" concurrency.
2. However, the STM system does use locks where appropriate - it's just that they're handled by the implementation. On the first access to a transactional variable, by each thread, a lock is taken, and a copy of the value is promoted to the log, together with a copy of the original value.
3. When a transaction is ready to commit, the log is validated. All transactional values that have been read or written are locked, and the transaction is only allowed to commit if all the stored original values match the ones in memory at the end of the transaction.
4. Hence, depending on how many philosophers there are, at least one will be able to validate the transaction and commit. Some of course will have to rollback and retry - and retries can be made efficient via events.

Maybe it was a bit misleading of me to present it as lock-free - it's not, locks are taken as needed by the implementation, since you need to use the locking primitives somewhere. Here's an analogy: at some level in the system, GOTOs are a necessity (eg at machine level). Client code however, shouldn't use that low-level construct, but should use something higher-level, say, foreach. I feel the same way about locks. (Whether STM is a suitable higher-level construct is of course open for debate).

If you want to look at the source in advance of the rest of the articles, you can find it at the bottom of this page:

http://www.binarycomponents.com/Labs

Regards,
Stu

What do you think?

Your name: (optional)
Comment: (no HTML please)
Enter the numbers: Security Code