Skip to content

Commit 0e995ad

Browse files
committed
wip: update type slots, stop-the-world
1 parent db1e582 commit 0e995ad

6 files changed

Lines changed: 100 additions & 4 deletions

File tree

Include/internal/pycore_interp.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ struct _stoptheworld_state {
5757
bool requested; // Set when a pause is requested.
5858
bool world_stopped; // Set when the world is stopped.
5959
bool is_global; // Set when contained in PyRuntime struct.
60+
Py_ssize_t world_stops; // Number of times the world was stopped.
6061

6162
PyEvent stop_event; // Set when thread_countdown reaches zero.
6263
Py_ssize_t thread_countdown; // Number of threads that must pause.

Objects/typeobject.c

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3490,6 +3490,9 @@ static int update_slot(PyTypeObject *, PyObject *);
34903490
static void fixup_slot_dispatchers(PyTypeObject *);
34913491
static int type_new_set_names(PyTypeObject *);
34923492
static int type_new_init_subclass(PyTypeObject *, PyObject *);
3493+
#ifdef Py_GIL_DISABLED
3494+
static bool has_slotdef(PyObject *);
3495+
#endif
34933496

34943497
/*
34953498
* Helpers for __dict__ descriptor. We don't want to expose the dicts
@@ -5943,6 +5946,23 @@ _Py_type_getattro(PyObject *tp, PyObject *name)
59435946
return _Py_type_getattro_impl(type, name, NULL);
59445947
}
59455948

5949+
#ifdef Py_GIL_DISABLED
5950+
static int
5951+
update_slot_world_stopped(PyTypeObject *type, PyObject *name)
5952+
{
5953+
// Modification of type slots is protected by the global type
5954+
// lock. However, type slots are read non-atomically without holding the
5955+
// type lock. So, we need to stop-the-world while modifying slots, in
5956+
// order to avoid data races. This is unfortunately quite expensive.
5957+
int ret;
5958+
PyInterpreterState *interp = _PyInterpreterState_GET();
5959+
_PyEval_StopTheWorld(interp);
5960+
ret = update_slot(type, name);
5961+
_PyEval_StartTheWorld(interp);
5962+
return ret;
5963+
}
5964+
#endif
5965+
59465966
static int
59475967
type_update_dict(PyTypeObject *type, PyDictObject *dict, PyObject *name,
59485968
PyObject *value, PyObject **old_value)
@@ -5972,9 +5992,15 @@ type_update_dict(PyTypeObject *type, PyDictObject *dict, PyObject *name,
59725992
return -1;
59735993
}
59745994

5995+
#if Py_GIL_DISABLED
5996+
if (is_dunder_name(name) && has_slotdef(name)) {
5997+
return update_slot_world_stopped(type, name);
5998+
}
5999+
#else
59756000
if (is_dunder_name(name)) {
59766001
return update_slot(type, name);
59776002
}
6003+
#endif
59786004

59796005
return 0;
59806006
}
@@ -11002,6 +11028,21 @@ resolve_slotdups(PyTypeObject *type, PyObject *name)
1100211028
#undef ptrs
1100311029
}
1100411030

11031+
#ifdef Py_GIL_DISABLED
11032+
// Return true if "name" corresponds to at least one slot definition. This is
11033+
// used to avoid calling update_slot() if is_dunder_name() is true but it's
11034+
// not actually a slot.
11035+
static bool
11036+
has_slotdef(PyObject *name)
11037+
{
11038+
for (pytype_slotdef *p = slotdefs; p->name_strobj; p++) {
11039+
if (p->name_strobj == name) {
11040+
return true;
11041+
}
11042+
}
11043+
return false;
11044+
}
11045+
#endif
1100511046

1100611047
/* Common code for update_slots_callback() and fixup_slot_dispatchers().
1100711048
*
@@ -11241,20 +11282,32 @@ fixup_slot_dispatchers(PyTypeObject *type)
1124111282
END_TYPE_LOCK();
1124211283
}
1124311284

11285+
// Called when __bases__ is re-assigned.
1124411286
static void
1124511287
update_all_slots(PyTypeObject* type)
1124611288
{
1124711289
pytype_slotdef *p;
1124811290

1124911291
ASSERT_TYPE_LOCK_HELD();
1125011292

11293+
// Similar to update_slot_world_stopped(), this is required to
11294+
// avoid races. We do it once here rather than once per-slot.
11295+
#ifdef Py_GIL_DISABLED
11296+
PyInterpreterState *interp = _PyInterpreterState_GET();
11297+
_PyEval_StopTheWorld(interp);
11298+
#endif
11299+
1125111300
/* Clear the VALID_VERSION flag of 'type' and all its subclasses. */
1125211301
type_modified_unlocked(type);
1125311302

1125411303
for (p = slotdefs; p->name; p++) {
1125511304
/* update_slot returns int but can't actually fail */
1125611305
update_slot(type, p->name_strobj);
1125711306
}
11307+
11308+
#ifdef Py_GIL_DISABLED
11309+
_PyEval_StartTheWorld(interp);
11310+
#endif
1125811311
}
1125911312

