Search code examples
multithreadingopencvrustchannelegui

Sending data between threads using MPSC channels sometimes takes 5-10 seconds, possible thread starvation?


I am trying to send some data between two threads but it is taking upwards of 5-10 seconds for it to be received on the other thread. The main thread is running a GUI (written in egui), and when the user clicks a button, it spawns the second thread, creating an MPSC channel in the process. The second thread opens an camera and tries to decode a QR code using openCV, and if it detects a string length of greater than zero, it sends that back to the main thread. The main thread then drops its end of the channel, which causes the other thread to end.

I have set up a 'heartbeat' message which comes through in under a second, but whenever I actually detect a QR code and send the string, it takes ages to come through to the other thread. All the output pauses, I get multiple instances of it 'sending' the string, but no debug output from the main thread saying it received it until 5-10 seconds later.

This is my code running in the second thread, the one that seems to get all of the cpu time (I also tried it with it creating a openCV window and showing the output, and that continued to update fast, so that thread was still running fine) :

pub enum QRThreadResponse {
    NewQRString(String),
    ThreadError(String),
    Heartbeat,
    ThreadShutdown,
}


pub fn SpawnQrThread(camera_id: i32, chl: Sender<QRThreadResponse>){
    thread::spawn(move || {
        println!("Hello from QR Thread");
        let mut qr_detector = objdetect::QRCodeDetector::default().expect("FAILED to create QR Detector");
        let mut res = types::VectorOfPoint::new();
        let mut camera = videoio::VideoCapture::new(camera_id, videoio::CAP_DSHOW).expect("Open Camera Failed");
        let mut img = Mat::default();
        let mut recqr = Mat::default();

        loop{
            match camera.read(&mut img) {
                Err(_) => break,
                _ => (),
            };
            let ret = qr_detector.detect_and_decode(&img, &mut res, &mut recqr).expect("QR DETECT ERROR");
            let s = String::from_utf8_lossy(&ret);
            if s.len() > 0 {
                println!("SENDING: {:?}", s);
                match chl.send(QRThreadResponse::NewQRString(s.to_string())){
                    Err(_) => {println!("QUITTING QR THREAD NQS"); break},
                    _ => (),
                }
            }

            match chl.send(QRThreadResponse::Heartbeat){
                Err(_) => {println!("QUITTING QR THREAD HB"); break},
                _ => (),
            }
        }

    });
    ()
}

In my main GUI thread, I simply call this function when the button is pressed:

    fn spawn_camera_thread(&mut self) {
        if self.QRRecieveThread.is_none() {
            let (snd, rcv) = channel();
            SpawnQrThread(0, snd);
            self.QRRecieveThread = Some(rcv);
        }
    }

And in the GUI thread I check for a response:

if let Some(qr_thread) = &self.QRRecieveThread{
    if let Ok(qr_response) = qr_thread.recv_timeout(Duration::from_millis(1)) {
        match qr_response {
            QRThreadResponse::NewQRString(data) => {
                self.update_activity_log(data);
                self.app_state = AppState::NewItemScanned;
                self.kill_qr_thread();
            },
            QRThreadResponse::Heartbeat => println!("Got Heartbeat"),
            _ => (),
        }
    }
}

How do I figure out why its taking so long for data to be sent between the threads? Could this just be a result of the openCV bindings and the underlying C++ code not sharing the thread time?

I have tried this in Debug and release mode, no change.


Solution

  • The issue turned out to be my own misunderstanding of the egui library. It turns out the egui draw thread is (sensibly) only redrawn when an event occurs, such as mouse/keyboard interaction. In this case, I had to trigger an event so that the egui draw function could pick up the message.

    Note that the 'CPU Usage' section of the egui documentation does clearly state this, but I missed it:

    egui only repaints when there is interaction (e.g. mouse movement) or an animation, so if your app is idle, no CPU is wasted.

    There is a request_repaint() function within the egui::Context (note the documentation states it is within 'Frame' but this seems to no longer be the case), so I had to pass my app_ctx into the QR thread. ctx is cheap to clone and uses ref-counting internally so no issues there.

    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        ...
        self.spawn_camera_thread(ctx.clone());
        ...
    }
    
    fn spawn_camera_thread(&mut self, gui: egui::Context) {
       if self.QRRecieveThread.is_none() {
            let (snd, rcv) = channel();
            self.QRRecieveThread = Some(rcv);
            spawn_qr_thread(0, snd, gui);
        }
    }
    
    pub fn spawn_qr_thread(camera_id: i32, chl: Sender<QRThreadResponse>, gui: Context){
        //QR Detection Code
        match chl.send(QRThreadResponse::NewQRString(s.to_string())){
              Err(_) => {println!("QUITTING QR THREAD"); break},
              _ => gui.request_repaint(),
        }
    }
    
    

    This resolved the latency issues I was having, and I hope this post might help someone else (who like me didn't read the docs thoroughly enough) out in the future.