<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Python GUIs - multiprocessing</title><link href="https://www.pythonguis.com/" rel="alternate"/><link href="https://www.pythonguis.com/feeds/multiprocessing.tag.atom.xml" rel="self"/><id>https://www.pythonguis.com/</id><updated>2021-01-14T09:00:00+00:00</updated><subtitle>Create GUI applications with Python and Qt</subtitle><entry><title>Understanding QTimer Timing and Delays in PyQt6 — Why your QTimer might not fire on time, and what you can do about it</title><link href="https://www.pythonguis.com/faq/qtimer-expectations/" rel="alternate"/><published>2021-01-14T09:00:00+00:00</published><updated>2021-01-14T09:00:00+00:00</updated><author><name>Martin Fitzpatrick</name></author><id>tag:www.pythonguis.com,2021-01-14:/faq/qtimer-expectations/</id><summary type="html">I have a PyQt script that reads sensor values every three seconds using a QTimer. However, as the script runs longer, the time between QTimer timeouts drifts significantly &amp;mdash; sometimes exceeding 40 seconds instead of 3. The GUI also becomes less responsive over time. Why is QTimer not firing on schedule, and what can I do about it?</summary><content type="html">
            &lt;blockquote&gt;
&lt;p&gt;I have a PyQt script that reads sensor values every three seconds using a QTimer. However, as the script runs longer, the time between QTimer timeouts drifts significantly &amp;mdash; sometimes exceeding 40 seconds instead of 3. The GUI also becomes less responsive over time. Why is QTimer not firing on schedule, and what can I do about it?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If you've set up a &lt;code&gt;QTimer&lt;/code&gt; to fire every few seconds but noticed that the actual interval between firings is wildly inconsistent, you're not alone. This is a common source of confusion, and understanding &lt;em&gt;why&lt;/em&gt; it happens will help you design more reliable PyQt6 applications.&lt;/p&gt;
&lt;h2 id="how-qtimer-actually-works"&gt;How QTimer Actually Works&lt;/h2&gt;
&lt;p&gt;A &lt;code&gt;QTimer&lt;/code&gt; doesn't operate like a hardware interrupt or a real-time clock. Instead, it works through Qt's &lt;strong&gt;event loop&lt;/strong&gt;. When a timer's interval elapses, Qt places a timer event onto the event queue. Your application then processes that event when it gets to it &amp;mdash; but only after it has finished processing whatever else is currently happening.&lt;/p&gt;
&lt;p&gt;This means that if your application is busy doing something else &amp;mdash; running a Python function, updating a widget, processing data &amp;mdash; the timer event has to wait its turn. The timer might &lt;em&gt;expire&lt;/em&gt; at the right moment, but your slot won't &lt;em&gt;execute&lt;/em&gt; until the event loop is free.&lt;/p&gt;
&lt;p&gt;Here's a simple example showing a 3-second timer:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;import sys
import time
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget
from PyQt6.QtCore import QTimer


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QTimer Demo")

        self.label = QLabel("Waiting for timer...")
        layout = QVBoxLayout()
        layout.addWidget(self.label)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

        self.last_time = time.time()

        self.timer = QTimer()
        self.timer.setInterval(3000)  # 3 seconds
        self.timer.timeout.connect(self.on_timeout)
        self.timer.start()

    def on_timeout(self):
        now = time.time()
        elapsed = now - self.last_time
        self.last_time = now
        self.label.setText(f"Time since last fire: {elapsed:.2f}s")
        print(f"Elapsed: {elapsed:.2f}s")


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Run this and you'll see the elapsed time reported close to 3.00 seconds each time. So far, so good. The timer fires reliably because the event loop has very little else to do.&lt;/p&gt;
&lt;h2 id="what-causes-timer-delays"&gt;What Causes Timer Delays&lt;/h2&gt;
&lt;p&gt;Now let's simulate a problem. Imagine your timer's callback does some work that takes a while &amp;mdash; say, reading from sensors, processing data, or updating plots. If that work blocks the event loop, subsequent timer events pile up and get delayed.&lt;/p&gt;
&lt;p&gt;Add a &lt;code&gt;time.sleep()&lt;/code&gt; call to simulate slow work:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;def on_timeout(self):
    now = time.time()
    elapsed = now - self.last_time
    self.last_time = now
    self.label.setText(f"Time since last fire: {elapsed:.2f}s")
    print(f"Elapsed: {elapsed:.2f}s")

    # Simulate slow sensor reading or data processing
    time.sleep(2)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;With a 3-second interval and 2 seconds of blocking work, you'll see the effective interval stretch to about 5 seconds. And this compounds: if each cycle takes a little longer (perhaps because a plot has more data points to render), the delays grow over time. This matches the pattern described in the original question &amp;mdash; the application getting progressively less responsive.&lt;/p&gt;
