Repository: shssoichiro/oxipng (language: rust)

Issue:
Parallel mode hangs when invoked from Rayon global thread pool
My app needs to generate and optimize multiple images in parallel, so it uses the Rayon global thread pool. However, the time these images take varies unpredictably, so utilization at the end would be higher if I could also use Oxipng's parallel mode. Unfortunately, Oxipng deadlocks when I invoke oxipng from within a Rayon task in the global thread pool. Here's the app where I'm experiencing this: https://app.circleci.com/pipelines/github/Pr0methean/OcHd-RustBuild?branch=oxipng-parallel


Discussion / hints:
I've tried wrapping the `optimize_from_memory` call in `ThreadPool::install` with an explicitly-created thread pool, but that leads to starvation of the global pool and out-of-memory errors. A workaround would be if oxipng made the interface of `RowFilter` public, in which case I could create a separate task for each filter algorithm that should be tried.
Is this on master or v8? Could you try some older versions perhaps?
Do you know the exact point that it deadlocks?
Master.
Maybe this is relevant? https://github.com/rayon-rs/rayon/issues/592
Oxipng explicitly avoids using mutexes in this way, but perhaps your code does?

Out of interest, are you using the new raw API?
That bug is unlikely to be relevant, since the PNG-writing task that invokes Oxipng calls into_inner() on its Mutex before running.

I'm not using the raw API but will look into it.
As I'm not exactly an expert in Rayon, I'm afraid there's not much else I can do to help, sorry. Perhaps @kornelski may have some ideas?
Very simple to reproduce, using this code as an example. My CPU has 12 threads. This works perfectly well with 11, but as soon as I uncomment 12, none of them finish, there is no CPU activity and the program just hangs. On an 8 thread CPU, it works with 7, but hangs with 8, etc.

```rs
use rayon::prelude::*;
use oxipng::{optimize_from_memory, Options, Deflaters};

fn main() {
    let buffer = include_bytes!("sample.png").as_slice();

    let options = Options {
        deflate: Deflaters::Libdeflater { compression: 12 },
        ..Default::default()
    };

    vec![
        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
        // 12,
    ].par_iter().for_each(|x| {
        println!("Optimizing {x}");
        optimize_from_memory(&buffer, &options).unwrap();
        println!("Done {x}");
    });
}
```

Fix patch:
diff --git a/src/evaluate.rs b/src/evaluate.rs
index 0d187198..da84ac13 100644
--- a/src/evaluate.rs
+++ b/src/evaluate.rs
@@ -85,7 +85,10 @@ impl Evaluator {
     #[cfg(feature = "parallel")]
     pub fn get_best_candidate(self) -> Option<Candidate> {
         let (eval_send, eval_recv) = self.eval_channel;
-        drop(eval_send); // disconnect the sender, breaking the loop in the thread
+        // Disconnect the sender, breaking the loop in the thread
+        drop(eval_send);
+        // Yield to ensure evaluations are finished - this can prevent deadlocks when run within an existing thread pool
+        while let Some(rayon::Yield::Executed) = rayon::yield_local() {}
         eval_recv.into_iter().min_by_key(Candidate::cmp_key)
     }
 


Test patch: