Thread::Sociable

Description Enhanced replacement for threads::shared
Thread::Sociable > Package Manuals > Thread-Sociable
Source

Index


NAME ^

Thread::Sociable - Less restrictive replacement for threads::shared

VERSION ^

0.10

SYNOPSIS ^

  use threads;
  use Thread::Sociable;

  my $var : Sociable;
  my $lockvar : Lockable;
  $var = $scalar_value;
  $var = $sociable_ref_value;
  $var = socialize($simple_unshared_ref_value);
  $lockvar = socialize_with_lock($simple_unshared_ref_value);

  my($scalar, @array, %hash);
  socialize($scalar);
  socialize(@array);
  socialize_with_lock(%hash);
  my $bar = &socialize([]);
  $hash{bar} = &socialize({});

  { lock(%hash); ...  }

  cond_wait($scalar);
  cond_timedwait($scalar, time() + 30);
  cond_broadcast(@array);
  cond_signal(%hash);

  my $lockvar : Lockable;
  # condition var != lock var
  cond_wait($var, $lockvar);
  cond_timedwait($var, time()+30, $lockvar);
  #
  # verify the variable is sociable
  #
  print "\$lockvar is sociable\n"
      if is_sociable($lockvar);
  #
  # verify the variable is lockable
  #
  print "\$lockvar is lockable\n"
      if is_lockable($lockvar);
  #
  #    discard the sociable variable; causes
  # $lockvar to be come private again, and
  # all other references in other threads
  # will also become private
  #
  sociable_discard($lockvar);

  print "\$lockvar is not sociable\n"
      unless is_sociable($lockvar);
  #
  #    set default extents
  #
  my ($old_extend, $old_himark) =
      sociable_default_extent($new_extent, $new_himark);
  #
  #    set extents on individual array or hash
  #
  my ($old_extend, $old_himark) =
      sociable_extent(@array, $new_extent, $new_himark);

  my ($old_extend, $old_himark) =
      sociable_extent(%hash, $new_extent, $new_himark);
  #
  # tie a sociable using a Thread::Sociable::Apartment closure container
  #
  sociable_tie $sociable, $tsa_closure;
  #
  # execute a transaction
  #
  sociable_begin_work(sub {
      },
      onCommit => sub {
          print "We're in like Flynn!\n";
      },
      onRestart => sub {
          print "Curses, foiled again!\n";
      },
      onRollback => sub {
          print "OOOOPS!! We're hosed!\n";
      }
  );

DESCRIPTION ^

Within Perl's current ithreads implementation, variables are private to each thread, and each newly created thread gets a private copy of each existing variable. Thread::Sociable permits sharing of variables between threads (and pseudoforks on Win32). While the interface is largely based on threads::shared, a modified backend is provided that minimizes the performance penalty of the highly pessimistic/contentious global shared interpretter locking upon which threads:shared is heavily reliant.

Additionally, Thread::Sociable provides

Background

The current solution for sharing variables between threads, threads::shared, imposes a significant burden: since all threads::shared variables are managed within a shared Perl interpretter context, a single global lock on that context must be acquired before any thread can do anything to any thread::shared variable. No matter if each thread is referencing completely different variables, and is only doing read operations, they all must first exclusively acquire the global lock before accessing the shared variables (Frankly, it calls into question the point of providing application-level locking primitives...). The problem is exacerbated by the frequent reference count adjustments applied to the shared variables as various threads create and destroy proxy versions and references.

While such overhead may not be significant for a few threads making infrequent accesses to shared variables, for large scale threaded applications, especially those which frequently pass data amongst themselves via either Thread::Queue or Thread::Queue::Duplex (e.g., Thread::Apartment based applications), the performance penalty can be very severe (so much so that using process-based concurrency with, e.g., IPC::Mmap and Storable becomes a viable alternative).

threads::shared's design seems to stem from a "give me convenience or give me death" approach: by reusing the Perl variable management code already available in the shared interpretter, minimal additional code is required. In addition, it preserves the convenience of reference counted reclamation which Perl applications have come to rely on. Alas, to paraphrase, "The wages of convenience is death", or at least performance closely resembling a moribund state.

An Inconvenient Alternative