&lt;p&gt;The same thing happens when you update complex widgets like plots. If your &lt;code&gt;pyqtgraph&lt;/code&gt; plot has thousands of data points, each redraw takes longer, and the event loop spends more time on painting than processing timer events.&lt;/p&gt;
&lt;h2 id="strategy-1-move-work-to-a-thread"&gt;Strategy 1: Move Work to a Thread&lt;/h2&gt;
&lt;p&gt;One way to keep the event loop responsive is to move your sensor reading and data processing into a separate thread, using Qt's &lt;code&gt;QThread&lt;/code&gt; and signals. For a broader introduction to running background tasks in PyQt6, see &lt;a href="https://www.pythonguis.com/tutorials/multithreading-pyqt6-applications-qthreadpool/"&gt;Multithreading PyQt6 applications with QThreadPool&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The idea: the timer still fires in the main thread, but instead of doing the heavy work directly, it signals a worker thread to do it. The worker thread sends results back via a signal, and the main thread updates the GUI.&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;import sys
import time
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget
from PyQt6.QtCore import QTimer, QThread, QObject, pyqtSignal


class SensorWorker(QObject):
    result_ready = pyqtSignal(float)

    def read_sensor(self):
        # Simulate a slow sensor read
        time.sleep(2)
        value = time.time()  # Placeholder for actual sensor data
        self.result_ready.emit(value)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("QTimer with Thread")

        self.label = QLabel("Waiting for sensor data...")
        layout = QVBoxLayout()
        layout.addWidget(self.label)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

        self.last_time = time.time()

        # Set up the worker thread
        self.thread = QThread()
        self.worker = SensorWorker()
        self.worker.moveToThread(self.thread)
        self.worker.result_ready.connect(self.on_result)
        self.thread.start()

        # Timer triggers the worker
        self.timer = QTimer()
        self.timer.setInterval(3000)
        self.timer.timeout.connect(self.worker.read_sensor)
        self.timer.start()

    def on_result(self, value):
        now = time.time()
        elapsed = now - self.last_time
        self.last_time = now
        self.label.setText(
            f"Sensor value: {value:.2f}\n"
            f"Time since last result: {elapsed:.2f}s"
        )
        print(f"Elapsed: {elapsed:.2f}s")

    def closeEvent(self, event):
        self.thread.quit()
        self.thread.wait()
        super().closeEvent(event)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;With this approach, the 2-second sensor read happens in the background. The main thread's event loop stays free, so the timer fires reliably every 3 seconds and the GUI remains responsive.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A note about Python's GIL:&lt;/strong&gt; Python's Global Interpreter Lock means that only one thread can execute Python code at a time. However, many I/O operations (like reading from a sensor over a serial port or network) release the GIL while waiting for data. So threads work well for I/O-bound tasks. If your sensor reading involves pure Python computation, threads may not help as much.&lt;/p&gt;
&lt;h2 id="strategy-2-use-a-separate-process"&gt;Strategy 2: Use a Separate Process&lt;/h2&gt;
&lt;p&gt;For the most reliable separation between your GUI and your data collection, you can run the sensor reading in a completely separate process using Python's &lt;code&gt;multiprocessing&lt;/code&gt; module. This sidesteps the GIL entirely and gives you true parallel execution.&lt;/p&gt;
&lt;p&gt;Here's a complete example using &lt;code&gt;multiprocessing&lt;/code&gt; with a &lt;code&gt;Queue&lt;/code&gt; to pass data back to the GUI:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;import sys
import time
import multiprocessing
from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel, QVBoxLayout, QWidget
from PyQt6.QtCore import QTimer


def sensor_process(queue, interval):
    """Run in a separate process. Reads sensors and puts results in the queue."""
    while True:
        # Simulate sensor reading
        time.sleep(interval)
        value = time.time()  # Placeholder for real sensor data
        queue.put(value)


class MainWindow(QMainWindow):
    def __init__(self, queue):
        super().__init__()
        self.setWindowTitle("QTimer with Multiprocessing")
        self.queue = queue

        self.label = QLabel("Waiting for sensor data...")
        layout = QVBoxLayout()
        layout.addWidget(self.label)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralLayout = container
        self.setCentralWidget(container)

        self.last_time = time.time()

        # Poll the queue frequently to pick up results
        self.poll_timer = QTimer()
        self.poll_timer.setInterval(500)  # Check every 500ms
        self.poll_timer.timeout.connect(self.check_queue)
        self.poll_timer.start()

    def check_queue(self):
        while not self.queue.empty():
            value = self.queue.get()
            now = time.time()
            elapsed = now - self.last_time
            self.last_time = now
            self.label.setText(
                f"Sensor value: {value:.2f}\n"
                f"Time since last result: {elapsed:.2f}s"
            )
            print(f"Elapsed: {elapsed:.2f}s")


def main():
    queue = multiprocessing.Queue()

    # Start the sensor process
    process = multiprocessing.Process(
        target=sensor_process,
        args=(queue, 3),
        daemon=True,
    )
    process.start()

    app = QApplication(sys.argv)
    window = MainWindow(queue)
    window.show()
    app.exec()

    process.terminate()


if __name__ == "__main__":
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;In this design, the sensor reading runs in its own process with its own Python interpreter. It sleeps for the desired interval and puts data into a shared queue. The GUI process polls the queue with a fast, lightweight timer. Because the polling timer's callback does almost no work (just checking a queue), it never blocks the event loop.&lt;/p&gt;
&lt;h2 id="why-timer-drift-gets-worse-over-time"&gt;Why Timer Drift Gets Worse Over Time&lt;/h2&gt;
&lt;p&gt;The pattern described in the original question &amp;mdash; timers starting out reasonably accurate but degrading over time &amp;mdash; is a telltale sign of accumulating work in the main thread. Common causes include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Growing plot data.&lt;/strong&gt; If you append data to a plot every 3 seconds without limiting the visible range, the plot has more and more points to render. Each redraw takes longer, blocking the event loop for longer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory pressure.&lt;/strong&gt; Growing data structures can cause Python's garbage collector to run more frequently, introducing pauses.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accumulated widget updates.&lt;/strong&gt; If your GUI triggers repaints or layout recalculations on each update, these can become more expensive as the window state becomes more complex.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To address plot-related slowdowns, consider limiting the number of data points displayed. For example, keep only the most recent 1000 points in your plot data:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;MAX_POINTS = 1000

