Skip to content

Add macros for mutex use around libc functions that need them#24098

Draft
khwilliamson wants to merge 30 commits intoPerl:bleadfrom
khwilliamson:lock_definitions.h
Draft

Add macros for mutex use around libc functions that need them#24098
khwilliamson wants to merge 30 commits intoPerl:bleadfrom
khwilliamson:lock_definitions.h

Conversation

@khwilliamson
Copy link
Contributor

@khwilliamson khwilliamson commented Jan 18, 2026

This replaces #22283
This file is intended to insulate C code from thread issues, when used as directed.

This commit merely creates the file; it is not #included yet.

  • This set of changes requires a perldelta entry, and I will write it before merging this

@khwilliamson khwilliamson marked this pull request as draft January 18, 2026 04:31
@khwilliamson
Copy link
Contributor Author

@tonycoz wrote

Why does this generate:

#  error_foo_not suitable...

rather than

#  error foo_not suitable...

? The first may produce an error, but it might not include the text of the symbol.

The second will produce the requested message, though it might be better to quote it, which will prevent macro replacement, allowing you to avoid the need for all_the_underscores.

@khwilliamson
Copy link
Contributor Author

The answer is that it turns out that #error is not valid as an expansion of a #define. So, I got rid of the # and the symbol should show up including 'foo'

@khwilliamson
Copy link
Contributor Author

@tonycoz wrote:

Two issues, in general:

1. This defines a huge number of names most of which will never be used, and have a potential to conflict with names from other libraries.

I think this should add a PERL_ prefix to each lock name.

ok

2. This defines separate symbols for each function in a closely related group of functions, for example `dbm_*` and any read locale function.

The separate macros imply the need to do each of those locks, even though in some cases it doesn't make sense, eg. if I do a dbm_delete(), I really don't want to release the lock until I do dbm_error().

I think macros should be defined for groups of such functions where it makes sense, eg. a single macro for the dbm_* functions, and just document the need for LC_*_LOCK for locale reading functions etc.

I don't know what to do about this. First, for the casual reader, these all expand to no-ops except on a threaded build.

This commit doesn't have anything about how these locks are actually implemented. My proposed implementation, I believe avoids the possibility of deadlock, and doesn't introduce any new mutexes, which doing would make deadlock possible. These locks look like they have three possible mutexes, the existing ENV and LOCALE ones, plus a GENeric one. That one is mapped to either of the other two, depending on the Configuration. It turns out on systems with thread-safe locales, the LOCALE mutex is essentially unused, so is available for the generic one. Otherwise, that mutex can get used a lot, and so the ENV mutex is used instead, as we expect changes to the environment to be fairly rare. The ENV and LOCALE mutexes have co-existed for quite a few releases now without deadlock reports. Deadlock is avoided by always doing things in a particular order.

By using a common mutex for unrelated libc calls, threads block that wouldn't if there were a mutex for just the related libc calls. For example asctime needs to lock out threads that are simultaneously executing itself or ctime and nothing else.. But this implementation locks out threads executing a whole host of other libc calls, such as drand48. If there were a mutex for each possible group, you would find that there would be overlap, so that to fully protect such a group would require multiple mutexes, which greatly increases the likelihood of deadlock occurring. My belief is that the performance is going to be acceptable when the common mutex is just for a single libc call that returns quickly.

But if we use a common mutex for a series of operations, unlocking it at the end, threads using unrelated operations which use the same mutex are locked out for the duration. It would be better I think to have a new mutex for just the series. This would entail that the code using it be carefully crafted to not have deadlock potential.

And, then would there be a mutex for each type of series, like dbm and pwent'?

I do have text in the comments indicating there is an issue here. But maybe the solution is to just not generate locks for functions that are part of a series

@khwilliamson khwilliamson marked this pull request as ready for review January 18, 2026 17:20
@tonycoz
Copy link
Contributor

tonycoz commented Jan 20, 2026

My belief is that the performance is going to be acceptable when the common mutex is just for a single libc call that returns quickly.

The problem is that something like:

PERL_DBM_STORE_LOCK;
int status = dbm_store(db, key, value, DBM_INSERT);
PERL_DBM_STORE_UNLOCK;
if (status < 0) {
  PERL_DBM_ERROR_LOCK;
  printf("error %d\n", dbm_error(db));
  PERL_DBM_ERROR_UNLOCK;
}

is invalid, since other operations may have occurred in the gaps between locks to invalidate the stored error value.

Now the user could just lock everything they need in a block:

PERL_DBM_STORE_LOCK;
PERL_DBM_ERROR_LOCK;
int status = dbm_store(db, key, value, DBM_INSERT);
if (status < 0) {
  printf("error %d\n", dbm_error(db));
}
PERL_DBM_ERROR_UNLOCK;
PERL_DBM_STORE_UNLOCK;

but that's wasteful, and annoying.

Note that the extended span of the lock here is about guarding access to a working API, not just about preventing crashes that might occur if there were multiple concurrent dbm_store()s in progress.

@khwilliamson
Copy link
Contributor Author

I removed the locks for functions that need to be used in conjunction with other ones as an atomic unit

