Read/write lock

Serializing threads with read/write locks

EVL provides process-local read/write locks for serializing threads accessing a shared resource from out-of-band context, with multiple-reader, single writer semantics. Up to 2^30 threads may take a read-side lock concurrently, which should be, well, enough at the moment.

The implementation is biased towards writers on acquiring such lock, since a waiting writer shall be granted access before any new reader subsequently joining the wait, even if the lock is owned by some reader at the time of the request. In other words, write-starvation cannot happen as a result of more threads indefinitely joining the pool of current readers. Thread priority is otherwise enforced among waiters for gaining access to a free lock.

The implementation relies on the assumption that operations are perfectly paired and matched by callers (read_lock -> read_unlock, write_lock -> write_unlock). Such assumption is not verified by the API.

Threads serializing with read/write locks may be subject to priority inversion, there is no priority inheritance mechanism involved. This may create situations where lock owners who got delayed as a result of preemption may exclude others from the critical section for an undefined amount of time, particularly when writers are delayed by preempted readers.

Read/write locks are suitable for sharing read-mostly data in the following use cases:

  • all readers and writers have the same scheduling priority.

  • writers might have higher priority than readers, but the former would not mind being delayed by readers. For instance, some algorithms which need to update a shared data structure lazily may be fine waiting for readers to be quiescent before applying the change.

A plain mutex with PI enabled may perform better than a read/write lock in other cases.

Read/write locking services


int evl_create_rwlock(struct evl_rwlock *rwlock)

This call creates a process-local read/write lock. Internally, such lock relies on a flag group reader/writer threads might have to wait on when appropriate.

  • rwlock

    An in-memory rwlock descriptor is constructed by evl_create_rwlock(), which contains ancillary information other calls will need. rwlock is a pointer to such descriptor of type struct evl_rwlock.

  • evl_create_rwlock() returns zero on success. Otherwise, a negated error code might be returned among those defined by evl_create_flags(). The error codes which may apply in this set are:

    • -EMFILE The per-process limit on the number of open file descriptors has been reached.

    • -ENFILE The system-wide limit on the total number of open files has been reached.

    • -ENOMEM No memory available.

    • -ENXIO The EVL library is not initialized for the current process. Such initialization happens implicitly when evl_attach_self() is called by any thread of your process, or by explicitly calling evl_init(). You have to bootstrap the library services in a way or another before creating a read/write lock.

    #include <evl/rwlock.h>
    
    static struct evl_rwlock rwlock;
    
    void create_new_rwlock(void)
    {
    	int ret;
    
    	/* Create a (process-local) read/write lock. */
    	ret = evl_create_rwlock(&rwlock);
    	if (ret)
    		panic("failed with code %d", ret);
    }
    

    int evl_new_rwlock(struct evl_rwlock *rwlock)

    This call is a plain alias to evl_create_rwlock().


    EVL_RWLOCK_INITIALIZER()

    The static initializer you can use with read/write locks.

    Static initialization involves lazy binding to the EVL core, which causes an implicit call to evl_create_rwlock() to be issued upon the first attempt to acquire the lock, which in turn may trigger a transition to the in-band execution mode for the caller.

    /* Initialize a (process-local) read/write lock. */
    static struct evl_rwlock some_rwlock = EVL_RWLOCK_INITIALIZER();
    

    DEFINE_EVL_RWLOCK(name)

    A macro helper which defines a read/write lock variable using the static initializer EVL_RWLOCK_INITIALIZER().

  • name

    The name of the read/write lock variable to define.

  • /* Define a (process-local) read/write lock. */
    static DEFINE_EVL_RWLOCK(some_rwlock);
    /* Which is the same as defining: */
    static struct evl_rwlock some_rwlock = EVL_RWLOCK_INITIALIZER();
    

    int evl_destroy_rwlock(struct evl_rwlock *rwlock)

    Delete the read/write lock unconditionally. Threads sleeping on this lock at the time of the call would receive the -EIDRM error from evl_lock_read() or evl_lock_write().

  • rwlock

    The descriptor of the read/write lock to delete.

  • This call returns zero on success, or -EINVAL if the lock descriptor is invalid.


    int evl_lock_read(struct evl_rwlock *rwlock)

    Get a read-side lock, waiting for any active writer to drop it if present. Multiple readers may acquire the same lock concurrently. However, writers have to wait for acquiring that lock as long as at least one reader holds it. Requests to acquire the lock may be nested, provided each successful call to evl_lock_read() is paired with a call to evl_unlock_read() to release the lock.

    For the sake of sanity, a thread sleeping on a read/write lock cannot be unblocked by a call to evl_unblock_thread().

  • rwlock

    The descriptor of the read/write lock to acquire.

  • This call returns zero on success, or -EINVAL if the lock descriptor is invalid.


    int evl_trylock_read(struct evl_rwlock *rwlock)

    Like evl_lock_read(), but does not wait if the request cannot be satisfied immediately. Requests to acquire the lock may be nested, provided each successful call to evl_trylock_read() is paired with a call to evl_unlock_read() to release the lock.

  • rwlock

    The descriptor of the read/write lock to acquire.

  • This call returns zero on success acquiring the read-side lock, otherwise:

    -EAGAIN if the lock was unavailable on entry.

    -EINVAL if the lock descriptor is invalid.


    int evl_unlock_read(struct evl_rwlock *rwlock)

    Release a read-side lock previously acquired by a successful call to evl_lock_read() or evl_trylock_read().

  • rwlock

    The descriptor of the read/write lock to release.

  • This call returns zero on success, or -EINVAL if the lock descriptor is invalid.


    int evl_lock_write(struct evl_rwlock *rwlock)

    Get a write-side lock, waiting until no more reader/writer holds it if necessary. A single writer may acquire a given lock at any point in time, preventing all other threads from acquiring that lock. A successful call to evl_lock_write() must be paired with a call to evl_unlock_write() to release the lock.

    For the sake of sanity, a thread sleeping on a read/write lock cannot be unblocked by a call to evl_unblock_thread().

  • rwlock

    The descriptor of the read/write lock to acquire.

  • This call returns zero on success, or -EINVAL if the lock descriptor is invalid.


    int evl_trylock_write(struct evl_rwlock *rwlock)

    Like evl_lock_write(), but does not wait if the request cannot be satisfied immediately. A successful call to evl_trylock_write() must be paired with a call to evl_unlock_write() to release the lock.

  • rwlock

    The descriptor of the read/write lock to acquire.

  • This call returns zero on success acquiring the write-side lock, otherwise:

    -EAGAIN if the lock was unavailable on entry.

    -EINVAL if the lock descriptor is invalid.


    int evl_unlock_write(struct evl_rwlock *rwlock)

    Release a write-side lock previously acquired by a successful call to evl_lock_write() or evl_trylock_write().

  • rwlock

    The descriptor of the read/write lock to release.

  • This call returns zero on success, or -EINVAL if the lock descriptor is invalid.


    Last modified: Fri, 04 Aug 2023 11:25:12 +0200