Skip to content

Commit b5df12d

Browse files
authored
Add user space locking primitives via futex (#203)
1 parent 0a8cf28 commit b5df12d

8 files changed

Lines changed: 677 additions & 0 deletions

File tree

core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ set(PUBLIC_HEADERS
2020
include/join/mutex.hpp
2121
include/join/condition.hpp
2222
include/join/semaphore.hpp
23+
include/join/futex.hpp
2324
include/join/thread.hpp
2425
include/join/threadpool.hpp
2526
include/join/macaddress.hpp

core/include/join/futex.hpp

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
/**
2+
* MIT License
3+
*
4+
* Copyright (c) 2026 Mathieu Rabine
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
#ifndef JOIN_CORE_FUTEX_HPP
26+
#define JOIN_CORE_FUTEX_HPP
27+
28+
// libjoin.
29+
#include <join/mutex.hpp>
30+
31+
// Linux.
32+
#include <sys/syscall.h>
33+
#include <linux/futex.h>
34+
#include <unistd.h>
35+
36+
// C++.
37+
#include <atomic>
38+
39+
namespace join
40+
{
41+
/**
42+
* @brief Futex-based mutex for intra-process locking.
43+
*/
44+
class Futex
45+
{
46+
public:
47+
/**
48+
* @brief default constructor.
49+
*/
50+
Futex () noexcept
51+
: _futex (0)
52+
{
53+
}
54+
55+
/**
56+
* @brief copy constructor.
57+
* @param other other object to copy.
58+
*/
59+
Futex (const Futex& other) = delete;
60+
61+
/**
62+
* @brief copy assignment.
63+
* @param other other object to copy.
64+
* @return a reference to the current object.
65+
*/
66+
Futex& operator= (const Futex& other) = delete;
67+
68+
/**
69+
* @brief move constructor.
70+
* @param other other object to move.
71+
*/
72+
Futex (Futex&& other) = delete;
73+
74+
/**
75+
* @brief move assignment.
76+
* @param other other object to move.
77+
* @return a reference to the current object.
78+
*/
79+
Futex& operator= (Futex&& other) = delete;
80+
81+
/**
82+
* @brief destructor.
83+
*/
84+
~Futex () = default;
85+
86+
/**
87+
* @brief lock the futex, blocking until it becomes available.
88+
*/
89+
void lock () noexcept
90+
{
91+
uint32_t expected = 0;
92+
if (_futex.compare_exchange_strong (expected, 1, std::memory_order_acquire, std::memory_order_relaxed))
93+
{
94+
return;
95+
}
96+
97+
do
98+
{
99+
if (expected == 2 ||
100+
_futex.compare_exchange_strong (expected, 2, std::memory_order_relaxed, std::memory_order_relaxed))
101+
{
102+
::syscall (SYS_futex, reinterpret_cast<uint32_t*> (&_futex), FUTEX_WAIT_PRIVATE, 2, nullptr,
103+
nullptr, 0);
104+
}
105+
expected = 0;
106+
}
107+
while (!_futex.compare_exchange_strong (expected, 2, std::memory_order_acquire, std::memory_order_relaxed));
108+
}
109+
110+
/**
111+
* @brief try to lock the futex without blocking.
112+
* @return true if the lock was acquired, false if it was already locked.
113+
*/
114+
bool tryLock () noexcept
115+
{
116+
uint32_t expected = 0;
117+
return _futex.compare_exchange_strong (expected, 1, std::memory_order_acquire, std::memory_order_relaxed);
118+
}
119+
120+
/**
121+
* @brief unlock the futex, waking one waiter if any.
122+
*/
123+
void unlock () noexcept
124+
{
125+
if (_futex.exchange (0, std::memory_order_release) == 2)
126+
{
127+
::syscall (SYS_futex, reinterpret_cast<uint32_t*> (&_futex), FUTEX_WAKE_PRIVATE, 1, nullptr, nullptr,
128+
0);
129+
}
130+
}
131+
132+
/**
133+
* @brief get a pointer to the underlying futex word.
134+
* @return pointer to the atomic futex word.
135+
*/
136+
std::atomic_uint32_t* handle () noexcept
137+
{
138+
return &_futex;
139+
}
140+
141+
private:
142+
/// Futex.
143+
alignas (4) std::atomic_uint32_t _futex;
144+
};
145+
146+
/**
147+
* @brief Futex-based mutex suitable for inter-process locking via shared memory.
148+
*/
149+
class SharedFutex
150+
{
151+
public:
152+
/**
153+
* @brief default constructor.
154+
*/
155+
SharedFutex () noexcept
156+
: _futex (0)
157+
{
158+
}
159+
160+
/**
161+
* @brief copy constructor.
162+
* @param other other object to copy.
163+
*/
164+
SharedFutex (const SharedFutex& other) = delete;
165+
166+
/**
167+
* @brief copy assignment.
168+
* @param other other object to copy.
169+
* @return a reference to the current object.
170+
*/
171+
SharedFutex& operator= (const SharedFutex& other) = delete;
172+
173+
/**
174+
* @brief move constructor.
175+
* @param other other object to move.
176+
*/
177+
SharedFutex (SharedFutex&& other) = delete;
178+
179+
/**
180+
* @brief move assignment.
181+
* @param other other object to move.
182+
* @return a reference to the current object.
183+
*/
184+
SharedFutex& operator= (SharedFutex&& other) = delete;
185+
186+
/**
187+
* @brief destructor.
188+
*/
189+
~SharedFutex () = default;
190+
191+
/**
192+
* @brief lock the futex, blocking until it becomes available.
193+
*/
194+
void lock () noexcept
195+
{
196+
uint32_t expected = 0;
197+
if (_futex.compare_exchange_strong (expected, 1, std::memory_order_acquire, std::memory_order_relaxed))
198+
{
199+
return;
200+
}
201+
202+
do
203+
{
204+
if (expected == 2 ||
205+
_futex.compare_exchange_strong (expected, 2, std::memory_order_relaxed, std::memory_order_relaxed))
206+
{
207+
::syscall (SYS_futex, reinterpret_cast<uint32_t*> (&_futex), FUTEX_WAIT, 2, nullptr, nullptr, 0);
208+
}
209+
expected = 0;
210+
}
211+
while (!_futex.compare_exchange_strong (expected, 2, std::memory_order_acquire, std::memory_order_relaxed));
212+
}
213+
214+
/**
215+
* @brief try to lock the futex without blocking.
216+
* @return true if the lock was acquired, false if it was already locked.
217+
*/
218+
bool tryLock () noexcept
219+
{
220+
uint32_t expected = 0;
221+
return _futex.compare_exchange_strong (expected, 1, std::memory_order_acquire, std::memory_order_relaxed);
222+
}
223+
224+
/**
225+
* @brief unlock the futex, waking one waiter if any.
226+
*/
227+
void unlock () noexcept
228+
{
229+
if (_futex.exchange (0, std::memory_order_release) == 2)
230+
{
231+
::syscall (SYS_futex, reinterpret_cast<uint32_t*> (&_futex), FUTEX_WAKE, 1, nullptr, nullptr, 0);
232+
}
233+
}
234+
235+
/**
236+
* @brief get a pointer to the underlying futex word.
237+
* @return pointer to the atomic futex word.
238+
*/
239+
std::atomic_uint32_t* handle () noexcept
240+
{
241+
return &_futex;
242+
}
243+
244+
private:
245+
/// Futex word: 0=unlocked, 1=locked, 2=locked+waiters.
246+
alignas (4) std::atomic_uint32_t _futex;
247+
};
248+
}
249+
250+
#endif

core/tests/CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,16 @@ target_link_libraries(sharedsemaphore.gtest ${JOIN_CORE} GTest::gtest_main rt)
147147
add_test(NAME sharedsemaphore.gtest COMMAND sharedsemaphore.gtest)
148148
install(TARGETS sharedsemaphore.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test)
149149

150+
add_executable(futex.gtest futex_test.cpp)
151+
target_link_libraries(futex.gtest ${JOIN_CORE} GTest::gtest_main)
152+
add_test(NAME futex.gtest COMMAND futex.gtest)
153+
install(TARGETS futex.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test)
154+
155+
add_executable(sharedfutex.gtest sharedfutex_test.cpp)
156+
target_link_libraries(sharedfutex.gtest ${JOIN_CORE} GTest::gtest_main)
157+
add_test(NAME sharedfutex.gtest COMMAND sharedfutex.gtest)
158+
install(TARGETS sharedfutex.gtest RUNTIME DESTINATION ${CMAKE_INSTALL_DATADIR}/${PROJECT_NAME}/test)
159+
150160
add_executable(thread.gtest thread_test.cpp)
151161
target_link_libraries(thread.gtest ${JOIN_CORE} GTest::gtest_main)
152162
add_test(NAME thread.gtest COMMAND thread.gtest)

core/tests/futex_test.cpp

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/**
2+
* MIT License
3+
*
4+
* Copyright (c) 2026 Mathieu Rabine
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
// libjoin.
26+
#include <join/futex.hpp>
27+
28+
// Libraries.
29+
#include <gtest/gtest.h>
30+
31+
// C++.
32+
#include <future>
33+
#include <thread>
34+
35+
using join::Futex;
36+
using join::ScopedLock;
37+
38+
using namespace std::chrono_literals;
39+
40+
/**
41+
* @brief test lock.
42+
*/
43+
TEST (Futex, lock)
44+
{
45+
Futex futex;
46+
auto task = std::async (std::launch::async, [&futex] () {
47+
futex.lock ();
48+
std::this_thread::sleep_for (15ms);
49+
futex.unlock ();
50+
});
51+
std::this_thread::sleep_for (5ms);
52+
auto beg = std::chrono::high_resolution_clock::now ();
53+
futex.lock ();
54+
auto end = std::chrono::high_resolution_clock::now ();
55+
EXPECT_GT (std::chrono::duration_cast<std::chrono::milliseconds> (end - beg), 5ms);
56+
futex.unlock ();
57+
task.wait ();
58+
}
59+
60+
/**
61+
* @brief test tryLock.
62+
*/
63+
TEST (Futex, tryLock)
64+
{
65+
Futex futex;
66+
auto task = std::async (std::launch::async, [&futex] () {
67+
futex.lock ();
68+
std::this_thread::sleep_for (15ms);
69+
futex.unlock ();
70+
});
71+
std::this_thread::sleep_for (5ms);
72+
EXPECT_FALSE (futex.tryLock ());
73+
std::this_thread::sleep_for (15ms);
74+
EXPECT_TRUE (futex.tryLock ());
75+
futex.unlock ();
76+
task.wait ();
77+
}
78+
79+
/**
80+
* @brief test scoped lock.
81+
*/
82+
TEST (Futex, scopedLock)
83+
{
84+
Futex futex;
85+
auto task = std::async (std::launch::async, [&futex] () {
86+
ScopedLock<Futex> lock (futex);
87+
std::this_thread::sleep_for (15ms);
88+
});
89+
std::this_thread::sleep_for (5ms);
90+
auto beg = std::chrono::high_resolution_clock::now ();
91+
futex.lock ();
92+
auto end = std::chrono::high_resolution_clock::now ();
93+
EXPECT_GT (std::chrono::duration_cast<std::chrono::milliseconds> (end - beg), 5ms);
94+
futex.unlock ();
95+
task.wait ();
96+
}
97+
98+
/**
99+
* @brief test handle.
100+
*/
101+
TEST (Futex, handle)
102+
{
103+
Futex futex;
104+
EXPECT_NE (futex.handle (), nullptr);
105+
}
106+
107+
/**
108+
* @brief main function.
109+
*/
110+
int main (int argc, char** argv)
111+
{
112+
testing::InitGoogleTest (&argc, argv);
113+
return RUN_ALL_TESTS ();
114+
}

0 commit comments

Comments
 (0)