Description | Enhanced replacement for threads::shared |
tie()
'd VariablesThread::Sociable - Less restrictive replacement for threads::shared
0.10
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"; } );
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
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.
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:
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).
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).
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.
Despite these inconveniences, Thread::Sociable provides some benefits (aside from the performance improvment):
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.
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()
SupportThread::Sociable supports array splicing; threads::shared does not.
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:
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.
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.
tie()
SupportThread::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.
Thread::Sociable provides an STM implementation (currently supporting x86, x64, SPARC, and PowerPC platforms) to simplify development of concurrent applications.
The following variable attributes are provided:
Sociable
creates the specified VARIABLE, and applies socialize()
to it. Likewise,
Lockable
creates the specified VARIABLE, and applies socialize_with_lock()
to it.
Note that all these methods are exported into the use
'ing package.
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;
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"); }
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"); }
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.
Enable/disable copious diagnostic messages from Thread::Sociable internals. Only useful if Thread::Sociable has been compiled with the SOCIABLE_DEBUG preprocessor symbol defined.
Enable/disable generation of Perl warnings on attempts to reference a sociable that has been discarded.
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).
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.
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.
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.
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).
These methods are identical to their threads::shared counterparts, except
lock($array[23]);
) is not currently supported.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'
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
.
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.
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:
scalar @array
or $#array
reference.keys %hash
reference.die
's with the message
"Thread::Sociable STM RESTART".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
onCommit
or onRestart
closuresNote 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 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 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 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.
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.
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":
my $a : Sociable; my $b : Sociable; my @ary : Sociable; $a = \@ary; $b = \@ary; sociable_begin_work( sub { $a->[1] = 20; $b->[2] = 30; if ($a->[2] == 30) { print "proper update"; } else { print "improper update"; } } );
@ary
-
is made. The dereference of the private copy will then create a (shallow) private copy
of @ary. Then the reference to $b creates a private copy of its contents
- again, a reference to @ary
-, and the dereference would cause another private
copy of @ary. With 2 different copies of @ary, the conditional would indicate
"improper copy". Keeping an additional map of the private copy of the referent,
keyed by the sociable version's address, assures that the updates are applied to
the same private version.
yield()
is executed to provide a pseudo-random
restart backoff delay, which various studies indicate improve performance/reduce
contention. While such studies usually rely on exponential backoff spinlocks, yield()
should provide a similar delay mechanism that automatically adapts to the processing
load of the system.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
threads-
yield()> to inject a pseudorandom
delay, which frees CPU resources for other threads to execute.* 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.
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
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:
Thread::Sociable could be extended to provide an interprocess shared variable solution by replacing the CRT heap with a shared memory heap manager.
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.
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.