Thread::Sociable is based on a more pragmatic view of sharing data between threads. By definition (with a few exceptional platforms most users will never encounter), all threads have access to all other threads' memory. Assuming an internal structure to manage variable state between threads, and support for standard application level locking primitives, there's little need for the heavyweight solution provided by threads::shared...except for the convenience of reusing Perl variable management and reclamation code, and automatic lock structure creation and destruction. Lets look at the drawbacks and benefits of Thread::Sociable:

Drawbacks

Requires more internal code to manage variables

Since it uses its own context structures allocated from the process's native (rather than Perl's) heap, Thread::Sociable cannot simply delegate variable management to a shared interpretter. Fortunately, much of the additional code is simply cut/paste from Perl core (e.g., hash management). The advantage of this approach is that, by careful coding, global locking can be avoided except for variable creation and destruction; beyond that, application of concurrency control to individual variables is at the sole discretion of the application.

Another benefit is that it provides opportunities to add new capabilities, and/or exploit additional external libraries (e.g., Hoard).

Requires explicit declaration of lockable variables

Thread::Sociable requires explicit declaration of variables that are to be subjected to locking.

This feature helps avoid the need for frequent interstitial global locks, since the additional locking resources can be established within the global lock during variable creation, rather than at some subsequent arbitrary point in time. In most (nearly all ?) instances, an application will have a priori knowledge whether a variable will need to negotiate concurrent access during its lifetime, so imposing this requirement shouldn't be too much of a burden (and may even be considered a beneficial annotation).

Requires explicit destruction to free resources

Sociable variables must be explicitly destroyed to free their resources.

Like all sociable beings, sociable variables enjoy a good party, and don't respond to hints that the party is over: they must be politely told to get out.

Thread::Sociable does not perform reference counting as variables are passed between threads, thereby avoiding the repeated need for locking/unlocking a global context to perform the refcount, and possible destruction when a refcount drops to zero. Instead, applications are responsible for determining when a sociable variable can be discarded. Explicit destruction is a minor inconvenience paid to improve scalability and performance, and likewise may be considered a useful annotation. Furthermore, in many instances, shared variables are intended to be long lived, and thus all the overhead of reference counting - including the global locking - is a waste of time. In fact, many consider the pre-allocation and pooling of a fixed number of such shared resources to be best practice (much like creating a pool of reusable threads).

Note that explicit destruction is NOT required on process exit (or at all), but only when an application determines a sociable variable is no longer needed, and its resources might be recycled for a future variable.

Also note that in the event of an inadvertant access to a discarded sociable variable, the operation will cause the sociable variable to revert to an empty private version. The resource slot for each sociable variable contains a recycle count signature which is verified for each attempted access; if the recycle counts do not match, the proxy variable's "magic" is removed, and the current private contents are then used. However, the sociable_enable_warnings() method can be called to cause a Perl warning to be issued whenever such an inadvertant access occurs.

Benefits

Despite these inconveniences, Thread::Sociable provides some benefits (aside from the performance improvment):

Performance

By avoiding the frequent global locking required by threads::shared, Thread::Sociable achieves significant performance improvements. Benchmarks indicate that, in the simplest single CPU, single threaded case, sociable variables are twice as fast to read and 3x faster to write than threads::shared equivalents. When that benchmark is expanded to multithreaded operations, the performance improvement becomes more dramatic, ranging from 4x for a few concurrent threads, to 8x or more for 20+ threads. Finally, when benchmarked on multi-CPU/multicore platforms, the performance differences can be Ax to Bx.

Optimized Scalar Reads

An additional optimization is provided to exploit the manner in which Perl's internal scalar data is stored. A sequence counter is maintained for scalar sociables that is incremented each time the variable is written, and is copied to the private proxy version on every read operation. When a proxy read is initiated, the proxy's sequence number is compared to the sociable version, and if the same, no copy operation is performed. While the impact on small size scalars is negligible, access speeds for larger size scalars can be greatly improved by avoiding an unneccesary buffer copy - which may be very useful for certain operations (e.g., complex regular expression evaluation, etc.)

splice() Support

Thread::Sociable supports array splicing; threads::shared does not.

Optimized Resource Allocation

Thread::Sociable allocates sociable context resources in clusters.

In addition, Thread::Sociable supports tunable "extents" for aggregate sociable variables (arrays and hashes). Extents are per-variable pools of sociable scalar elements, and consist of 2 adjustable control values:

