Skip to content

Commit 06ec1fd

Browse files
committed
[libc][darwin] add basic mutex support for darwin
This patch adds a basic implementation/wrapper for mutex using the underlying os_unfair_lock API that macos provides.
1 parent d281b97 commit 06ec1fd

File tree

5 files changed

+233
-1
lines changed

5 files changed

+233
-1
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
if(NOT TARGET libc.src.__support.OSUtil.osutil)
2+
return()
3+
endif()
4+
5+
add_header_library(
6+
mutex
7+
HDRS
8+
mutex.h
9+
DEPENDS
10+
libc.src.__support.threads.mutex_common
11+
)
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//===--- Implementation of a Darwin mutex class ------------------*- C++
2+
//-*-===//
3+
//
4+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
5+
// See https://llvm.org/LICENSE.txt for license information.
6+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7+
//
8+
//===----------------------------------------------------------------------===//
9+
10+
#ifndef LLVM_LIBC_SRC___SUPPORT_THREADS_DARWIN_MUTEX_H
11+
#define LLVM_LIBC_SRC___SUPPORT_THREADS_DARWIN_MUTEX_H
12+
13+
#include "src/__support/libc_assert.h"
14+
#include "src/__support/macros/config.h"
15+
#include "src/__support/threads/mutex_common.h"
16+
#include "src/__support/threads/sleep.h" // For sleep_briefly
17+
#include "src/__support/time/linux/abs_timeout.h"
18+
19+
#include <mach/mach_init.h> // For mach_thread_self
20+
#include <mach/mach_port.h> // For mach_port_t and MACH_PORT_NULL
21+
#include <os/lock.h> // For os_unfair_lock
22+
#include <time.h> // For clock_gettime
23+
24+
namespace LIBC_NAMESPACE_DECL {
25+
26+
// This file is an implementation of `LIBC_NAMESPACE::mutex` for Darwin-based
27+
// platforms. It is a wrapper around `os_unfair_lock`, which is a low-level,
28+
// high-performance locking primitive provided by the kernel.
29+
//
30+
// `os_unfair_lock` is a non-recursive, thread-owned lock that blocks waiters
31+
// efficiently in the kernel. As the name implies, it is "unfair," meaning
32+
// it does not guarantee the order in which waiting threads acquire the lock.
33+
// This trade-off allows for higher performance in contended scenarios.
34+
//
35+
// The lock must be unlocked from the same thread that locked it. Attempting
36+
// to unlock from a different thread will result in a runtime error.
37+
//
38+
// This implementation is suitable for simple critical sections where fairness
39+
// and reentrancy are not concerns.
40+
41+
class Mutex final {
42+
os_unfair_lock_s lock_val = OS_UNFAIR_LOCK_INIT;
43+
mach_port_t owner = MACH_PORT_NULL;
44+
45+
// API compatibility fields.
46+
unsigned char timed;
47+
unsigned char recursive;
48+
unsigned char robust;
49+
unsigned char pshared;
50+
51+
public:
52+
LIBC_INLINE constexpr Mutex(bool is_timed, bool is_recursive, bool is_robust,
53+
bool is_pshared)
54+
: owner(MACH_PORT_NULL), timed(is_timed), recursive(is_recursive),
55+
robust(is_robust), pshared(is_pshared) {}
56+
57+
LIBC_INLINE constexpr Mutex()
58+
: owner(MACH_PORT_NULL), timed(0), recursive(0), robust(0), pshared(0) {}
59+
60+
LIBC_INLINE static MutexError init(Mutex *mutex, bool is_timed, bool is_recur,
61+
bool is_robust, bool is_pshared) {
62+
mutex->lock_val = OS_UNFAIR_LOCK_INIT;
63+
mutex->owner = MACH_PORT_NULL;
64+
mutex->timed = is_timed;
65+
mutex->recursive = is_recur;
66+
mutex->robust = is_robust;
67+
mutex->pshared = is_pshared;
68+
return MutexError::NONE;
69+
}
70+
71+
LIBC_INLINE static MutexError destroy(Mutex *lock) {
72+
LIBC_ASSERT(lock->owner == MACH_PORT_NULL &&
73+
"Mutex destroyed while locked.");
74+
return MutexError::NONE;
75+
}
76+
77+
LIBC_INLINE MutexError lock() {
78+
os_unfair_lock_lock(&lock_val);
79+
owner = mach_thread_self();
80+
return MutexError::NONE;
81+
}
82+
83+
LIBC_INLINE MutexError timed_lock(internal::AbsTimeout abs_time) {
84+
while (true) {
85+
if (try_lock() == MutexError::NONE) {
86+
return MutexError::NONE;
87+
}
88+
89+
// Manually check if the timeout has expired.
90+
struct timespec now;
91+
// The clock used here must match the clock used to create the
92+
// absolute timeout.
93+
clock_gettime(abs_time.is_realtime() ? CLOCK_REALTIME : CLOCK_MONOTONIC,
94+
&now);
95+
const timespec &target_ts = abs_time.get_timespec();
96+
97+
if (now.tv_sec > target_ts.tv_sec || (now.tv_sec == target_ts.tv_sec &&
98+
now.tv_nsec >= target_ts.tv_nsec)) {
99+
// We might have acquired the lock between the last try_lock() and now.
100+
// To avoid returning TIMEOUT incorrectly, we do one last try_lock().
101+
if (try_lock() == MutexError::NONE)
102+
return MutexError::NONE;
103+
return MutexError::TIMEOUT;
104+
}
105+
106+
sleep_briefly();
107+
}
108+
}
109+
110+
LIBC_INLINE MutexError unlock() {
111+
// This check is crucial. It prevents both double-unlocks and unlocks
112+
// by threads that do not own the mutex.
113+
if (owner != mach_thread_self()) {
114+
return MutexError::UNLOCK_WITHOUT_LOCK;
115+
}
116+
owner = MACH_PORT_NULL;
117+
os_unfair_lock_unlock(&lock_val);
118+
return MutexError::NONE;
119+
}
120+
121+
LIBC_INLINE MutexError try_lock() {
122+
if (os_unfair_lock_trylock(&lock_val)) {
123+
owner = mach_thread_self();
124+
return MutexError::NONE;
125+
}
126+
return MutexError::BUSY;
127+
}
128+
};
129+
130+
} // namespace LIBC_NAMESPACE_DECL
131+
132+
#endif // LLVM_LIBC_SRC___SUPPORT_THREADS_DARWIN_MUTEX_H

