Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement a recursive RWLock class and synchronization primitive. #91682

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions core/core_bind.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,41 @@ void Mutex::_bind_methods() {
ClassDB::bind_method(D_METHOD("unlock"), &Mutex::unlock);
}

////// RecurisveRWLock //////

void RWLock::read_lock() const {
rwlock.read_lock();
}

bool RWLock::read_try_lock() const {
return rwlock.read_try_lock();
}

void RWLock::read_unlock() const {
rwlock.read_unlock();
}

void RWLock::write_lock() {
rwlock.write_lock();
}

bool RWLock::write_try_lock() {
return rwlock.write_try_lock();
}

void RWLock::write_unlock() {
rwlock.write_unlock();
}

void RWLock::_bind_methods() {
ClassDB::bind_method(D_METHOD("read_lock"), &RWLock::read_lock);
ClassDB::bind_method(D_METHOD("read_try_lock"), &RWLock::read_try_lock);
ClassDB::bind_method(D_METHOD("read_unlock"), &RWLock::read_unlock);
ClassDB::bind_method(D_METHOD("write_lock"), &RWLock::write_lock);
ClassDB::bind_method(D_METHOD("write_try_lock"), &RWLock::write_try_lock);
ClassDB::bind_method(D_METHOD("write_unlock"), &RWLock::write_unlock);
}

////// Thread //////