print $l <<EOT;
/* This file contains macros to wrap their respective libc uses to ensure that
* those uses are thread-safe in a multi-threaded environment. It is ordered
* alphabetically by fucntion name.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fucntion name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@tonycoz
Copy link
Contributor

tonycoz commented Feb 5, 2026

This commit merely creates the file; it is not #included yet.

I assume (hope?) it will be perl's headers including this.

In any case, I think lock_definitions.h should have a perl related name to avoid conflicts with headers from other sources.

Whether that's perl_locks,h, perl_lock_definitions.h I'm not too worried.

@khwilliamson khwilliamson changed the title Add lock_definitions.h Add macros for mutex use around libc functions that need them Feb 5, 2026
@khwilliamson
Copy link
Contributor Author

khwilliamson commented Feb 5, 2026

I have now added commits intended to complete this pull request. Previously, only the file lock_definitions.h was included. This #includes it in perl.h and creates two different implementations for them, and adds some usage for them in POSIX.xs. Still needed are changes to perlclib, perlapi, and perldelta to refer to these.

The intent is that an XS writer or perl core writer can wrap calls to libc functions with the appropriate macros without needing to understand the nuances, and get thread-safe operation without the possibility of deadlock

One implementation is for when there are thread-safe locales. In this case, an existing mutex is essentially unused, and can be repurposed. The other implementation works, with somewhat less performance, otherwise.

The file is not intended to be included directly into XS space, but the definitions are always there to anyone who includes perl.h

@khwilliamson khwilliamson force-pushed the lock_definitions.h branch 2 times, most recently from d08ee38 to 1a940d6 Compare February 5, 2026 23:27
This file is intended to insulate C code from thread issues, when used
as directed.

This commit merely creates the file; it is not #included yet.
These symbols aren't really documented, and we're not about to.  So move
them to the resolved list.
This is for debugging mutex locking/unlocking.  Convert some locale
debug statements that are really about locking to use this.

This is not compile in by default because of the overhead during
time-critical operations.  It requires -Accflags=-DPERL_DEBUG_MUTEXES
to be passed to Configure.  Its use is for very low level problems.

It is incompatible with being compiled to have mem log.  Putting that
logic in its definition simplifies some code a bit.
This is in preparation for the next commit when it otherwise would be
confusing.  The terminology is also changed to exclusive lock, as that
is clearer.
Make the unlock mate of its corresponding lock adjacent to it
This sets xcounter unconditionally, but now after the lock.  It's a
small thing, but I think it is slightly clearer
Perl has mutexes to prevent cooperating threads from colliding in
accessing shared resources.  This requires all threads to use these; a
rogue thread that doesn't follow the protocol can wreak havoc.  We can
make sure that the perl core fully cooperates, but an XS module could go
rogue.

A thread can declare that it accesses the resource only to query it, and
will not change it in any way.  Any number of threads can be accessing
the resource in this manner at the same time without fear of collisions.
We say that such threads have read-only access.

A thread can also declare that it will be changing the resource.  Only
one thread can safely be doing this at a time.  It has exclusive access.

Perl has macros to call to lock and unlock the resource for both types
of access.  While a thread has exclusive access, any other thread
wanting either type of access will hang until the first thread releases
it.  Conversely, if a thread wants exclusive access, it will hang until
all the threads that have read-only access release their locks.  Any
number of threads may have simultaneous read-only locks.

The macros allow a thread to lock a resource while already holding it.
They are called general semaphores or recursive locks.  Only when the
unlock matching the first lock is executed does the resource get
released.  Locking the rest of the system out from a resource should be
done for the shortest time possible to prevent bottlenecks.  A recursive
lock could encourage the bad habit of locking it longer than the minimum
possible.  But these locks have been added to existing code.  It would
be a tremendous amount of effort to change that code, hence the
recursiveness.

This commit temporarily removes a few DEBUG_K statements.  This is so
that the logic of the code can be seen without this distraction.  The
next commit adds them, and more like them, back in.

Commit 262c141 added two unused macros
for future use.  It turns out this was somewhat premature, because the
implementation was buggy (except they're not called).

This new commit fixes that, and the next few commits will finally start
to use these macros.

The problem was the interaction with exclusive locking attempts.  If a
thread owns an exclusive lock, and then recursively asks for a read one,
that should trivially succeed.  But instead it would hang, depending on
the system's locking implementation.  The solution is to add a
per-thread bookkeeping counter ourselves, so we bypass the system's in
this situation.

Conversely, an attempt to add an exclusive lock when already holding a
read lock was't handled properly.  I discovered via testing that this
can lead to deadlock (the comments added in this commit explain the
scenario).  This commit hence panics when such an attempt is made.  The
panic is to try to make sure that code that can trigger this doesn't get
shipped.
Now that we have a general reentrant read-only lock, use it.  This
prepares for future commits
A lock by this name has existed for a few releases, but it actually was
an exclusive lock.  Previous commits have added the infrastructure to do
an actual read-only lock.  Take advantage of it.
That header file maps libc calls that need mutex locking into
various macros, based on the characteristics of each libc call.  But it
leaves it to perl.h to define those various expansions.  This commit
does that, based on the Configuration of the build.
The many macros in this file now all begin with "PERL_" to avoid
namespace pollution.  However, a few names already existed in previous
Perl releases without that prefix.  This commit allows those names to be
retained, along with the new spellings.
This commit could break something that relies on these, but it would be
broken anyway because these locks are ineffective.  The reason is that
these functions need to be used in conjunction with other functions in a
critical section, and the locks are not designed for that.  Using them
could give a false sense of security.
The mutex lock macros in this commit are retaine for backwards
compatibility, but are now formulated in terms of their new names
This makes it more convenient for a reader to find a line
This code had gotten fragmented.  Put as much stuff controlled by the
same #ifdef logic under a single #ifdef
Since 5335601, there has been the
capability of having nested locks, where the inner one is necessary in
all conditions, and the outer one is desirable in just some conditions.

But this hasn't been enabled because in some Configurations it could
cause a deadlock, with the thread locking the ENV mutex, and then in a
nested call trying to lock it again.  The previous commit made this mutex reentrant, so the deadlock is gone.
Use these new macros to assure thread safety
@khwilliamson khwilliamson marked this pull request as draft February 6, 2026 13:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants