1

I have some code that runs a bunch of threads in parallel. Each thread execute a function in order to copy a "watermark" over an image loaded from a directory, and save the modified image in another directory, specified by the user. In order to achieve the result I have to use the CImg library. I can't figure out why sometimes CImg throws CImgIOException like CImg<unsigned char>::save_other(): Failed to save file, or CImg<unsigned char>::save_jpeg(): Failed to save file. I have imagemagick installed, so there shouldn't be a problem to save and open images using the jpeg/jpg format.

This is my code:

#include <atomic>
#include <chrono>
#include "CImg.h"
#include <cstdlib>
#include <filesystem>
#include <functional>
#include <iostream>
#include <mutex>
#include <optional>
#include <queue>
#include <string>
#include <thread>
#include <typeinfo>

using namespace cimg_library;

std::mutex IMAGES_MUTEX, IO_MUTEX;
std::atomic_int PROCESSED_IMAGES = 0;

void apply_watermark(std::queue<std::string>& images, CImg<unsigned char>& watermark, int workload, \
                     std::string output_dir, int id) {
    std::queue<std::string> to_process;
    int counter = 0;
    bool stop = false;
    CImg<unsigned char> img;

    while (!stop) {
        IMAGES_MUTEX.lock();
        while(counter < workload) {
            if (images.empty()) {
                std::cout << "Thread " << id << ": founded empty queue" << std::endl;
                counter = workload;
                stop = true;
            } else {
                std::cout << "Thread " << id << ": aquiring image" << images.front() << std::endl;
                to_process.push(images.front());
                images.pop();
                counter += 1;
            }
        }
        IMAGES_MUTEX.unlock();

        counter = 0;

        while(!(to_process.empty())) {
            img.assign(to_process.front().c_str());

            if (!(img.width() != 1024 || img.height() != 768)) {
                cimg_forXY(watermark, x, y) {
                    int R = (int)watermark(x, y, 0, 0);

                    if (R != 255) {
                        img(x, y, 0, 0) = 0;
                        img(x, y, 0, 1) = 0;
                        img(x, y, 0, 2) = 0;
                    }
                }

                std::string fname = to_process.front().substr(to_process.front().find_last_of('/') + 1);

                IO_MUTEX.lock();
                std::cout << "Thread " << id << ": saving image " << fname << std::endl;
                IO_MUTEX.unlock();

                img.save_jpeg(((std::string)output_dir + (std::string)"/" + fname).c_str());
                to_process.pop();
                PROCESSED_IMAGES += 1;
            } else {
                to_process.pop();
            }
            img.clear();
        }
    }
    return;
}

int main(int argc, char const *argv[]) {
    auto completion_time_start = std::chrono::high_resolution_clock::now();

    if (argc != 5) {
        std::cout << "MISSING PARAMETERS!" << std::endl;

        return -1;
    }

    int par_degree = std::atoi(argv[3]);

    if (!(std::filesystem::exists(argv[2]))) {
        std::cout << "WATERMARK NOT FOUND!" << std::endl;

        return -1;
    }

    CImg<unsigned char> wtrk(argv[2]);
    std::queue<std::string> images;

    if (!(std::filesystem::exists(argv[1]))) {
        std::cout << "IMAGES' DIRECTORY NOT FOUND!" << std::endl;

        return -1;
    }

    for (auto& path : std::filesystem::directory_iterator(argv[1])) {
        std::string fname = path.path().string().substr(path.path().string().find_last_of('/') + 1);

        if (fname != ".DS_Store") {
            images.push(path.path().string());
        }
    }

    if (!(std::filesystem::exists((std::string)argv[4]))) {
        std::filesystem::create_directory((std::string)argv[4]);
    }

    if (par_degree == 1) {
        while(!(images.empty())) {
            CImg<unsigned char> img(images.front().c_str());

            if (!(img.width() != 1024 || img.height() != 768)) {
                cimg_forXY(wtrk, x, y) {
                    int R = (int)wtrk(x, y, 0, 0);

                    if (R != 255) {
                        img(x, y, 0, 0) = 0;
                        img(x, y, 0, 1) = 0;
                        img(x, y, 0, 2) = 0;
                    }
                }

                std::string fname = images.front().substr(images.front().find_last_of('/') + 1);
                img.save_jpeg(((std::string)argv[4] + (std::string)"/" + fname).c_str());
                images.pop();
                PROCESSED_IMAGES += 1;
            } else {
                images.pop();
            }
        }
    } else {
        int workload = (int)images.size() / par_degree;
        std::thread workers[par_degree];

        for (int i = 0; i < par_degree; i++) {
            workers[i] = std::thread(apply_watermark, std::ref(images), std::ref(wtrk), workload, \
                                     (std::string)argv[4], i);
        }

        for (int i = 0; i < par_degree; i++) {
            workers[i].join();
        }
    }

    auto completion_time_end = std::chrono::high_resolution_clock::now();
    std::chrono::duration<double, std::ratio<1>> completion_time = completion_time_end - \
                                                                   completion_time_start;

    std::cout << "\nPARALLELISM DEGREE: " << par_degree << std::endl;
    std::cout << "COMPLETION TIME: " << completion_time.count() << " SECONDS" << std::endl;
    std::cout << "PROCESSED IMAGES: " << PROCESSED_IMAGES << std::endl;

    return 0;
}

