The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Thread::Resource::RWLock - read/write lock base class for Perl ithreads

SYNOPSIS

        package LockedObject;

        use threads;
        use threads::shared;
        use Thread::Queue::Queueable;
        use Thread::Resource::RWLock;

        use base qw(Thread::Queue::Queueable Thread::Resource::RWLock);

        sub new {
                my $class = shift;

                my %obj : shared = ();

                my $self = bless \%obj, $class;
        #
        #       init the locking members
        #
                $self->Thread::Resource::RWLock::adorn();
                return $self;
        }

        sub redeem {
                my ($class, $self);

                return bless $self, $class;
        }

        package main;
        use threads;
        use threads::shared;
        use Thread::Queue::Duplex;
        use LockedObject;
        #
        #       in threaded app:
        #
        my $read_write = LockedObject->new();
        my $tqd = Thread::Queue::Duplex->new();
        my $thrdA = threads->new(\&read_thread, $tqd);
        my $thrdB = threads->new(\&write_thread, $tqd);
        #
        # pass the shared object to each thread
        #
        $tqd->enqueue_and_wait($read_write);
        $tqd->enqueue_and_wait($read_write);

        # Reader
        sub read_thread {
                my $tqd = shift;
                my $request = $tqd->dequeue();
                $tqd->respond($request->[0], 1);
                my $obj = $request->[1];

                my $locktoken = $obj->read_lock();
        #
        #       do some stuff
        #
                $obj->unlock($locktoken);
        }

        # Writer
        sub write_thread {
                my $tqd = shift;
                my $request = $tqd->dequeue();
                $tqd->respond($request->[0], 1);
                my $obj = $request->[1];
        #
        #       first grab a readlock
        #
                my $locktoken = $obj->read_lock();
        #
        #       do some stuff, then upgrade to a writelock
        #
                $obj->write_lock();
        #
        #       do some stuff, then unlock
        #
                $obj->unlock($locktoken);
        }

DESCRIPTION

Thread::Resource::RWLock provides both an inheritable abstract class, as well as a concrete object implementation, to regulate concurrent access to resources. Multiple concurrent reader threads may hold a Thread::Resource::RWLock readlock at the same time, while a single writer thread holds the lock exclusively.

New reader threads are blocked if any writer is currently waiting to obtain the lock. The read lock is granted after all pending write lock requests have been released.

Thread::Resource::RWLock accomodates a thread which already holds a lock and then requests another lock on the resource, as follows:

no lock held, requests readlock

Lock is granted when any pending writelock requests are applied, and then released. Returned value is a unique locktoken value.

no lock held, requests writelock

Lock is granted when any current readlocks are released. If multiple writelock requests are pending, the writelock will be granted in a random fashion. Returned value is a unique locktoken value.

holds readlock, requests readlock

The lock level remains the same, but the returned value is -1, indicating a lock was already held.

holds readlock, requests writelock

The lock level is upgraded to write when all other readers have unlocked, and the returned value is -1, indicating a lock was already held.

holds writelock, requests readlock

The lock level is downgraded to read, regardless if any other writelock requests are pending. The returned value is -1, indicating a lock was already held.

holds writelock, requests writelock

The lock level remains the same, but the returned value is -1, indicating a lock was already held.

In addition, both nonblocking and timed interfaces are provided to permit acquiring a lock only if the lock can be granted immediately, or within a specified number of seconds. If the lock is not granted, the returned value is undef.

This implementation provides 2 constructors: the usual new() method which constructs a shared object instance, suitable for use as a member of a shared object, and an adorn() method for classes which subclass Thread::Resource::RWLock.

Finally, note that this implementation supports both array and hash based objects. Array-based subclasses should reserve the first 4 entries in their array for the Thread::Resource::RWLock member variables.

Locks Do Not Accumulate

The application is responsible for tracking and preserving lock consistency when it repeatedly requests locks on a resource for which it already holds locks. In support of this, Thread::Resource::RWLock's lock methods return a positive locktoken value when the lock is initially granted (the timestamp returned by Time::HiRes::time()), and returns -1 when a thread is granted a lock on a resource on which it already holds a lock.

The unlock() method takes a single (optional) $locktoken parameter. If the $locktoken matches the locktoken returned when the thread was originally locked, then the lock will be released; otherwise, the unlock() is ignored, and the lock will continue to be held. If no $locktoken parameter is provided, then the unlock is applied unconditionally.

METHODS

adorn

Adorns the input resource object with Thread::Resource::RWLock object member variables in an unlocked state.

new

Creates a new concrete instance of an unlocked Thread::Resource::RWLock object.

$locktoken = $resource->read_lock()