libc/src/__support/threads/mutex.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242

4343
#if defined(__linux__)
4444
#include "src/__support/threads/linux/mutex.h"
45-
#endif // __linux__
45+
#elif defined(__APPLE__)
46+
#include "src/__support/threads/darwin/mutex.h"
47+
#endif
4648

4749
#elif LIBC_THREAD_MODE == LIBC_THREAD_MODE_SINGLE
4850

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
add_libc_test(
2+
mutex_test
3+
SUITE
4+
libc-support-threads-tests
5+
SRCS
6+
mutex_test.cpp
7+
DEPENDS
8+
libc.src.__support.threads.darwin.mutex
9+
)
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//===-- Unittests for Darwin's Mutex ------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#include "src/__support/threads/darwin/mutex.h"
10+
#include "src/__support/threads/mutex_common.h"
11+
#include "test/UnitTest/Test.h"
12+
13+
TEST(LlvmLibcSupportThreadsMutexTest, SmokeTest) {
14+
LIBC_NAMESPACE::Mutex mutex;
15+
ASSERT_EQ(mutex.lock(), LIBC_NAMESPACE::MutexError::NONE);
16+
ASSERT_EQ(mutex.unlock(), LIBC_NAMESPACE::MutexError::NONE);
17+
ASSERT_EQ(mutex.try_lock(), LIBC_NAMESPACE::MutexError::NONE);
18+
ASSERT_EQ(mutex.try_lock(), LIBC_NAMESPACE::MutexError::BUSY);
19+
ASSERT_EQ(mutex.unlock(), LIBC_NAMESPACE::MutexError::NONE);
20+
ASSERT_EQ(mutex.unlock(), LIBC_NAMESPACE::MutexError::UNLOCK_WITHOUT_LOCK);
21+
}
22+
23+
// TEST(LlvmLibcSupportThreadsRawMutexTest, Timeout) {
24+
// LIBC_NAMESPACE::RawMutex mutex;
25+
// ASSERT_TRUE(mutex.lock());
26+
// timespec ts;
27+
// LIBC_NAMESPACE::internal::clock_gettime(CLOCK_MONOTONIC, &ts);
28+
// ts.tv_sec += 1;
29+
// // Timeout will be respected when deadlock happens.
30+
// auto timeout = LIBC_NAMESPACE::internal::AbsTimeout::from_timespec(ts,
31+
// false); ASSERT_TRUE(timeout.has_value());
32+
// // The following will timeout
33+
// ASSERT_FALSE(mutex.lock(*timeout));
34+
// ASSERT_TRUE(mutex.unlock());
35+
// // Test that the mutex works after the timeout.
36+
// ASSERT_TRUE(mutex.lock());
37+
// ASSERT_TRUE(mutex.unlock());
38+
// // If a lock can be acquired directly, expired timeout will not count.
39+
// // Notice that the timeout is already reached during preivous deadlock.
40+
// ASSERT_TRUE(mutex.lock(*timeout));
41+
// ASSERT_TRUE(mutex.unlock());
42+
// }
43+
//
44+
// TEST(LlvmLibcSupportThreadsRawMutexTest, PSharedLock) {
45+
// struct SharedData {
46+
// LIBC_NAMESPACE::RawMutex mutex;
47+
// LIBC_NAMESPACE::cpp::Atomic<size_t> finished;
48+
// int data;
49+
// };
50+
// void *addr =
51+
// LIBC_NAMESPACE::mmap(nullptr, sizeof(SharedData), PROT_READ |
52+
// PROT_WRITE,
53+
// MAP_ANONYMOUS | MAP_SHARED, -1, 0);
54+
// ASSERT_NE(addr, MAP_FAILED);
55+
// auto *shared = reinterpret_cast<SharedData *>(addr);
56+
// shared->data = 0;
57+
// LIBC_NAMESPACE::RawMutex::init(&shared->mutex);
58+
// // Avoid pull in our own implementation of pthread_t.
59+
// #ifdef SYS_fork
60+
// long pid = LIBC_NAMESPACE::syscall_impl<long>(SYS_fork);
61+
// #elif defined(SYS_clone)
62+
// long pid = LIBC_NAMESPACE::syscall_impl<long>(SYS_clone, SIGCHLD, 0);
63+
// #endif
64+
// for (int i = 0; i < 10000; ++i) {
65+
// shared->mutex.lock(LIBC_NAMESPACE::cpp::nullopt, true);
66+
// shared->data++;
67+
// shared->mutex.unlock(true);
68+
// }
69+
// // Mark the thread as finished.
70+
// shared->finished.fetch_add(1);
71+
// // let the child exit early to avoid output pollution
72+
// if (pid == 0)
73+
// LIBC_NAMESPACE::exit(0);
74+
// while (shared->finished.load() != 2)
75+
// LIBC_NAMESPACE::sleep_briefly();
76+
// ASSERT_EQ(shared->data, 20000);
77+
// LIBC_NAMESPACE::munmap(addr, sizeof(SharedData));
78+
// }

0 commit comments

Comments
 (0)