All the exceptions are throwed during the saving of an image, I'm quite new to the C++ language, and I really have no clue. Running the program with only one thread, the main one, doesn't create any problem.

This is an example of launch command:

./test ../imgs ../watermark.jpg 4 ../output_dir

Any help will be appreciated.

g_rmz
  • 721
  • 2
  • 8
  • 20
  • Simplify and [learn how to debug your programs](https://ericlippert.com/2014/03/05/how-to-debug-small-programs/). An important simplification you should start with: Don't use threads. In fact, you should always make sure your algorithms work single-threaded before you start using threads. Another simplification is to not write to much code at once before testing. Start small, add new things in small, small baby-steps, and test between each baby-step. – Some programmer dude Jul 04 '18 at 19:35
  • Also, don't try to save space by using abbreviation, especially if they don't really save much (and only in the source code itself). For example, why use `wtrk` instead of the perfectly find `watermark`? The latter is *much* easier to understand what it is than the cryptic `wtrk`. Compilers these days can handle symbols longer than six characters. – Some programmer dude Jul 04 '18 at 19:38
  • @Someprogrammerdude As I wrote, I've already written the sequential version of the program. It is in the main function, and it starts when the parallelism degree is 1. I appreciate your suggestions, but I continue to don't understand why this exceptions are launched. I've tested every line of code and this problem persists. For the debugging part I've used lldb, and still found nothing. – g_rmz Jul 04 '18 at 19:42
  • It seems that the exception happens when you attempt to save the image. What is the file-name you try to use at that time? Is it a valid name? Is it a valid path? You're allowed to write to that path and file? Does every part of the path exist? Do the file already exist? – Some programmer dude Jul 04 '18 at 19:56
  • Oh, and should you really unlock `IO_MUTEX` before you actually *do* any IO (i.e. save the image to file)? – Some programmer dude Jul 04 '18 at 19:59
  • @Someprogrammerdude In order to save an image I use a c_string, for example, like this ../output_dir/1.jpg, where 1.jpg is an image I take from the directory in which all the images to be processed are contained, and ../output_dir is the destination directory I pass in input and, in case it doesn't exists, is created. And yes, the critical sections is resolved before any IO operation. – g_rmz Jul 04 '18 at 20:01
  • All you do in the `IO_MUTEX` "critical section" is *print a string*, and you print some values that aren't even thread-sensitive (`id` is passed by value, and `fname` is a local variable). That mutex just doesn't make any sense there. And to be certain that it works okay, *check* the actual value of `output_dir + "/" + fname` (note that the casts aren't needed). – Some programmer dude Jul 04 '18 at 20:03
  • @Someprogrammerdude I use the critical section to access the images queue, which is shared among all threads and hence requires a lock to be accessed. – g_rmz Jul 04 '18 at 20:04
  • And I don't talk about `IMAGE_MUTEX`, but the (in the code you show) useless `IO_MUTEX`. – Some programmer dude Jul 04 '18 at 20:05
  • @Someprogrammerdude Oh yes, I was using that mutex in order to debug the code and print without the interleaving of the threads. – g_rmz Jul 04 '18 at 20:07
  • Anyway, my best recommendation is still to scale back and simplify. Comment out all of the code in the thread function. Then add back one little bit att a time, testing multiple times before adding back the next little bit. Once you start to get the exceptions again then you at least should have narrowed down the problematic parts. And one of the early bits you should put back in is the writing of the (empty!) image. – Some programmer dude Jul 04 '18 at 20:12

1 Answers1

0

I suspect the write command isn't thread safe. For example, most jpeg encoders/decoders use a structure which needs to be made for each thread, here they might be shared. Despite the claims on the cimg website, it is not immediately obvious if/how this taken into consideration.

I'd test the hypothesis by serializing execution of the .save_jpeg command, although the solution will come from not using cimg, but instead calling libjpeg-turbo directly:

//somewhere in global
std::mutex protect_save_image;

//At your section
{
    std::unique_lock lk(protect_save_image);
    img.save_jpeg(((std::string)argv[4] + (std::string)"/" + fname).c_str());`
}

Also if the thread count is really high (aka par_degree > 1000), you might exceed the maximum number of available file descriptions causing IO failure.

Mikhail
  • 7,749
  • 11
  • 62
  • 136
  • I've come to the same conclusion as yours. While trying to solve the issue I've putted the saving function between a try/catch block, in which the catch-part also apply the saving function, and this has solved the problem, for now. I've not considered the use of another mutex for the saving function in order to optimize the performance of the program. I'll mark your answer as correct, thank you :) – g_rmz Jul 11 '18 at 08:08
  • @g_rmz as a friendly word of apocryphal advice, your code will probably benefit from multi threads. 1) The saving jpeg command performs a substantial amount of computation when compressing the data 2) most IO system are made of many layers that need to be saturated to achieve peak performance (even a single HDD needs to be saturated to keep the head always writing). You gotta call libjpeg-turbo directly. – Mikhail Jul 11 '18 at 19:20