1126011313

Python/clinic/sysmodule.c.h

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

Python/pystate.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2355,6 +2355,7 @@ start_the_world(struct _stoptheworld_state *stw)
23552355
assert(PyMutex_IsLocked(&stw->mutex));
23562356

23572357
HEAD_LOCK(runtime);
2358+
stw->world_stops++;
23582359
stw->requested = 0;
23592360
stw->world_stopped = 0;
23602361
// Switch threads back to the detached state.
@@ -2730,6 +2731,7 @@ _PyGILState_Fini(PyInterpreterState *interp)
27302731
return;
27312732
}
27322733
interp->runtime->gilstate.autoInterpreterState = NULL;
2734+
//fprintf(stderr, "world stops %zd\n", interp->stoptheworld.world_stops);
27332735
}
27342736

27352737

Python/sysmodule.c

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2527,6 +2527,27 @@ sys__is_gil_enabled_impl(PyObject *module)
25272527
#endif
25282528
}
25292529

2530+
/*[clinic input]
2531+
sys._get_world_stops
2532+
2533+
Return the number of times the "stop-the-world" condition was true.
2534+
[clinic start generated code]*/
2535+
2536+
static PyObject *
2537+
sys__get_world_stops_impl(PyObject *module)
2538+
/*[clinic end generated code: output=7886d32b71a94e72 input=44a9bde7e07b30e3]*/
2539+
{
2540+
Py_ssize_t stops;
2541+
#ifdef Py_GIL_DISABLED
2542+
PyInterpreterState *interp = _PyInterpreterState_GET();
2543+
stops = interp->stoptheworld.world_stops;
2544+
#else
2545+
stops = 0;
2546+
#endif
2547+
return PyLong_FromLong(stops);
2548+
}
2549+
2550+
25302551

25312552
static PerfMapState perf_map_state;
25322553

@@ -2703,6 +2724,7 @@ static PyMethodDef sys_methods[] = {
27032724
#endif
27042725
SYS__GET_CPU_COUNT_CONFIG_METHODDEF
27052726
SYS__IS_GIL_ENABLED_METHODDEF
2727+
SYS__GET_WORLD_STOPS_METHODDEF
27062728
SYS__DUMP_TRACELETS_METHODDEF
27072729
{NULL, NULL} // sentinel
27082730
};

Tools/tsan/suppressions_free_threading.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@
1414

1515
race_top:assign_version_tag
1616
race_top:_multiprocessing_SemLock_acquire_impl
17-
race_top:_Py_slot_tp_getattr_hook
17+
#race_top:_Py_slot_tp_getattr_hook
1818
race_top:dump_traceback
1919
race_top:fatal_error
2020
race_top:_multiprocessing_SemLock_release_impl
2121
race_top:_PyFrame_GetCode
2222
race_top:_PyFrame_Initialize
2323
race_top:_PyObject_TryGetInstanceAttribute
2424
race_top:PyUnstable_InterpreterFrame_GetLine
25-
race_top:type_modified_unlocked
25+
#race_top:type_modified_unlocked
2626
race_top:write_thread_id
2727

2828
# gh-129068: race on shared range iterators (test_free_threading.test_zip.ZipThreading.test_threading)
@@ -32,7 +32,7 @@ race_top:rangeiter_next
3232
race_top:mi_block_set_nextx
3333

3434
# gh-127266: type slot updates are not thread-safe (test_opcache.test_load_attr_method_lazy_dict)
35-
race_top:update_one_slot
35+
#race_top:update_one_slot
3636

3737
# https://gist.github.com/mpage/6962e8870606cfc960e159b407a0cb40
3838
thread:pthread_create

0 commit comments

Comments
 (0)