By maintaining a small pool of scalar elements attached individual arrays/hashes, most additions/deletions to/from the aggregate variable can be managed without acquiring the global context lock, thereby reducing thread contention and improving overall performance. A global default extent and himark are applied when the variable is created; the variable's value may then be individually adjusted if needed. Methods are provided to adjust both the global default extent and the per-variable extent. The default extent is 10, and the default himark is 20.

To illustrate the purpose of extents, consider the following examples:

Integrated Queueing Solution

Thread::Sociable incorporates significant portions of the Thread::Sociable::DuplexQueue module within its XS code, thereby achieving significant performance improvements for some common uses of thread-shared data, i.e., passing the data between threads. Additionally, the queue management context is optimized for the duplex queueing operations used by Thread::Sociable::Apartment thereby improving performance of inter-apartment method calls. Benchmarks indicate that Thread::Sociable::DuplexQueue is X times faster than the Thread::Queue equivalent operations, and Y times faster the the equivalent Thread::Queue::Duplex and Thread::Queue::Multiplex. Furthermore, tests on round trip times for calls between apartment threaded objects indicate Thread::Sociable::Apartment is Z times faster than Thread::Apartment.

Transient Sociables

Thread::Sociable supports transient sociables.

In certain instances (primarily for passing data between threads over a Thread::Sociable::DuplexQueue), a variable only needs to be sociable for a brief time while it is referenced by another sociable variable. Once the reference to the transient sociable variable is removed, the variable is immediately made private in the dereferencing thread, and its sociable resources are freed.

Interthread tie() Support

Thread::Sociable and Thread::Sociable::Apartment provide an inter-thread tie capability

One drawback of threads::shared is the inability to apply tie's to the variables. However, the fault may not lie entirely with threads::shared, as the behavior of a tie'd variable with respect to all the threads referencing the variable is nebulous at best. E.g., if Thread A tie's $x, then executes its STORE() method, how should $x's reference in Thread B behave ?

Thread::Sociable (along with Thread::Sociable::Apartment attempts to provide a partial solution, primarily to address issues with apartment threaded versions of DBI (DBIx::Threaded) and Perl/Tk (Tk::Threaded), both of which rely on tie's for important interfaces.

Thread::Sociable introduces the notion of a shadow variable. Essentially, the shadow variable is a private variable within a thread (specifically, an apartment thread in a Thread::Sociable::Apartment container) that is associated with a sociable variable. Thread::Sociable tracks information regarding the shadowed sociable variable in its internal structures that is used to initiate a proxied closure call to the thread with the private shadow variable whenever the shadowed variable is accessed.

E.g., consider the -textvariable attribute of a Perl/Tk Tk::Entry() widget. In a nonthreaded application, Perl/Tk uses a form of tie to set or update the Entry contents automatically, so an application can simply read or assign to the variable, and the entry's contents are immediately returned or set.

Alas, thread::shared variables can't be tie'd, and so this convenient interface is not available to Tk::Threaded. However, by using shadow variables, and a simple wrapper around the relevant widget constructors, Thread::Sociable can

1. replace the sociable -textvariable value with a private shadow variable

2. create a Thread::Sociable::Apartment::Shadow object to wrap the shadow variable

3. provide a proxy for the Shadow object to Thread::Sociable to associate with the sociable variable.

E.g., assuming Tk::Threaded is resident in Thread B, and Thread A creates an Entry widget with -textvariable = \$x> (where $x is sociable), Thread B will

- translate the -textvariable value into a private $x_shadow

- create a Shadow object to contain $x_shadow

- associate the Shadow object with $x

- Tk can then tie and otherwise manipulate $x_shadow as needed.

When Thread A later needs to read the entry widget contents, it simply reads $x, which Thread::Sociable detects as shadowed, and then queues up a proxied closure call to Thread B to FETCH() the current value of the shadow, which in turn causes Tk to perform any processing needed to retrieve the contents of the widget.

Likewise, if Thread A writes to $x, Thread::Sociable detects the shadow, and queues a proxied closure call to Thread B to STORE() the current value to the shadow, which in turn causes Tk to fill/replace the current contents of the widget.

Similar operations can be performed for tie'd array and hash objects (via tie's PUSH(), POP(), etc. interfaces). In addition, the shadowed behavior is available to all threads, i.e., Threads C, D, and E can also get/set the shadowed $x. Currently, a sociable variable can only be tied by a single thread; a future release will remove this restriction.