Requests a read lock. If another thread currently holds a writelock on the resource, read_lock blocks until all pending writelock requests have been released. If the requesting thread holds a writelock on the resource, the lock is downgraded to a readlock, without granting the writelock to any pending requestors. Returned value is Time::HiRes::time() if the requestor did not already hold a lock on the resource, or -1 if it did.

$locktoken = $resource->read_lock_nb()

Same as read_lock(), except it returns immediately without granting the readlock if the resource is currently writelocked by another thread. Returns undef if the lock cannot be granted immediately, Time::HiRes::time() if the lock is granted and the requestor did not already hold a lock on the resource, or -1 if it did hold a lock.

$locktoken = $resource->read_lock_timed ($timeout)

Same as read_lock(), except it returns undef if the readlock is not granted within $timeout seconds. Returns Time::HiRes::time() if the lock is granted and the requestor did not already hold a lock on the resource, or -1 if it did.

$locktoken = $resource->write_lock()

Requests a writelock on the resource. Writelocks are exclusive, so no other readers or writers are granted access until the writelock is released. Note that a thread may be granted the writelock if the resource is currently only readlocked by the requesting thread (i.e., the thread is requesting a lock upgrade). write_lock() blocks until the lock is available. Returns Time::HiRes::time() if the lock is granted and the requestor did not already hold a lock on the resource, or -1 if it did hold a lock.

$locktoken = $resource->write_lock_nb()

Same as write_lock(), but returns undef immediately if the writelock cannot be granted (i.e., another thread holds a read or write lock on the resource). Returns Time::HiRes::time() if the lock is granted and the requestor did not already hold a lock on the resource, or -1 if it did hold a lock.

$locktoken = $resource->write_lock_timed($timeout)

Same as write_lock(), but returns undef if the write lock cannot be granted within $timeout seconds Returns Time::HiRes::time() if the lock is granted and the requestor did not already hold a lock on the resource, or -1 if it did hold a lock.

$result = $resource->unlock( [ $locktoken ] )

Releases a lock held by the requesting thread. If a $locktoken is provided, it must match the original token returned when the requesting thread was granted the lock. If $locktoken is not provided, the lock is released unconditionally. $result is 1 if the lock is released, or undef if the lock is retained.

CAVEATS

Differences from Thread::RWLock

Thread::Resource::RWLock provides a significantly different interface than Thread::RWLock. Most importantly, the latter uses the old Perl 5.005 Thread module, and depends on its locked method attribute. In addition, Thread::RWLock's interface

        - uses somewhat obscure method names (up_read, down_write, etc.)

        - does not support lock upgrades and downgrades

        - hence, can lead to deadlock, if a thread holding
                a readlock attempts to upgrade to a writelock,
                or attempts to downgrade to a readlock from a writelock

        - accumulates readlocks from the same thread, thereby
                requiring multple unlock() calls to completely
                release a resource which has been repeatedly readlocked

        - does not support a subclassing capability
Starvation

Due to the ability to upgrade/downgrade locks, it is possible for starvation to occur, wherein a thread waiting on a write lock may be indefinitely blocked while another thread repeatedly upgrades, then downgrades its lock without ever releasing the lock. Use of lock upgrade/downgrade should be applied judiciously.

Multiple readers concurrently attempting to upgrade to writelocks can also induce deadlock (since the readlocker count will never drop to 1). A future release may provide an upgrade queue to handle this case.

Zone Threading

Applications using Thread::Apartment to support zone threading (i.e., multiple objects installed in a single apartment thread) may need to implement extra locking functionality if the objects within the thread are sharing the same resource in read and write modes, as Thread::Resource::RWLock relies on the current TID (via threads::tid()) to disambiguate lockers of the same resource. If all objects within the thread are using only readlocks, there should be no impact. However, multiple objects using write locks, or attempting upgrades or downgrades of locks, may cause unexpected behavior, including deadlock or indeterminate values. Therefore, best practice would be to segregate resource writers in their own apartment thread. A future implementation may provide a Thread::Resource::Locker interface which Thread::Apartment objects can implement to disambiguate co-resident zone threaded objects.

Context Accumulation

In the event a thread holding a lock exits without explicitly unlock()'ing, the lock will be retained until the resource object is DESTROY'ed, resulting in dead context accumulation, deadlock, and/or starvation. A future release may inject an occassional timer event to verify lock holders are still running.

SEE ALSO

threads

threads::shared

Thread::RWLock

Thread::Semaphore

AUTHOR AND COPYRIGHT

Copyright (c) 2005 Dean Arnold, Presicient Corp, USA. All rights reserved.

Permission to use and redistirbute this software is granted under the same terms as Perl itself; refer to perlartistic for license details.