Skip to content

Commit a1be2f8

Browse files
Add condition variable wrapper (#17)
Co-authored-by: Jonatan Kłosko <jonatanklosko@gmail.com>
1 parent edadd7d commit a1be2f8

5 files changed

Lines changed: 208 additions & 4 deletions

File tree

README.md

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -524,10 +524,14 @@ can be called from multiple Erlang processes simultaneously, leading to race
524524
conditions. While C++ provides synchronization mechanisms, these are unknown to
525525
Erlang and cannot take advantage of tools like *lock checker* or *lcnt*.
526526
527-
Fine provides analogues to `std::mutex` and `std::shared_mutex`, respectively
528-
called `fine::Mutex` and `fine::SharedMutex`. Those are compatible with the
529-
standard mutex wrappers, such as `std::unique_lock` and `std::shared_lock`.
530-
For example:
527+
Fine provides analogues to `std::mutex`, `std::shared_mutex`, and
528+
`std::condition_variable`, respectively called `fine::Mutex`,
529+
`fine::SharedMutex`, and `fine::ConditionVariable`. All of their implementations
530+
are provided in the `<fine/sync.h>` header.
531+
532+
533+
`fine::Mutex` and `fine::SharedMutex` are compatible with `std::unique_lock`
534+
and `std::shared_lock`. For example:
531535
532536
```c++
533537
#include <fine/sync.hpp>
@@ -571,6 +575,33 @@ const char* my_object__name(struct my_object*);
571575
572576
fine::SharedMutex my_object_rwlock("my_lib", "my_object", my_object__name(my_object));
573577
```
578+
579+
Fine also provides `fine::ConditionVariable` with an API similar to
580+
`std::condition_variable`:
581+
582+
```c++
583+
bool notified = false;
584+
fine::Mutex mutex;
585+
fine::ConditionVariable cond;
586+
587+
std::thread t1([&]() {
588+
auto lock = std::unique_lock{mutex};
589+
cond.wait(lock, []() { return notified; });
590+
});
591+
592+
std::thread t2([&]() {
593+
{
594+
auto lock = std::unique_lock{mutex};
595+
notified = true;
596+
}
597+
598+
cond.notify_all();
599+
});
600+
601+
t2.join();
602+
t1.join();
603+
```
604+
574605
## Allocators
575606
576607
For compatibility with the STL, fine supports stateless allocators when

c_include/fine/sync.hpp

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,112 @@ class SharedMutex final {
194194
};
195195
std::unique_ptr<ErlNifRWLock, Deleter> m_handle;
196196
};
197+
198+
// Condition variable. Used when threads must wait for a specific
199+
// condition to appear before continuing execution. Condition
200+
// variables must be used with associated mutexes.
201+
class ConditionVariable final {
202+
public:
203+
// Creates a condition variable.
204+
ConditionVariable() : m_handle{enif_cond_create(nullptr)} {
205+
if (!m_handle) {
206+
throw std::runtime_error("failed to create cond");
207+
}
208+
}
209+
210+
// Creates a ConditionVariable from an ErlNifCond handle.
211+
explicit ConditionVariable(ErlNifCond *handle) : m_handle{handle} {}
212+
213+
// Creates a condition variable.
214+
//
215+
// `name` is a string identifying the created condition variable. It is used
216+
// to identify the condition variable in planned future debug functionality.
217+
explicit ConditionVariable(const char *name)
218+
: m_handle{enif_cond_create(const_cast<char *>(name))} {
219+
if (!m_handle) {
220+
throw std::runtime_error("failed to create cond");
221+
}
222+
}
223+
224+
// Creates a condition variable.
225+
//
226+
// `name` is a string identifying the created condition variable. It is used
227+
// to identify the condition variable in planned future debug functionality.
228+
explicit ConditionVariable(const std::string &name)
229+
: m_handle{enif_cond_create(const_cast<char *>(name.c_str()))} {
230+
if (!m_handle) {
231+
throw std::runtime_error("failed to create cond");
232+
}
233+
}
234+
235+
// Converts this ConditionVariable to a ErlNifConditionVariable handle.
236+
//
237+
// Ownership still belongs to this instance.
238+
operator ErlNifCond *() const & noexcept { return m_handle.get(); }
239+
240+
// Releases ownership of the ErlNifCond handle to the caller.
241+
//
242+
// This operation is only possible by:
243+
// ```
244+
// static_cast<ErlNifCond*>(std::move(rwlock))
245+
// ```
246+
explicit operator ErlNifCond *() && noexcept { return m_handle.release(); }
247+
248+
// Broadcasts on this condition variable. That is, if other threads are
249+
// waiting on the condition variable being broadcast on, all of them are
250+
// woken.
251+
//
252+
// This function is thread-safe.
253+
void notify_all() noexcept { enif_cond_broadcast(m_handle.get()); }
254+
255+
// Signals on a condition variable. That is, if other threads are waiting on
256+
// the condition variable being signaled, one of them is woken.
257+
//
258+
// This function is thread-safe.
259+
void notify_one() noexcept { enif_cond_signal(m_handle.get()); }
260+
261+
// Prefer the use of `wait(std::unique_lock<Mutex>&, Predicate)` over this
262+
// function.
263+
//
264+
// Waits on a condition variable. The calling thread is blocked until another
265+
// thread wakes it by signaling or broadcasting on the condition variable.
266+
// Before the calling thread is blocked, it unlocks the mutex passed as
267+
// argument. When the calling thread is woken, it locks the same mutex before
268+
// returning. That is, the mutex currently must be locked by the calling
269+
// thread when calling this function.
270+
//
271+
// `wait` can return even if no one has signaled or broadcast on the condition
272+
// variable. Code calling `wait` is always to be prepared for `wait` returning
273+
// even if the condition that the thread was waiting for has not occurred.
274+
// That is, when returning from `wait`, always check if the condition has
275+
// occurred, and if not call `wait` again.
276+
//
277+
// This function is thread-safe.
278+
void wait(std::unique_lock<Mutex> &lock) noexcept {
279+
enif_cond_wait(m_handle.get(), *lock.mutex());
280+
}
281+
282+
// Waits on a condition variable. The calling thread is blocked until another
283+
// thread wakes it by signaling or broadcasting on the condition variable.
284+
// Before the calling thread is blocked, it unlocks the mutex passed as
285+
// argument. When the calling thread is woken, it locks the same mutex before
286+
// returning. That is, the mutex currently must be locked by the calling
287+
// thread when calling this function.
288+
//
289+
// This function is thread-safe.
290+
template <typename Predicate>
291+
void wait(std::unique_lock<Mutex> &lock, Predicate pred) {
292+
while (!pred()) {
293+
enif_cond_wait(m_handle.get(), *lock.mutex());
294+
}
295+
}
296+
297+
private:
298+
struct Deleter {
299+
void operator()(ErlNifCond *handle) noexcept { enif_cond_destroy(handle); }
300+
};
301+
std::unique_ptr<ErlNifCond, Deleter> m_handle;
302+
};
197303
} // namespace fine
198304