Software Transactional Memory (aka STM) Support

Thread::Sociable provides an STM implementation (currently supporting x86, x64, SPARC, and PowerPC platforms) to simplify development of concurrent applications.

ATTRIBUTES ^

The following variable attributes are provided:

VARIABLE : Sociable

VARIABLE : Lockable

Sociable creates the specified VARIABLE, and applies socialize() to it. Likewise, Lockable creates the specified VARIABLE, and applies socialize_with_lock() to it.

METHODS ^

Note that all these methods are exported into the use'ing package.

socialize(VARIABLE)

socialize_with_lock(VARIABLE)

socialize takes a value and marks it as shared. You can share a scalar, array, hash, scalar ref, array ref, or hash ref. socialize will return the shared rvalue, but always as a reference. socialize_with_lock is the same as socialize, but also creates lock structures for the sociable variable. All descriptions of sociable() apply equally to sociable_with_lock().

A variable can also be marked as sociable at compile time by using the : Sociable attribute, e.g., my $var : Sociable;, or sociably lockable via the : Lockable attribute: e.g., my $var : Lockable;.

Due to problems with Perl's prototyping, if you want to socialize a newly created reference, you need to use the &socialize([]) and &socialize({}) syntax.

The only values that can be assigned to a sociable scalar are other scalar values, or sociable refs:

  my $var : Sociable;
  $var = 1;              # ok
  $var = [];             # error
  $var = &socialize([]);     # ok

socialize will traverse up references exactly one level. socialize(\$a) is equivalent to socialize($a), while socialize(\\$a) is not. This means that you must create nested sociable data structures by first creating individual sociable leaf nodes, and then adding them to a sociable hash or array.

  my %hash : Sociable;
  $hash{'meaning'} = &socialize([]);
  $hash{'meaning'}[0] = &socialize({});
  $hash{'meaning'}[0]{'life'} = 42;

$id = is_sociable(VARIABLE)

is_sociable checks if the specified variable is sociable or not. If sociable, returns the variable's internal ID (similar to refaddr()). Otherwise, returns undef.

  if (is_sociable($var)) {
      print("\$var is sociable\n");
  } else {
      print("\$var is not sociable\n");
  }

$id = is_lockable(VARIABLE)

is_lockable checks if the specified variable is lockable or not. If lockable, returns the variable's internal ID (similar to refaddr()). Otherwise, returns undef.

  if (is_lockable($var)) {
      print("\$var is lockable\n");
  } else {
      print("\$var is not lockable\n");
  }

sociable_discard(VARIABLE)

Explicitly destroys the specified sociable variable, returning its resources to the sociable pool to be recycled for any future sociable variable creation. Removes the "magic" from the private proxy version of the variable and clears it, so that any future attempt to reference the variable will use the existing private version. Likewise, any subsequent reference the sociable in another thread will remove the magic from that thread's private proxy.

If sociable_enable_warnings() has been called, any subsequent reference to the sociable in any thread will also cause a Perl warning to be generated.

sociable_enable_debug()

sociable_disable_debug()

Enable/disable copious diagnostic messages from Thread::Sociable internals. Only useful if Thread::Sociable has been compiled with the SOCIABLE_DEBUG preprocessor symbol defined.

sociable_enable_warnings()

sociable_disable_warnings()

Enable/disable generation of Perl warnings on attempts to reference a sociable that has been discarded.

($extent, $himark) = sociable_default_extent()

($extent, $himark) = sociable_default_extent($new_extent)

($extent, $himark) = sociable_default_extent($new_extent, $new_himark)

Read or set the global default extent values. The first form simply reads the current values. The second form sets the default extent to the specified value, and sets the himark to max(10, $new_extent * 1.1). The last form sets both the default extent and himark to the specified values. If $new_himark is less than $new_extent, the specified value is ignored, and the himark is set to max(10, $new_extent * 1.1). Setting extent to zero turns off the extent optimization behavior. These default values are applied to sociable hashes and arrays when they are created; the values of individual variables may then be altered using the sociable_extent() method. The initial default values are (10, 20).

($extent, $himark) = sociable_extent(@array)

