Segmentation fault for scoped references with multiple QNetworkReply objects
Phil Thompson
phil at riverbankcomputing.com
Mon Aug 12 10:02:45 BST 2024
On 11/08/2024 23:44, Maurizio Berti wrote:
> I am writing a web radio player as personal project. Other than
> accessing
> the media stream, it also access some related files (json for the
> program
> list, related images, etc.)
>
> Since I'm in the middle of the development, I've not dotted all i's
> yet.
> Specifically, some QNetworkReply objects don't get deleted yet once I'm
> done with them (which I'll eventually do with deleteLater as expected,
> but
> I'll need to find a proper way to handle that while providing a common
> interface).
>
> Some of those replies work by simply referencing the reply itself in a
> local function.
> For example, this is a simplified version of some code that gets a
> remote
> image:
>
> def getRemoteImage(self, someObject):
> # "self.nam" is the common QNetworkAccessManager
> reply = self.nam.get(QNetworkRequest(someObject.url()))
> def doSomeStuff():
> pm = QPixmap()
> if pm.loadFromData(reply.readAll()):
> someObject.setIcon(QIcon(pm))
> reply.finished.connect(doSomeStuff)
>
> I know that this is not very elegant, but it's concise, and it works
> for
> simple situations, without requiring a more comprehensive
> implementation
> that isn't based on local context.
>
> Well, at least, I *thought* it worked.
> After some time and different testing, I stumbled upon unexpected
> segfaults. The result of my research is that if the replies are not
> deleted, at some point the program crashes.
> What I found interesting is that what *seems* to trigger the crash is
> the
> number of connections done to the reply, like there's a total amount of
> possible existing replies and their scoped references.
>
> I prepared a MRE that should be able to show the problem:
>
> from PyQt5.QtCore import *
> from PyQt5.QtWidgets import *
> from PyQt5.QtNetwork import *
>
> class Test(QWidget):
> def __init__(self):
> super().__init__()
> btn = QPushButton('Test')
> QVBoxLayout(self).addWidget(btn)
>
> self.nam = QNetworkAccessManager()
> btn.clicked.connect(btn.setEnabled)
> btn.clicked.connect(self.download)
>
> self.timer = QTimer(self, timeout=self.download, interval=100)
> self.count = 0
>
> base = 'https://www.google.com/search?q={}'
> self.urls = [QUrl(base.format(i)) for i in range(100)]
>
> def download(self):
> if not self.timer.isActive():
> self.timer.start()
> url = self.urls.pop(0)
> reply = self.nam.get(QNetworkRequest(url))
>
> def test1():
> self.count += 1
> print(self.count, 'finished (first connect)')
> def test2():
> pass
> def test3():
> pass
> def testWithReference():
> print(self.count, 'finished > reference to reply:', reply)
> print()
>
> reply.finished.connect(test1)
> reply.finished.connect(test2)
> reply.finished.connect(test3)
>
> reply.finished.connect(self.doSomething)
> reply.finished.connect(testWithReference)
>
> if not self.urls:
> self.timer.stop()
>
> def doSomething(self):
> print(self.sender())
>
>
> app = QApplication([])
> test = Test()
> test.show()
> app.exec()
>
> I'm currently able to reproduce this on Linux with PyQt 5.15.4 (Qt
> 5.15.2)
> and PyQt/Qt 6.6.1, with Python 3.9.5.
> Interestingly enough, this doesn't happen with an old PyQt/Qt 5.7.1 and
> Python 3.4.5.
>
> Note that I'm completely aware that avoiding replies deletion and using
> local references are not recommended approaches, yet, at least based on
> the
> generic behavior of wrapped Qt objects and python scope management, it
> should work in theory: a similar concept could normally be easily and
> reliably applied with other QObjects, leading me to think that,
> probably,
> at some point there is some issue in the management of references to
> objects directly generated by Qt.
>
> I will obviously implement a more thorough deletion mechanism for the
> replies, which, based on my tests, should avoid the problem completely.
> Yet, I wanted to report this anyway, at least in order to understand
> its
> origins (I *suppose* it's a Python problem, but I'm not 100% sure),
> what
> causes it and know more about the underlying aspects.
> Specifically, I'd like to understand what could happen in a stress
> case,
> with dozens of current requests (and replies) that have not been
> completed
> yet.
>
> Regards,
> MaurizioB
First off, I can't reproduce the problem - even if I double the number
of URLs to 200.
However looking at the reply object using sip.dump() shows it has no
parent wrapped object whereas the docs for QNetworkReply::manager()
state that the manager is the reply's parent. Adding a /Transfer/
annotation to QNetworkManager::get() fixes this - but I have no idea if
this has any effect on your test case.
Phil
More information about the PyQt
mailing list