199305
#endif

test/c_src/finest.cpp

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#include <atomic>
12
#include <cstring>
23
#include <exception>
34
#include <functional>
@@ -422,6 +423,64 @@ std::nullopt_t shared_mutex_shared_lock_test(ErlNifEnv *) {
422423
}
423424
FINE_NIF(shared_mutex_shared_lock_test, 0);
424425

426+
class ResetEvent {
427+
public:
428+
explicit ResetEvent(const bool signaled = false) noexcept
429+
: m_signaled{signaled} {}
430+
431+
void set() {
432+
{
433+
auto lock = std::unique_lock{m_mutex};
434+
m_signaled = true;
435+
}
436+
437+
m_cond.notify_one();
438+
}
439+
440+
void reset() {
441+
auto lock = std::unique_lock{m_mutex};
442+
m_signaled = false;
443+
}
444+
445+
void wait() {
446+
auto lock = std::unique_lock{m_mutex};
447+
m_cond.wait(lock, [&] { return m_signaled; });
448+
m_signaled = false;
449+
}
450+
451+
private:
452+
bool m_signaled;
453+
fine::Mutex m_mutex;
454+
fine::ConditionVariable m_cond;
455+
};
456+
457+
std::nullopt_t condition_variable_test(ErlNifEnv *) {
458+
ResetEvent event{true};
459+
event.reset();
460+
461+
std::thread wait_thread_1([&] {
462+
event.wait();
463+
event.set();
464+
});
465+
std::thread wait_thread_2([&] {
466+
event.wait();
467+
event.set();
468+
});
469+
std::thread wait_thread_3([&] {
470+
event.wait();
471+
event.set();
472+
});
473+
474+
event.set();
475+
476+
wait_thread_1.join();
477+
wait_thread_2.join();
478+
wait_thread_3.join();
479+
480+
return std::nullopt;
481+
}
482+
FINE_NIF(condition_variable_test, 0);
483+
425484
bool compare_eq(ErlNifEnv *, fine::Term lhs, fine::Term rhs) noexcept {
426485
return lhs == rhs;
427486
}

test/lib/finest/nif.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ defmodule Finest.NIF do
7070
def shared_mutex_unique_lock_test(), do: err!()
7171
def shared_mutex_shared_lock_test(), do: err!()
7272

73+
def condition_variable_test(), do: err!()
74+
7375
def compare_eq(_lhs, _rhs), do: err!()
7476
def compare_ne(_lhs, _rhs), do: err!()
7577
def compare_lt(_lhs, _rhs), do: err!()

test/test/finest_test.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,12 @@ defmodule FinestTest do
451451
end
452452
end
453453

454+
describe "condition_variable" do
455+
test "condition_variable" do
456+
NIF.condition_variable_test()
457+
end
458+
end
459+
454460
describe "comparison" do
455461
test "equal" do
456462
refute NIF.compare_eq(64, 42)

0 commit comments

Comments
 (0)