0

Short question: Does the POSIX thread API offer me a way to determine if the calling thread already holds a particular lock?

Long question:

Suppose I want to protect a data structure with a lock. Acquiring and releasing the lock need to happen in different functions. Calls between the functions involved are rather complex (I am adding multithreading to a 16-year-old code base). For example:

  • do_dangerous_stuff() does things for which the current thread needs to hold the write lock. Therefore, I acquire the lock at the beginning of the function and release it at the end, as the caller does not necessarily hold the lock.
  • Another function, do_extra_dangerous_stuff(), calls do_dangerous_stuff(). However, before the call it already does things which also require a write lock, and the data is not consistent until the call to do_dangerous_stuff() returns (so releasing and immediately re-acquiring the lock might break things).

In reality it is more complicated than that. There may be a bunch of functions calling do_dangerous_stuff(), and requiring each of them to obtain the lock before calling do_dangerous_stuff() may be impractical. Sometimes the lock is acquired in one function and released in another.

With a read lock, I could just acquire it multiple times from the same thread, as long as I make sure I release the same number of lock instances that I have acquired. For a write lock, this is not an option (attempting to do so will result in a deadlock). An easy solution would be: test if the current thread already holds the lock and acquire it if not, and conversely, test if the current thread still holds the lock and release it if it does. However, that requires me to test if the current thread already holds the lock—is there a way to do that?

user149408
  • 5,385
  • 4
  • 33
  • 69

2 Answers2

1

Looking at the man page for pthread_rwlock_wrlock(), I see it says:

If successful, the pthread_rwlock_wrlock() function shall return zero; otherwise, an error number shall be returned to indicate the error.

[…]

The pthread_rwlock_wrlock() function may fail if:

EDEADLK The current thread already owns the read-write lock for writing or reading.

As I read it, EDEADLK is never used to indicate chains involving multiple threads waiting for each other’s resources (and from my observations, such deadlocks indeed seem to result in a freeze rather than EDEADLK). It seems to indicate exclusively that the thread is requesting a resource already held by the current thread, which is the condition I want to test for.

If I have misunderstood the documentation, please let me know. Otherwise the solution would be to simply call pthread_rwlock_wrlock(). One of the following should happen:

  • It blocks because another thread holds the resource. When we get to run again, we will hold the lock. Business as usual.
  • It returns zero (success) because we have just acquired the lock (which we didn’t hold before). Business as usual.
  • It returns EDEADLK because we are already holding the lock. No need to reacquire, but we might want to consider this when we release the lock—that depends on the code in question, see below
  • It returns some other error, indicating something has truly gone wrong. Same as with every other lock operation.

It may make sense to keep track of the number of times we have acquired the lock and got EDEADLK. Taking from Gil Hamilton’s answer, the lock depth would work for us:

  • Reset the lock depth to 0 when we have acquired a lock.
  • Increase the lock depth by 1 each time we get EDEADLK.
  • Match each attempt to acquire the lock with the following: If lock depth is 0, release the lock. Else decrease the lock depth.

This should be thread-safe without further synchronization, as the lock depth is effectively protected by the lock it refers to (we touch it only while holding the lock).

Caveat: if the current thread already holds the lock for reading (and no others do), this will also report it as being “already locked”. Further tests will be needed to determine if the currently held lock is indeed a write lock. If multiple threads, among them the current one, hold the read lock, I do not know if attempting to obtain a write lock will return EDEADLK or freeze the thread. This part needs some more work…

Community
  • 1
  • 1
user149408
  • 5,385
  • 4
  • 33
  • 69
0

AFAIK, there's no easy way to accomplish what you're trying to do.

In linux, you can use the "recursive" mutex attribute to achieve your purpose (as shown here for example: https://stackoverflow.com/a/7963765/1076479), but this is not "posix"-portable.

The only really portable solution is to roll your own equivalent. You can do that by creating a data structure containing a lock along with your own thread index (or equivalent) and an ownership/recursion count.

CAVEAT: Pseudo-code off the top of my head

Recursive lock:

// First try to acquire the lock without blocking...
if ((err = pthread_mutex_trylock(&mystruct.mutex)) == 0) {
    // Acquire succeeded. I now own the lock. 
    // (I either just acquired it or already owned it)
    assert(mystruct.owner == my_thread_index || mystruct.lock_depth == 0);
    mystruct.owner = my_thread_index;
    ++mystruct.lock_depth;
} else if (mystruct.owner == my_thread_index) {
    assert(err == EBUSY);
    // I already owned the lock. Now one level deeper
    ++mystruct.lock_depth;
} else {
    // I don't own the lock: block waiting for it.
    pthread_mutex_lock(&mystruct.mutex);
    assert(mystruct.lock_depth == 0);
}

On the way out, it's simpler, because you know you own the lock, you only need to determine whether it's time to release it (i.e. the last unlock). Recursive unlock:

if (--mystruct.lock_depth == 0) {
    assert(mystruct.owner == my_thread_index);
    // Last level of recursion unwound
    mystruct.owner = -1;    // Mark it un-owned
    pthread_mutex_unlock(&mystruct.mutex);
}

I would want to add some additional checks and assertions and significant testing before trusting this too.

Gil Hamilton
  • 11,973
  • 28
  • 51