Skip to content

Commit c5dad28

Browse files
committed
pythongh-111956: Add thread-safe one-time initialization.
The one-time initialization (`_PyOnceFlag`) is used in two places: * `Python/Python-ast.c` * `Python/getargs.c`
1 parent d61313b commit c5dad28

File tree

9 files changed

+152
-57
lines changed

9 files changed

+152
-57
lines changed

Include/internal/pycore_ast_state.h

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_lock.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ typedef struct _PyMutex PyMutex;
4646
#define _Py_UNLOCKED 0
4747
#define _Py_LOCKED 1
4848
#define _Py_HAS_PARKED 2
49+
#define _Py_ONCE_INITIALIZED 4
4950

5051
// (private) slow path for locking the mutex
5152
PyAPI_FUNC(void) _PyMutex_LockSlow(PyMutex *m);
@@ -166,6 +167,35 @@ _PyRawMutex_Unlock(_PyRawMutex *m)
166167
_PyRawMutex_UnlockSlow(m);
167168
}
168169

170+
// A data structure that can be used to run initialization code once in a
171+
// thread-safe manner. The C++11 equivalent is std::call_once.
172+
typedef struct {
173+
uint8_t v;
174+
} _PyOnceFlag;
175+
176+
// Type signature for one-time initialization functions. The function should
177+
// return 1 on success and 0 on failure.
178+
typedef int _Py_once_fn_t(void *arg);
179+
180+
// (private) slow path for one time initialization
181+
PyAPI_FUNC(int)
182+
_PyOnceFlag_CallOnceSlow(_PyOnceFlag *flag, _Py_once_fn_t *fn, void *arg);
183+
184+
// Calls `fn` once using `flag`. The `arg` is passed to the call to `fn`.
185+
//
186+
// Returns 1 on success and 0 on failure.
187+
//
188+
// If `fn` returns 1 (success), then subsequent calls immediately return 1.
189+
// If `fn` returns 0 (failure), then subsequent calls will retry the call.
190+
static inline int
191+
_PyOnceFlag_CallOnce(_PyOnceFlag *flag, _Py_once_fn_t *fn, void *arg)
192+
{
193+
if (_Py_atomic_load_uint8(&flag->v) == _Py_ONCE_INITIALIZED) {
194+
return 1;
195+
}
196+
return _PyOnceFlag_CallOnceSlow(flag, fn, arg);
197+
}
198+
169199
#ifdef __cplusplus
170200
}
171201
#endif

Include/internal/pycore_modsupport.h

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
#ifndef Py_INTERNAL_MODSUPPORT_H
22
#define Py_INTERNAL_MODSUPPORT_H
3+
4+
#include "pycore_lock.h" // _PyOnceFlag
5+
36
#ifdef __cplusplus
47
extern "C" {
58
#endif
@@ -65,15 +68,16 @@ PyAPI_FUNC(void) _PyArg_BadArgument(
6568
// --- _PyArg_Parser API ---------------------------------------------------
6669

6770
typedef struct _PyArg_Parser {
68-
int initialized;
6971
const char *format;
7072
const char * const *keywords;
7173
const char *fname;
7274
const char *custom_msg;
73-
int pos; /* number of positional-only arguments */
74-
int min; /* minimal number of arguments */
75-
int max; /* maximal number of positional arguments */
76-
PyObject *kwtuple; /* tuple of keyword parameter names */
75+
_PyOnceFlag once; /* atomic one-time initialization flag */
76+
int is_kwtuple_owned; /* does this parser own the kwtuple object? */
77+
int pos; /* number of positional-only arguments */
78+
int min; /* minimal number of arguments */
79+
int max; /* maximal number of positional arguments */
80+
PyObject *kwtuple; /* tuple of keyword parameter names */
7781
struct _PyArg_Parser *next;
7882
} _PyArg_Parser;
7983

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add internal-only one-time initialization API: ``_PyOnceFlag`` and
2+
``_PyOnceFlag_CallOnce``.

Modules/_testinternalcapi/test_lock.c

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,13 +341,45 @@ test_lock_benchmark(PyObject *module, PyObject *obj)
341341
Py_RETURN_NONE;
342342
}
343343

344+
static int
345+
init_maybe_fail(void *arg)
346+
{
347+
int *counter = (int *)arg;
348+
(*counter)++;
349+
if (*counter < 5) {
350+
// failure
351+
return 0;
352+
}
353+
assert(*counter == 5);
354+
return 1;
355+
}
356+
357+
static PyObject *
358+
test_lock_once(PyObject *self, PyObject *obj)
359+
{
360+
_PyOnceFlag once = {0};
361+
int counter = 0;
362+
for (int i = 0; i < 10; i++) {
363+
int res = _PyOnceFlag_CallOnce(&once, init_maybe_fail, &counter);
364+
if (i < 4) {
365+
assert(res == 0);
366+
}
367+
else {
368+
assert(res == 1);
369+
assert(counter == 5);
370+
}
371+
}
372+
Py_RETURN_NONE;
373+
}
374+
344375
static PyMethodDef test_methods[] = {
345376
{"test_lock_basic", test_lock_basic, METH_NOARGS},
346377
{"test_lock_two_threads", test_lock_two_threads, METH_NOARGS},
347378
{"test_lock_counter", test_lock_counter, METH_NOARGS},
348379
{"test_lock_counter_slow", test_lock_counter_slow, METH_NOARGS},
349380
_TESTINTERNALCAPI_BENCHMARK_LOCKS_METHODDEF
350381
{"test_lock_benchmark", test_lock_benchmark, METH_NOARGS},
382+
{"test_lock_once", test_lock_once, METH_NOARGS},
351383
{NULL, NULL} /* sentinel */
352384
};
353385

Parser/asdl_c.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,13 +1102,6 @@ def visitModule(self, mod):
11021102
static int
11031103
init_types(struct ast_state *state)
11041104
{
1105-
// init_types() must not be called after _PyAST_Fini()
1106-
// has been called
1107-
assert(state->initialized >= 0);
1108-
1109-
if (state->initialized) {
1110-
return 1;
1111-
}
11121105
if (init_identifiers(state) < 0) {
11131106
return 0;
11141107
}
@@ -1125,7 +1118,6 @@ def visitModule(self, mod):
11251118
self.file.write(textwrap.dedent('''
11261119
state->recursion_depth = 0;
11271120
state->recursion_limit = 0;
1128-
state->initialized = 1;
11291121
return 1;
11301122
}
11311123
'''))
@@ -1480,7 +1472,8 @@ def visit(self, object):
14801472

14811473
def generate_ast_state(module_state, f):
14821474
f.write('struct ast_state {\n')
1483-
f.write(' int initialized;\n')
1475+
f.write(' _PyOnceFlag once;\n')
1476+
f.write(' int finalized;\n')
14841477
f.write(' int recursion_depth;\n')
14851478
f.write(' int recursion_limit;\n')
14861479
for s in module_state:
@@ -1500,11 +1493,8 @@ def generate_ast_fini(module_state, f):
15001493
f.write(textwrap.dedent("""
15011494
Py_CLEAR(_Py_INTERP_CACHED_OBJECT(interp, str_replace_inf));
15021495
1503-
#if !defined(NDEBUG)
1504-
state->initialized = -1;
1505-
#else
1506-
state->initialized = 0;
1507-
#endif
1496+
state->finalized = 1;
1497+
state->once = (_PyOnceFlag){0};
15081498
}
15091499
15101500
"""))
@@ -1543,6 +1533,7 @@ def generate_module_def(mod, metadata, f, internal_h):
15431533
#include "pycore_ast.h"
15441534
#include "pycore_ast_state.h" // struct ast_state
15451535
#include "pycore_ceval.h" // _Py_EnterRecursiveCall
1536+
#include "pycore_lock.h" // _PyOnceFlag
15461537
#include "pycore_interp.h" // _PyInterpreterState.ast
15471538
#include "pycore_pystate.h" // _PyInterpreterState_GET()
15481539
#include <stddef.h>
@@ -1555,7 +1546,8 @@ def generate_module_def(mod, metadata, f, internal_h):
15551546
{
15561547
PyInterpreterState *interp = _PyInterpreterState_GET();
15571548
struct ast_state *state = &interp->ast;
1558-
if (!init_types(state)) {
1549+
assert(!state->finalized);
1550+
if (!_PyOnceFlag_CallOnce(&state->once, (_Py_once_fn_t *)&init_types, state)) {
15591551
return NULL;
15601552
}
15611553
return state;
@@ -1628,6 +1620,9 @@ def write_internal_h_header(mod, f):
16281620
print(textwrap.dedent("""
16291621
#ifndef Py_INTERNAL_AST_STATE_H
16301622
#define Py_INTERNAL_AST_STATE_H
1623+
1624+
#include "pycore_lock.h" // _PyOnceFlag
1625+
16311626
#ifdef __cplusplus
16321627
extern "C" {
16331628
#endif

Python/Python-ast.c

Lines changed: 5 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/getargs.c

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1877,8 +1877,9 @@ new_kwtuple(const char * const *keywords, int total, int pos)
18771877
}
18781878

18791879
static int
1880-
_parser_init(struct _PyArg_Parser *parser)
1880+
_parser_init(void *arg)
18811881
{
1882+
struct _PyArg_Parser *parser = (struct _PyArg_Parser *)arg;
18821883
const char * const *keywords = parser->keywords;
18831884
assert(keywords != NULL);
18841885
assert(parser->pos == 0 &&
@@ -1925,40 +1926,27 @@ _parser_init(struct _PyArg_Parser *parser)
19251926
parser->min = min;
19261927
parser->max = max;
19271928
parser->kwtuple = kwtuple;
1928-
parser->initialized = owned ? 1 : -1;
1929+
parser->is_kwtuple_owned = owned;
19291930

19301931
assert(parser->next == NULL);
1931-
parser->next = _PyRuntime.getargs.static_parsers;
1932-
_PyRuntime.getargs.static_parsers = parser;
1932+
parser->next = _Py_atomic_load_ptr(&_PyRuntime.getargs.static_parsers);
1933+
do {
1934+
// compare-exchange updates parser->next on failure
1935+
} while (_Py_atomic_compare_exchange_ptr(&_PyRuntime.getargs.static_parsers,
1936+
&parser->next, parser));
19331937
return 1;
19341938
}
19351939

19361940
static int
19371941
parser_init(struct _PyArg_Parser *parser)
19381942
{
1939-
// volatile as it can be modified by other threads
1940-
// and should not be optimized or reordered by compiler
1941-
if (*((volatile int *)&parser->initialized)) {
1942-
assert(parser->kwtuple != NULL);
1943-
return 1;
1944-
}
1945-
PyThread_acquire_lock(_PyRuntime.getargs.mutex, WAIT_LOCK);
1946-
// Check again if another thread initialized the parser
1947-
// while we were waiting for the lock.
1948-
if (*((volatile int *)&parser->initialized)) {
1949-
assert(parser->kwtuple != NULL);
1950-
PyThread_release_lock(_PyRuntime.getargs.mutex);
1951-
return 1;
1952-
}
1953-
int ret = _parser_init(parser);
1954-
PyThread_release_lock(_PyRuntime.getargs.mutex);
1955-
return ret;
1943+
return _PyOnceFlag_CallOnce(&parser->once, &_parser_init, parser);
19561944
}
19571945

19581946
static void
19591947
parser_clear(struct _PyArg_Parser *parser)
19601948
{
1961-
if (parser->initialized == 1) {
1949+
if (parser->is_kwtuple_owned) {
19621950
Py_CLEAR(parser->kwtuple);
19631951
}
19641952
}

Python/lock.c

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,3 +295,52 @@ PyEvent_WaitTimed(PyEvent *evt, _PyTime_t timeout_ns)
295295
return _Py_atomic_load_uint8(&evt->v) == _Py_LOCKED;
296296
}
297297
}
298+
299+
static int
300+
unlock_once(_PyOnceFlag *o, int res)
301+
{
302+
// On success (res=1), we set the state to _Py_ONCE_INITIALIZED.
303+
// On failure (res=0), we reset the state to _Py_UNLOCKED.
304+
uint8_t new_value = res ? _Py_ONCE_INITIALIZED : _Py_UNLOCKED;
305+
306+
uint8_t old_value = _Py_atomic_exchange_uint8(&o->v, new_value);
307+
if ((old_value & _Py_HAS_PARKED) != 0) {
308+
// wake up anyone waiting on the once flag
309+
_PyParkingLot_UnparkAll(&o->v);
310+
}
311+
return res;
312+
}
313+
314+
int
315+
_PyOnceFlag_CallOnceSlow(_PyOnceFlag *flag, _Py_once_fn_t *fn, void *arg)
316+
{
317+
uint8_t v = _Py_atomic_load_uint8(&flag->v);
318+
for (;;) {
319+
if (v == _Py_UNLOCKED) {
320+
if (!_Py_atomic_compare_exchange_uint8(&flag->v, &v, _Py_LOCKED)) {
321+
continue;
322+
}
323+
int res = fn(arg);
324+
return unlock_once(flag, res);
325+
}
326+
327+
if (v == _Py_ONCE_INITIALIZED) {
328+
return 1;
329+
}
330+
331+
// The once flag is initializing (locked).
332+
assert((v & _Py_LOCKED));
333+
if (!(v & _Py_HAS_PARKED)) {
334+
// We are the first waiter. Set the _Py_HAS_PARKED flag.
335+
uint8_t new_value = v | _Py_HAS_PARKED;
336+
if (!_Py_atomic_compare_exchange_uint8(&flag->v, &v, new_value)) {
337+
continue;
338+
}
339+
v = new_value;
340+
}
341+
342+
// Wait for initialization to finish.
343+
_PyParkingLot_Park(&flag->v, &v, sizeof(v), -1, NULL, 1);
344+
v = _Py_atomic_load_uint8(&flag->v);
345+
}
346+
}

0 commit comments

Comments
 (0)