1

Imagine we have a thread reading a temperature sensor 10 times per second via USB serial port.
And, when a user presses a button, we can change the sensor mode, also via USB serial.

Problem: the change_sensor_mode() serial write operation can happen in the middle of the polling thread "write/read_until" serial operation, and this causes serial data corruption.

How to handle this general multithreading problem with Python serial?

Example:

import threading, serial, time

serial_port = serial.Serial(port="COM2", timeout=1.0)

def poll_temperature():
    while True:
        serial_port.write(b"GETTEMP\r\n")
        recv = serial_port.read_until(b"END")
        print(recv)
        time.sleep(0.2)

def change_sensor_mode():
    serial_port.write(b"CHANGEMODE2\r\n")

t1 = threading.Thread(target=poll_temperature)
t1.start()

time.sleep(4.12342)  # simulate waiting for user input

change_sensor_mode()

I was thinking about adding a manual lock = True / lock = False before/after the serial operation in poll_temperature, and then the main thread should wait:

global lock

def change_sensor_mode():
    elapsed = 0
    t0 = time.time()
    while lock and elapsed < 1.0:
        time.sleep(0.010)
        elapsed += 0.010
    serial_port.write(b"CHANGEMODE2\r\n")

but there is surely a more standard way to achieve this. How to handle multiple threads writing to serial?

Basj
  • 41,386
  • 99
  • 383
  • 673

1 Answers1

2

You should use a threading.Lock that is shared by any thread that wants to write to this serial port.

import threading

write_lock = threading.Lock()  # all writers have to lock this
read_lock = threading.Lock()  # all readers have to lock this

def poll_temperature():
    while True:
        with write_lock:
            serial_port.write(b"GETTEMP\r\n")
        with read_lock:
            recv = serial_port.read_until(b"END")
        print(recv)
        time.sleep(0.2)

def change_sensor_mode():
    with write_lock:
        serial_port.write(b"CHANGEMODE2\r\n")

The way this works is that no two threads can be executing any code that is guarded by the same lock at the same time, so if one thread tries to execute the second function while another thread is executing the write in the first function, then the second thread must wait until the first thread releases the lock.

Using it in a context manager means that if one of the threads failed inside of that block then the lock is automatically released which avoids a deadlock.

Basj
  • 41,386
  • 99
  • 383
  • 673
Ahmed AEK
  • 8,584
  • 2
  • 7
  • 23
  • 1
    Thanks for your answer! I fixed a typo in my question: both `change_sensor_mode` were variations of the same function, so I renamed both to `change_sensor_mode`. It seems that with your lock, my second variation isn't needed anymore, is it right? I removed this second variation from your answer as well, do you confirm it's no longer needed? – Basj Oct 24 '22 at 14:54
  • 1
    @Basj yes, the second variation isn't needed anymore. – Ahmed AEK Oct 24 '22 at 14:55
  • Great @AhmedAEK! PS: is there a timeout for locks? I mean: if `poll_temperature` has the `lock` for 20 seconds, what happens if `change_sensor_mode` is called? Does it fail, or does it wait? (for how long?) – Basj Oct 24 '22 at 14:57
  • @Basj normally the lock is acquired indefinitely until released, and if one thread is blocked while it has the lock then all threads will be unable to execute that code, but it's highly unlikely that the code inside it will block indefinitely, but if that's the case then you can check this answer for lock with timeout https://stackoverflow.com/a/16782391/15649230 , note that `serial_port.write` won't block indefinitely and will likely raise an error when a problem happens, so a timeout isn't needed in your case. – Ahmed AEK Oct 24 '22 at 15:02
  • just a note, if you are reading from the serial port in multiple threads then you need to create another `read_lock` to lock the reads with it to prevent problems when reading, i have added it to the answer. – Ahmed AEK Oct 24 '22 at 15:07