Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Include/internal/pycore_critical_section.h
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ extern "C" {
# define Py_END_CRITICAL_SECTION() \
_PyCriticalSection_End(&_cs); \
}
# define Py_EXIT_CRITICAL_SECTION() \
Copy link
Member

Choose a reason for hiding this comment

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

Maybe it is better to define a local API instead of a global one? For example END_TYPE_LOCK() does that.

Copy link
Contributor

Choose a reason for hiding this comment

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

You can just extract the critical section into a helper function and then wrap the helper with the existing macros.

Copy link
Contributor

@erlend-aasland erlend-aasland Jun 17, 2024

Choose a reason for hiding this comment

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

For this PR, I'd just extract most of reversed_next into a reversed_next_impl and wrap that with the Py_{BEGIN,END}_CRITICAL_SECTION macros. See also #120318.

Copy link
Member

Choose a reason for hiding this comment

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

FWIW we ended up not doing that in #120318. There's this comment though that explains the approach and gives an example.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the feedback everyone! I added the Py_EXIT_CRITICAL_SECTION in this PR (and some others) to deal with return and goto statements. I am not in favor of adding a local API, since it will leak details of the locking implementation to modules. Changing the Py_EXIT_CRITICAL_SECTION to a more private _Py_EXIT_CRITICAL_SECTION (or something like that) would be fine with me.

In this case we can indeed refactor to create a reversed_next_impl and put a global lock around it. I am not too worried about performance here, and if it turns out performance is an issue we can change later.

Unless more suggestions come it, I will refactor to reversed_next_impl in the coming days.

_PyCriticalSection_End(&_cs);

# define Py_BEGIN_CRITICAL_SECTION2(a, b) \
{ \
Expand Down Expand Up @@ -156,6 +158,7 @@ extern "C" {
# define Py_BEGIN_CRITICAL_SECTION(op)
# define Py_END_CRITICAL_SECTION()
# define Py_BEGIN_CRITICAL_SECTION2(a, b)
# define Py_EXIT_CRITICAL_SECTION()
# define Py_END_CRITICAL_SECTION2()
# define Py_BEGIN_CRITICAL_SECTION_SEQUENCE_FAST(original)
# define Py_END_CRITICAL_SECTION_SEQUENCE_FAST()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make reversed thread-safe.
5 changes: 5 additions & 0 deletions Objects/enumobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "Python.h"
#include "pycore_call.h" // _PyObject_CallNoArgs()
#include "pycore_critical_section.h" // Py_BEGIN_CRITICAL_SECTION()
#include "pycore_long.h" // _PyLong_GetOne()
#include "pycore_modsupport.h" // _PyArg_NoKwnames()
#include "pycore_object.h" // _PyObject_GC_TRACK()
Expand Down Expand Up @@ -431,16 +432,20 @@ reversed_next(reversedobject *ro)
PyObject *item;
Py_ssize_t index = ro->index;

Py_BEGIN_CRITICAL_SECTION(ro);
if (index >= 0) {
item = PySequence_GetItem(ro->seq, index);
if (item != NULL) {
ro->index--;
Copy link
Member

Choose a reason for hiding this comment

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

We don't declare the critical section for the list_next

cpython/Objects/listobject.c

Lines 3890 to 3911 in dacc5ac

listiter_next(PyObject *self)
{
_PyListIterObject *it = (_PyListIterObject *)self;
Py_ssize_t index = FT_ATOMIC_LOAD_SSIZE_RELAXED(it->it_index);
if (index < 0) {
return NULL;
}
PyObject *item = list_get_item_ref(it->it_seq, index);
if (item == NULL) {
// out-of-bounds
FT_ATOMIC_STORE_SSIZE_RELAXED(it->it_index, -1);
#ifndef Py_GIL_DISABLED
PyListObject *seq = it->it_seq;
it->it_seq = NULL;
Py_DECREF(seq);
#endif
return NULL;
}
FT_ATOMIC_STORE_SSIZE_RELAXED(it->it_index, index + 1);
return item;
}

Why do we have to declare the critical section here?
And what about just handling as the out-of-bound,
and if the ro->index needs to be decremented, why not just use atomic operation in here?

cc @colesbury

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The check index >= 0 and decrement ro->index-- need to happen atomically. We could use the atomic _Py_atomic_compare_exchange for this, but it would increase code complexity. If performance is a concern, I can update the PR with this approach. The PySequence_GetItem is not inside the critical section, but I assumed it to be (or to be made later) thread safe

Py_EXIT_CRITICAL_SECTION();
return item;
}
if (PyErr_ExceptionMatches(PyExc_IndexError) ||
PyErr_ExceptionMatches(PyExc_StopIteration))
PyErr_Clear();
}
Py_END_CRITICAL_SECTION();

ro->index = -1;
Py_CLEAR(ro->seq);
return NULL;
Expand Down