def update_plot(self, new_value):
    self.data.append(new_value)
    if len(self.data) &amp;gt; MAX_POINTS:
        self.data = self.data[-MAX_POINTS:]
    self.plot_widget.plot(self.data, clear=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id="choosing-the-right-approach"&gt;Choosing the Right Approach&lt;/h2&gt;
&lt;p&gt;Here's a quick summary to help you decide:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Limitations&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;QTimer only (no threading)&lt;/td&gt;
&lt;td&gt;Light callbacks that return quickly&lt;/td&gt;
&lt;td&gt;Any slow work blocks the event loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QTimer + QThread&lt;/td&gt;
&lt;td&gt;I/O-bound tasks (sensor reads, network requests)&lt;/td&gt;
&lt;td&gt;Python's GIL limits CPU-bound parallelism&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;QTimer + multiprocessing&lt;/td&gt;
&lt;td&gt;CPU-bound work, or when you need guaranteed isolation&lt;/td&gt;
&lt;td&gt;More complex setup, data must be serializable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For a sensor monitoring application that reads values every few seconds and updates some plots, the &lt;strong&gt;QThread approach&lt;/strong&gt; is usually the best balance of simplicity and reliability. Move the sensor reading into a worker thread, send results back via signals, and keep your plot updates in the main thread. If plot rendering itself becomes the bottleneck, limit the amount of data you're drawing. To learn more about how signals and slots work in PyQt6, see &lt;a href="https://www.pythonguis.com/tutorials/pyqt6-signals-slots-events/"&gt;Signals, Slots &amp;amp; Events&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The multiprocessing approach gives the strongest guarantee that your data collection won't be affected by GUI activity, but it adds complexity around inter-process communication.&lt;/p&gt;
&lt;h2 id="complete-working-example"&gt;Complete Working Example&lt;/h2&gt;
&lt;p&gt;Here's a complete example that combines a worker thread for sensor reading with a &lt;a href="https://www.pythonguis.com/tutorials/pyqt6-plotting-pyqtgraph/"&gt;pyqtgraph&lt;/a&gt; plot, demonstrating reliable timing:&lt;/p&gt;
&lt;div class="code-block"&gt;
&lt;span class="code-block-language code-block-python"&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class="python"&gt;import sys
import time
import collections
from PyQt6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel
from PyQt6.QtCore import QTimer, QThread, QObject, pyqtSignal
import pyqtgraph as pg


class SensorWorker(QObject):
    """Reads sensor data in a background thread."""
    reading_ready = pyqtSignal(float, float)  # timestamp, value

    def do_read(self):
        # Simulate a slow sensor read (replace with your actual sensor code)
        time.sleep(1.5)
        timestamp = time.time()
        value = 42.0  # Replace with real sensor value
        self.reading_ready.emit(timestamp, value)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Reliable Sensor Monitor")
        self.resize(800, 400)

        # Data storage &amp;mdash; keep the last 200 readings
        self.max_points = 200
        self.timestamps = collections.deque(maxlen=self.max_points)
        self.values = collections.deque(maxlen=self.max_points)
        self.intervals = collections.deque(maxlen=self.max_points)
        self.last_timestamp = None

        # UI
        self.label = QLabel("Starting up...")
        self.plot_widget = pg.PlotWidget(title="Time Between Readings (s)")
        self.plot_widget.setLabel("left", "Interval (s)")
        self.plot_widget.setLabel("bottom", "Reading #")
        self.plot_curve = self.plot_widget.plot(pen="y")

        layout = QVBoxLayout()
        layout.addWidget(self.label)
        layout.addWidget(self.plot_widget)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

        # Worker thread
        self.thread = QThread()
        self.worker = SensorWorker()
        self.worker.moveToThread(self.thread)
        self.worker.reading_ready.connect(self.on_reading)
        self.thread.start()

        # Timer to trigger readings every 3 seconds
        self.timer = QTimer()
        self.timer.setInterval(3000)
        self.timer.timeout.connect(self.worker.do_read)
        self.timer.start()

    def on_reading(self, timestamp, value):
        if self.last_timestamp is not None:
            interval = timestamp - self.last_timestamp
            self.intervals.append(interval)
            self.plot_curve.setData(list(self.intervals))
            self.label.setText(
                f"Latest value: {value:.1f}  |  "
                f"Interval: {interval:.2f}s  |  "
                f"Readings: {len(self.intervals)}"
            )
        self.last_timestamp = timestamp

    def closeEvent(self, event):
        self.timer.stop()
        self.thread.quit()
        self.thread.wait()
        super().closeEvent(event)


def main():
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec()


if __name__ == "__main__":
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;In this example, the timer fires every 3 seconds and asks the worker thread to read the sensor. Even though the simulated sensor read takes 1.5 seconds, the main thread stays responsive. The plot updates smoothly, and the interval between readings remains consistent &amp;mdash; hovering around 3 seconds rather than drifting upward over time.&lt;/p&gt;
&lt;p&gt;If you swap out the &lt;code&gt;time.sleep(1.5)&lt;/code&gt; for your actual sensor reading code, you should see stable, predictable timing regardless of how long your application runs. For more on embedding pyqtgraph plots as custom widgets in your PyQt6 applications, see &lt;a href="https://www.pythonguis.com/tutorials/pyqt6-embed-pyqtgraph-custom-widgets-qt-app/"&gt;Embedding custom widgets from Qt Designer&lt;/a&gt;.&lt;/p&gt;
            &lt;p&gt;For an in-depth guide to building Python GUIs with PyQt6 see my book, &lt;a href="https://www.martinfitzpatrick.com/pyqt6-book/"&gt;Create GUI Applications with Python &amp; Qt6.&lt;/a&gt;&lt;/p&gt;
            </content><category term="pyqt6"/><category term="qtimer"/><category term="threads"/><category term="multiprocessing"/><category term="event-loop"/><category term="python"/><category term="qt"/><category term="qt6"/></entry></feed>