CPython 2.7: Weakset data changing size during internal iteration

T

Temia Eszteri

I've got a bit of a problem - my project uses weak sets in multiple
areas, the problem case in particular being to indicate what objects
are using a particular texture, if any, so that its priority in OpenGL
can be adjusted to match at the same time as it being (de)referenced
by any explicit calls.

Problem is that for certain high-frequency operations, it seems
there's too much data going in and out for it to handle - the
following traceback is given to me (project path changed to protect
the innocent):

Traceback (most recent call last):
File "C:\foo\bar\game.py", line 279, in update
self.player.update()
File "C:\foo\bar\player.py", line 87, in update
PlayerBullet((self.x + 8, self.y + 9), 0, self.parent)
File "C:\foo\bar\player.py", line 96, in __init__
self.sprite = video.Sprite("testbullet", 0)
File "C:\foo\bar\video.py", line 95, in __init__
self.opengl_id = reference_texture(self, target)
File "C:\foo\bar\video.py", line 310, in reference_texture
if not video_handler.textures[target].references:
File "C:\Python27\lib\_weakrefset.py", line 66, in __len__
return sum(x() is not None for x in self.data)
File "C:\Python27\lib\_weakrefset.py", line 66, in <genexpr>
return sum(x() is not None for x in self.data)
RuntimeError: Set changed size during iteration

I can post the sources relevant to the traceback upon request, but
hopefully a traceback is sufficient as the most immediate problem is
in Python's libraries.

Any suggestions on what to do about this? I can't exactly throw a
..copy() in on top of the data iteration and call it good since it's
part of the standard Python library.

~Temia
 
S

Steven D'Aprano

I've got a bit of a problem - my project uses weak sets in multiple
areas, the problem case in particular being to indicate what objects are
using a particular texture, if any, so that its priority in OpenGL can
be adjusted to match at the same time as it being (de)referenced by any
explicit calls.

Problem is that for certain high-frequency operations, it seems there's
too much data going in and out for it to handle

I doubt that very much. If you are using threads, it is more likely your
code has a race condition where you are modifying a weak set at the same
time another thread is trying to iterate over it (in this case, to
determine it's length), and because it's a race condition, it only
happens when conditions are *just right*. Since race conditions hitting
are usually rare, you only notice it when there's a lot of data.
 
T

Temia Eszteri

I doubt that very much. If you are using threads, it is more likely your
code has a race condition where you are modifying a weak set at the same
time another thread is trying to iterate over it (in this case, to
determine it's length), and because it's a race condition, it only
happens when conditions are *just right*. Since race conditions hitting
are usually rare, you only notice it when there's a lot of data.

Except that the few threads I use don't modify that data at all
because the functions that even touch the references set rely on
OpenGL contexts along with it which are thread-bound, ergo, impossible
to call without stopping the code in its tracks to begin with unless
the context's explicitly shifted (which it very much isn't).

And I've done some looking through the weak set's code in the
intervening time; it does easily have the potential to cause this kind
of problem because the weak references made are set to a callback to
remove them from the data set when garbage is collected. See for
yourself.:

Lines 81-84, _weakrefset.py:

def add(self, item):
if self._pending_removals:
self._commit_removals()
self.data.add(ref(item, self._remove)) <--

Lines 38-44, likewise: (for some reason called in __init__ rather than
at the class level, but likely to deal with a memory management issue)

def _remove(item, selfref=ref(self)):
self = selfref()
if self is not None:
if self._iterating: <--
self._pending_removals.append(item)
else:
self.data.discard(item) <--
self._remove = _remove

The thing is, as Terry pointed out, its truth value is tested based on
__len__(), which as shown does NOT set the _iterating protection:

def __len__(self):
return sum(x() is not None for x in self.data)

Don't be so fast to dismiss things when the situation would not have
made a race condition possible to begin with.

~Temia
 
S

Steven D'Aprano

Except that the few threads I use don't modify that data at all
[...]

And should I have known this from your initial post?


[...]
Don't be so fast to dismiss things when the situation would not have
made a race condition possible to begin with.

If you have been part of this newsgroup and mailing list as long as I
have, you should realise that there is no shortage of people who come
here and make grand claims that they have discovered a bug in Python
(either the language, or the standard library). Nine times out of ten,
they have not, and the bug is in their code, or their understanding.

Perhaps you are one of the few who has actually found a bug in the
standard library rather than one in your own code. But your initial post
showed no sign that you had done any investigation beyond reading the
traceback and immediately jumping to the conclusion that it was a bug in
the standard library.

Frankly, I still doubt that your analysis of the problem is correct:

Problem is that for certain high-frequency operations, it
seems there's too much data going in and out for it to handle
[end quote]


I still can't see any way for this bug to occur due to "too much data",
as you suggest, or in the absence of one thread modifying the set while
another is iterating over it. But I could be wrong.

In any case, it appears that this bug has already been reported and fixed:

http://bugs.python.org/issue14159


Consider updating to the latest bug fix of 2.7.
 
T

Temia Eszteri

And should I have known this from your initial post?

I did discuss the matter with Terry Reedy, actually, but I guess since
the newsgroup-to-mailing list mirror is one-way, there's no actual way
you could've known. :/ Sigh, another problem out of my hands to deal
with. I do apologize for the snippy attitude, if it means anything.
Frankly, I still doubt that your analysis of the problem is correct:

Problem is that for certain high-frequency operations, it
seems there's too much data going in and out for it to handle
[end quote]


I still can't see any way for this bug to occur due to "too much data",
as you suggest, or in the absence of one thread modifying the set while
another is iterating over it. But I could be wrong.

Well, in this case, I'd consider it more reasonable to look at it from
a different angle, but it was rather poorly-phrased at the beginning.
When you've got dozens of objects being garbage-collected from the set
every 16 miliseconds or so though, that's certainly high-frequency
enough to trigger the bug, is it not?
In any case, it appears that this bug has already been reported and fixed:

http://bugs.python.org/issue14159

Consider updating to the latest bug fix of 2.7.

Alas, I'm already on the latest official release, which doesn't have
the patch yet. I'll just apply it manually.

Though now I'm now curious about how regular sets get their truth
value, since weaksets internally performing a length check every time
a texture was being referenced or de-referenced, for simple lack of a
faster explicit __bool__ value, is going to be rather costly when
things'll be flying around and out of the screen area in large
quantities. Hoo boy.

~Temia
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,982
Messages
2,570,186
Members
46,743
Latest member
WoodrowMea

Latest Threads

Top