($extent, $himark) = sociable_extent(%hash)

($extent, $himark) = sociable_extent(@array, $new_extent)

($extent, $himark) = sociable_extent(%hash, $new_extent)

($extent, $himark) = sociable_extent(@array, $new_extent, $new_himark)

($extent, $himark) = sociable_extent(%hash, $new_extent, $new_himark)

Read or set the extent values on the specified array or hash. The first form simply reads the current values. The second form sets the extent to the specified value, and sets the himark to max(10, $new_extent * 1.1). The last form sets both the extent and himark to the specified values. If $new_himark is less than $new_extent, the specified value is ignored, and the himark is set to max(10, $new_extent * 1.1). If the specified hash or array is not sociable, the operation is silently ignored, unless the sociable_enable_warnings() is in effect, in which case a warning will be generated. Setting extent to zero turns off the extent optimization behavior.

sociable_begin_work(\&xaction_sub [, onCommit => \&commit_sub ] [, onRestart => \&restart_sub ] [, onRollback => \&rollback_sub ])

Execute &xaction_sub in a STM transaction. All sociable variable references executed within &xaction_sub will be performed as "dirty" reads, and logged writes, with a final reconcile and commit applied upon return from &xaction_sub. At commit time, if any read-first variable's sequence number has changed since its initial read, or any written variable cannot be acquired for write, the transaction will be restarted; otherwise, all logged write operations will be applied. If any fatal error occurs within &xaction_sub, the transaction will be rolled back and not restarted. Rollback can be initiated by any general Perl error, including by explicitly executing a die (or croak) statement.

During commit processing, if an onCommit closure is provided, it will be called after the reconcile step has completed without causing restart, but before any writes are committed. An onCommit closure may induce restart or rollback.

At any time during the transaction, if a restart is required, any provided onRestart closure will be called after any acquired variables are released, to permit the application to perform any private state cleanup. onRestart closures may induce rollback.

Likewise, if a rollback is initiated, any provided onRollback closure will be called after any acquired variables are released, with the associated die message passed as a parameter.

$oldlimit = sociable_stm_limit( [ $newlimit ] )

Adjust the maximum permitted number of concurrent transactions. Thread::Sociable maintains a pre-allocated pool of transaction context structures in order to avoid memory faults in the event of thread death or other logic errors. The number of contexts created in the pool is determined by this limit. If a new transaction is started, and no free transaction context is available, the transaction treats the condition as a restart with some random backoff period. This limit can be used to throttle the amount of contention. The default limit is 100. If the $newlimit argument is omitted, the current limit is returned; otherwise, contexts are added or removed to satisfy the $newlimit, and the $oldlimit value is returned. Note that, if $newlimit is less than the current number of inuse contexts, the in-use contexts will not be discarded until they are freed.

$oldtimeout = sociable_stm_timeout( [ $newtimeout ] )

Adjust the maximum lifetime of a transaction. In future, Thread::Sociable will provide a transaction lifetime monitor to kill transactions that have not completed within this timeout (in order to detect and remove hung threads). If the $newtimeout argument is omitted, the current timeout limit is returned; otherwise, the timeout is set to $newtimeout, and the previous timeout value is returned. The timeout values have seconds precision; the default value is 1 minute. A value of zero indicates no timeout (i.e., infinite transaction lifetime permitted).

lock(VARIABLE)

cond_wait(VARIABLE)

cond_wait(CONDVAR, LOCKVAR)

cond_timedwait(VARIABLE, ABS_TIMEOUT)

cond_timedwait(CONDVAR, ABS_TIMEOUT, LOCKVAR)

cond_signal(VARIABLE)

cond_broadcast(VARIABLE)

These methods are identical to their threads::shared counterparts, except

OBJECTS ^

Thread::Sociable exports a version of bless() that works on shared objects such that blessings propagate across threads.

  # Create a shared 'foo' object
  my $foo;
  socialize($foo);
  $foo = &socialize({});
  bless($foo, 'foo');

  # Create a shared 'bar' object
  my $bar;
  socialize($bar);
  $bar = &socialize({});
  bless($bar, 'bar');

  # Put 'bar' inside 'foo'
  $foo->{'bar'} = $bar;

  # Rebless the objects via a thread
  threads->create(sub {
      # Rebless the outer object
      bless($foo, 'yin');

      # Cannot directly rebless the inner object
      #bless($foo->{'bar'}, 'yang');

      # Retrieve and rebless the inner object
      my $obj = $foo->{'bar'};
      bless($obj, 'yang');
      $foo->{'bar'} = $obj;

  })->join();

  print(ref($foo),          "\n");    # Prints 'yin'
  print(ref($foo->{'bar'}), "\n");    # Prints 'yang'
  print(ref($bar),          "\n");    # Also prints 'yang'

NOTES ^

Thread::Sociable is designed to disable itself silently if threads are not available. If you want access to threads, you must use threads before you use Thread::Sociable.

Using Sociably tie()'d Variables ^

A future release will permit multiple threads to sociably tie a single variable. In that scenario, when a thread writes to the variable, a "multicast" operation is performed, wherein the STORE operation to applied to all the tying threads.

Read operations are a bit more problematic, due to the need to merge the FETCHed values from multiple threads into a single return value. In order to address this need, Thread::Sociable uses the read context to determine how to return the result. If a tied variable is read in list context, a hash mapping thread ID's to returned values is returned; in scalar context, only the returned value of the last responding thread is returned.

Applications using sociably tied variables must be vigilant regarding locking schemes: if a thread initiating a STORE/FETCH operation holds a lock which must be acquired by any of the tying threads, deadlock will result, and the STORE/FETCH operation will hang.

As noted in the following section, operations on sociably tied variables are not restartable/abortable inside STM transactions. However, as for thread-private variable, it may be possible to provide some level of recoverability via the onRestart/onRollback closures.

Using STM ^

Currently, STM is only supported on x86, x64, SPARC, and PowerPC CPU platforms (due to the availability of spinlock code). Support for other CPUs may be possible, assuming appropriate spinlock code is provided (patches welcome).

An STM transaction will be restarted if any of the following occur during the transaction:

Restart and rollback cannot rollback any external effects of, or changes to, thread-private variables or I/O operations. E.g., if an application writes to a file during a transaction which later restarts or rolls back, the file write operation is not rolled back. Applications can, however, provide logic to perform such rollbacks via the onRestart and onRollback closure arguments.

Threads spawned within a transaction do not inherit the transaction state.

Processes forked within a transaction will inherit any current transactions within the current threads; however, as the new process's address space is completely separate from parent process, the transactions of one process do not effect the transactions of another.

onCommit closures may induce either restart or rollback by executing an appropriate die. onRestart closures may induce rollback by executing a die. onRollback closures cannot induce a restart.

Attempting to open a new transaction in onCommit, onRestart, or onRollback closures will generate a fatal error. Any access to sociable variables within those closures will not be transactional, and will reference the sociable value, not any existing private transactional value.

Transactions may be nested; however,

As each pending transaction is committed, restarted, or rolled back, any associated onCmmit, onRestart, or onRollback closures will be invoked.

If a transaction's onCommit closure induces restart or rollback, or if a transaction's onRestart closure induces rollback, the onRestart or onRollback closures of all pending transactions will be executed, including

Note that certain pathological constructs should be avoided, e.g., consider

    sub lots_of_pending_xactions {
        for (1..1000) {
            sociable_begin_work(sub {
                #
                # do some stuff
                #
                },
                onRestart => sub {
                #
                #    do some restart stuff
                #
                },
                onRollback => sub {
                #
                #    do some rollback stuff
                #
                }
            );
        }
    }

    sociable_begin_work(sub {
        lots_of_pending_xactions()
        #
        #    do some stuff
        #
        }
    );

This code would cause up to 1000 inner transactions to be completed, but not committed. More importantly, each iteration would add an additional onRestart and onRollback closure to the transaction log; if a restart or rollback occurs, each of the registered closure instances will be executed.

In such cases, applications should use a more conservative strategy if possible, e.g.

    sub lots_of_pending_xactions {
        my $already_registered = 0;
        for (1..1000) {
            sociable_begin_work(sub {
                #
                # do some stuff
                #
                },
                onRestart => $already_registered ?
                    sub {
                    #
                    #    do some restart stuff
                    #
                    } : undef,
                onRollback =>  $already_registered ?
                    sub {
                    #
                    #    do some rollback stuff
                    #
                    } : undef
            );
            $already_registered = 1;
        }
    }