void Thread::_start_func(void *ud) {
Expand Down
16 changes: 16 additions & 0 deletions core/core_bind.h
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,22 @@ class Mutex : public RefCounted {
void unlock();
};

class RWLock : public RefCounted {
GDCLASS(RWLock, RefCounted);
::RecursiveRWLock rwlock;

static void _bind_methods();

public:
void read_lock() const;
bool read_try_lock() const;
void read_unlock() const;

void write_lock();
bool write_try_lock();
void write_unlock();
};

class Semaphore : public RefCounted {
GDCLASS(Semaphore, RefCounted);
::Semaphore semaphore;
Expand Down
150 changes: 150 additions & 0 deletions core/os/rw_lock.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,21 @@
#ifndef RW_LOCK_H
#define RW_LOCK_H

#include "core/error/error_macros.h"
#include "core/templates/vector.h"
#include "core/typedefs.h"
#include "mutex.h"
#include "thread.h"

#ifdef MINGW_ENABLED
#define MINGW_STDTHREAD_REDUNDANCY_WARNING
#include "thirdparty/mingw-std-threads/mingw.condition_variable.h"
#include "thirdparty/mingw-std-threads/mingw.mutex.h"
#include "thirdparty/mingw-std-threads/mingw.shared_mutex.h"
#define THREADING_NAMESPACE mingw_stdthread
#else
#include <condition_variable>
#include <mutex>
#include <shared_mutex>
#define THREADING_NAMESPACE std
#endif
Expand Down Expand Up @@ -103,4 +111,146 @@ class RWLockWrite {
}
};

#ifdef THREADS_ENABLED

class RecursiveRWLock {
mutable THREADING_NAMESPACE::condition_variable cv;
mutable THREADING_NAMESPACE::mutex cv_mtx;

//These two are protected by mtx
mutable Vector<Thread::ID> thread_ids;
mutable Thread::ID waiting_thread_id;
mutable BinaryMutex mtx;

mutable Mutex exclusive_mutex;

void lock_exclusive_mutex() const {
if (!exclusive_mutex.try_lock()) { // The exclusive mutex lock is held by writing.
int read_lock_times;
{ // On fail, release all read locks and relock them to prevent deadlocks.
mtx.lock();
read_lock_times = thread_ids.count(waiting_thread_id);
for (int i = 0; i < read_lock_times; i++) {
thread_ids.erase(Thread::get_caller_id());
}
if (thread_ids.size() == thread_ids.count(waiting_thread_id)) {
std::lock_guard lock(cv_mtx);
cv.notify_all();
}
mtx.unlock();
}

exclusive_mutex.lock();
mtx.lock();
for (int i = 0; i < read_lock_times; i++) {
thread_ids.push_back(Thread::get_caller_id());
}
mtx.unlock();
}
}

public:
// Lock the RecursiveRWLock, block if locked by someone else.
_ALWAYS_INLINE_ void read_lock() const {
lock_exclusive_mutex();
mtx.lock();
exclusive_mutex.unlock();

thread_ids.append(Thread::get_caller_id());
mtx.unlock();
}

// Unlock the RecursiveRWLock, let other threads continue.
_ALWAYS_INLINE_ void read_unlock() const {
mtx.lock();
Vector<Thread::ID>::Size index = thread_ids.rfind(Thread::get_caller_id());
if (index != -1) {
thread_ids.remove_at(index);
} else {
ERR_PRINT("Attempt to read_unlock RecursiveRWLock while the thread hasn't locked it!");
}
if (thread_ids.size() == thread_ids.count(waiting_thread_id)) {
std::lock_guard lock(cv_mtx);
cv.notify_all();
}
mtx.unlock();
}

// Attempt to lock the RecursiveRWLock for reading. True on success, false means it can't lock.
_ALWAYS_INLINE_ bool read_try_lock() const {
if (exclusive_mutex.try_lock()) {
mtx.lock();
exclusive_mutex.unlock();

thread_ids.append(Thread::get_caller_id());
mtx.unlock();

return true;
}
return false;
}

// Lock the RecursiveRWLock, block if locked by someone else.
_ALWAYS_INLINE_ void write_lock() {
lock_exclusive_mutex();
mtx.lock();

waiting_thread_id = Thread::get_caller_id();

while (thread_ids.size() > thread_ids.count(Thread::get_caller_id())) {
std::unique_lock lock(cv_mtx);

mtx.unlock();
cv.wait(lock);
mtx.lock();
}
mtx.unlock();
}

// Unlock the RecursiveRWLock, let other threads continue.
_ALWAYS_INLINE_ void write_unlock() {
exclusive_mutex.unlock();
}

// Attempt to lock the RecursiveRWLock for writing. True on success, false means it can't lock.
_ALWAYS_INLINE_ bool write_try_lock() {
if (exclusive_mutex.try_lock()) {
mtx.lock();

if (thread_ids.size() != thread_ids.count(Thread::get_caller_id())) {
mtx.unlock();
exclusive_mutex.unlock();
return false;
}

mtx.unlock();
return true;
}
return false;
}
};
#else // No threads.

class RecursiveRWLock {
public:
// Lock the RecursiveRWLock, block if locked by someone else.
_ALWAYS_INLINE_ void read_lock() const {}

// Unlock the RecursiveRWLock, let other threads continue.
_ALWAYS_INLINE_ void read_unlock() const {}

// Attempt to lock the RecursiveRWLock for reading. True on success, false means it can't lock.
_ALWAYS_INLINE_ bool read_try_lock() const { return true; }

// Lock the RecursiveRWLock, block if locked by someone else.
_ALWAYS_INLINE_ void write_lock() {}

// Unlock the RecursiveRWLock, let other threads continue.
_ALWAYS_INLINE_ void write_unlock() {}

// Attempt to lock the RecursiveRWLock for writing. True on success, false means it can't lock.
_ALWAYS_INLINE_ bool write_try_lock() { return true; }
};
#endif

#endif // RW_LOCK_H
1 change: 1 addition & 0 deletions core/register_core_types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ void register_core_types() {
GDREGISTER_ABSTRACT_CLASS(DirAccess);
GDREGISTER_CLASS(core_bind::Thread);
GDREGISTER_CLASS(core_bind::Mutex);
GDREGISTER_CLASS(core_bind::RWLock);
GDREGISTER_CLASS(core_bind::Semaphore);

GDREGISTER_CLASS(XMLParser);
Expand Down
56 changes: 56 additions & 0 deletions doc/classes/RWLock.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="RWLock" inherits="RefCounted" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<brief_description>
Multithreading primitive for multi-read exclusive write access.
</brief_description>
<description>
Read write lock to allow better multi-threaded access.
Unlike [Mutex], [RWLock] allows you to have multiple threads reading the same data, while only one can modify them, and not while its being read.
The implementation allows for the same thread to write lock / read lock multiple times.
[b]Warning:[/b] RWLocks must be used carefully to avoid deadlocks.
[b]Warning:[/b] To ensure proper cleanup without crashes or deadlocks, the following conditions must be met:
- When a [RWLock]'s reference count reaches zero and it is therefore destroyed, no threads (including the one on which the destruction will happen) must have it locked.
- When a [Thread]'s reference count reaches zero and it is therefore destroyed, it must not have any RWLock locked.
See also [Mutex]
</description>
<tutorials>
</tutorials>
<methods>
<method name="read_lock" qualifiers="const">
<return type="void" />
<description>
Locks for reading data.
</description>
</method>
<method name="read_try_lock" qualifiers="const">
<return type="bool" />
<description>
Tries to lock for reading data, fails if the write_lock is being held.
</description>
</method>
<method name="read_unlock" qualifiers="const">
<return type="void" />
<description>
Unlock after reading data.
</description>
</method>
<method name="write_lock">
<return type="void" />
<description>
Locks for writing data. Waits for all read locks to finish (except the ones held by the calling thread), while making it unable to acquire a read lock during this.
</description>
</method>
<method name="write_try_lock">
<return type="bool" />
<description>
Tries to lock for writing data, fails if write_lock is being held or any thread except the calling thread holds a read lock.
</description>
</method>
<method name="write_unlock">
<return type="void" />
<description>
Unlock after writing data.
</description>
</method>
</methods>
</class>
Loading