This modification results in only adding a single instance of the closures.

Certain sociable objects may be safely used with the classic locking/critical section technique while inside an STM transaction (notably, TSDQ's used for TSA interfaces). In general, however, mingling classic locking with STM is probably a very bad idea.

Operations on sociably tie()'d variables are not logged within a transaction, and therefore cannot be restarted/rolled back.

Transactional access to arrays or hashes is problematic: some applications may only access a few elements of a given array or hash, while others may iterate over the entire array or hash. The former scenario is reasonably well tolerated within an STM, with a restart probability comparable to accessing simple scalars. The latter scenario, however, can lead to restart starvation for large arrays or hashes which are subject to frequent updates, with significant heap thrashing and spinlocking congestion. STM users are advised to keep these issues in mind, and seek alternative designs where possible. Performing large splices on arrays, especially with a replacement list, is a particularly "heavyweight" operation within a transaction, and should be avoided if possible.

Currently, creation of sociable variables within a transaction is not transactional, i.e., if the transaction restarts or rolls back, the sociable variable will not be discarded until an explicit sociable_discard() call.

Creating or accessing references to sociable variables is not considered transactional access; an access is only logged on a read or write of a sociable variable's contents.

Passing a sociable reference to, or receiving a sociable reference from, another thread (e.g., via a TSDQ operation) does not convey any transactional context to the recipient/sender. Subsequent accesses to the sociable variable within the transaction will be logged as usual, but no notification is provided to the sender/receiver upon commit/restart/rollback, nor is any "two phase commit" protocol provided to coordinate such activities of communicationg threads.

Two different conflict detection protocols are supported:

Eager

Eager detection attempts to detect conflicts on each access to sociable variables during the transaction, and will initiate restart as early as possible. This protocol may be preferable in highly contentions environments by avoiding additional STM operations which would later be discarded due to restart at commit.

Lazy

Lazy detection ignores conflicts until commit. In so doing, the overhead of variable access during the transaction is reduced, which may be preferable in minimal contention environments with short transactions.

Likewise, two commit protocols are supported:

Locked

Locked commit first acquires a global mutex (not a spinlock) before acquiring the spinlocks of all accessed sociable variables. Once the spinlocks are acquired (or a conflict is detected), the mutex is released, and commit (or rollback) can proceed. The mutex is required to avoid deadlock between concurrently committing transactions which may attempt to spinlock the same variables, but in different orders. By requiring the mutex, only one transaction at a time can ever hold more than 1 spinlock at a time. However, the mutex may impede progress of non-overlapping transactions.

Lockfree

Prior to acquiring spinlocks, Lockfree commit first applies a sort to the addresses of the accessed sociable variables. By acquiring spinlocks in a sorted order, competing commits are guaranteed to avoid deadlock. For very large transactions, the sort overhead may be significant; however, other non-overlapping transactions are not impeded (as for the Locked commit).

The detection protocol is set using the sociable_stm_eager() and sociable_stm_lazy() methods; sociable_stm_locked() and sociable_stm_lockfree() are used to select the commit protocol. Note that the conflict and commit protocol settings are global, i.e., changing the protocol in any thread changes it for all threads (as the complementary protocols are not compatible).

The current protocol settings can be determined by calling sociable_stm_protocol(), which return a hashref with keys Commits (set to either "locked" or "lockfree"), and Conflicts (set to either "eager" or "lazy").

The advantages of any protocol combination are not as yet empirically known. Locked commit may help avoid restarts for long duration transactions by avoiding competition for spinlocks at commit time; but highly concurrent environments using small non-overlapping transactions may benefit from the ability to concurrently commit.

A lazy copy algorithm is used to acquire array or hash contents. Individual elements of a structure are only copied from the sociable version when they are first referenced, thereby avoiding significant copy overhead for transactions which only reference a few elements. However, this approach may be detrimental for transactions which iterate over an entire array or hash (due to possibly increased the probability of restarts). A future release may provide selectable lazy or eager copy support.

STM Implementation Details ^

Thread::Sociable currently uses invisible reads, with selectable eager or lazy conflict detection, and selectable locked or lockfree commits (default is eager, lockfree). Eager conflict detection is implemented using a modified "Polka" Contention Manager dubbed "Sociable Contention Manager":

The Sociable contention manager helps avoid livelock* with minimal access overhead. Sociable CM approximates the "Polka" Contention Manager described in http://www.cs.rochester.edu/u/scott/papers/2005_PODC_CM.pdf, which indicates that Polka provides near optimal contention management across various types of contention.

The Sociable Contention Manager differs from the Polka implementation in that

* Livelock occurs when some threads' (usually long-duration) transactions are repeatedly restarted due to conflicts created by other thread's (usually short duration) transactions modifying variables referenced by the livelocked transaction.

BUGS AND LIMITATIONS ^

When socialize is used on arrays, hashes, array refs or hash refs, any data they contain will be lost.

  my @arr = qw(foo bar baz);
  socialize(@arr);
  # @arr is now empty (i.e., == ());

  # Create a 'foo' object
  my $foo = { 'data' => 99 };
  bless($foo, 'foo');

  # Share the object
  socialize($foo);        # Contents are now wiped out
  print("ERROR: \$foo is empty\n")
      if (! exists($foo->{'data'}));

Therefore, populate such variables after declaring them as sociable. (Scalar and scalar refs are not affected by this problem.)

Care must be exersized when socializing an object. Unless the class itself has been written to support socialization, an object's destructor may get called multiple times, once for each thread's scope exit. Additionally, the contents of hash or array based objects will be lost (due to the above limitation) if the object is socialized after its members have been populated.

Due to significant performance impact, taking refs of individual array or hash elements (e.g., $x = \$ary[34]; $y = \$hash{SomeKey};) is not fully supported. While the reference may be returned, it will not be a reference to a sociable value, and any future dereference through such references will not apply to the sociable values, but only to the thread local copy of the data. A future release will support a Refable attribute and associated sociable_refable() method to declare arrays and hashes that will return shared references for elements, with an associated performance impact.

slice() of a sociable array or hash does not produce a sociable variable. A future Refable support may add support for sociable slices.

Taking references to the elements of sociable arrays and hashes does not autovivify the elements, and neither does slicing a sociable array/hash over non-existent indices/keys autovivify the elements.

socialize() allows you to socialize($hashref->{key}) without giving any error message. But the $hashref->{key} is not sociable, causing the error "locking can only be used on sociable values" to occur when you attempt to lock($hasref->{key}).

Mixing lock() and STM is probably a very, very, very bad idea.

View existing bug reports at, and submit any new bugs, problems, patches, etc. to: http://rt.cpan.org/NoAuth/Bugs.html?Dist=Thread-Sociable

Benchmarks ^

A set of simple benchmark test scripts (included in the ./bench directory) were executed against both this version of Thread::Sociable and threads::shared 1.03. The following table provides the results for various operations, using the specified number of threads on the specified platforms:

Single 2.4 GHz, WinXP, AS Perl 5.8.6

Single 100MHz SPARC, Solaris 10, Perl 5.8.6

Single 1.5GHz PPC, OS X 10.4.9, Perl 5.8.6

Dual 3GHz Xeon, Win2000 Server, AS Perl 5.8.8

Dual 3GHz Xeon, Linux, Perl 5.8.8

Dual 3GHz Xeon, Solaris 10, Perl 5.8.8

Quad 700MHz Xeon, Win2000 Server, AS Perl 5.8.8

TO DO ^

IPC Support

Thread::Sociable could be extended to provide an interprocess shared variable solution by replacing the CRT heap with a shared memory heap manager.

SEE ALSO ^

Thread::Sociable::DuplexQueue

Thread::Sociable::Apartment

threads::shared

threads

perlthrtut

Some spinlock code was borrowed from Parrot, which also provides an STM implementation.

Perl threads mailing list: http://lists.cpan.org/showlist.cgi?name=iThreads

For in-depth information regarding, STM, I recommend the University of Rochester Synchronization Group's Publication list at http://www.cs.rochester.edu/research/synchronization/pubs.shtml.

AUTHOR ^

Dean Arnold mailto:darnold@presicient.com

Based on threads::shared by

Artur Bergman <sky AT crucially DOT net>

and its CPAN version produced by Jerry D. Hedden <jdhedden AT cpan DOT org>.

Permission is granted to use this software under the same terms as Perl itself. Refer to the Perl Artistic License for details.