Search code examples
c++multithreadingwinapisemaphorecritical-section

Semaphore and Critical Section issue on multiple threads


I am having an issue with my multithreaded code and hope someone can help me out.

I wish to print on the console all files and folder starting from a folder given as an argument. I use this function for the enumeration:

void enumerate(char* path) {
    HANDLE hFind;
    WIN32_FIND_DATA data;

    char *fullpath = new char[strlen(path) - 1];
    strcpy(fullpath, path);
    fullpath[strlen(fullpath) - 1] = '\0';

    hFind = FindFirstFile(path, &data);
    do {
        if (hFind != INVALID_HANDLE_VALUE) {
            if (strcmp(data.cFileName, ".") != 0 && strcmp(data.cFileName, ".."))
            {
                EnterCriticalSection(&crit);
                queue.push(data.cFileName);
                LeaveCriticalSection(&crit);
                ReleaseSemaphore(semaphore, 1, NULL);
                if (data.dwFileAttributes == FILE_ATTRIBUTE_DIRECTORY)
                {
                    strcat(fullpath, data.cFileName);
                    strcat(fullpath, "\\*");
                    enumerate(fullpath);
                }
            }
        }
    } while (FindNextFile(hFind, &data));
    FindClose(hFind);

    return;
}

When I find a file or a folder, I want to add it to a global queue and have my worker threads print it to the console. My worker threads function is:

DWORD WINAPI print_queue(LPVOID param) {
    while (1) {
        WaitForSingleObject(semaphore, INFINITE);
        EnterCriticalSection(&crit);
        char *rez = queue.front();
        queue.pop();
        LeaveCriticalSection(&crit);

        if (strcmp(rez, "DONE") == 0)
            break;
        else
            std::cout << rez << std::endl;
    }

    return 1;
}

In main, I initialize the semaphore and critical section, both variables declared globally:

semaphore = CreateSemaphore(NULL, 0,1, NULL);
InitializeCriticalSection(&crit);

Then create 4 threads:

thread1 = CreateThread(NULL, 0, print_queue, NULL, 0, &tId1);
thread2 = CreateThread(NULL, 0, print_queue, NULL, 0, &tId2);
thread3 = CreateThread(NULL, 0, print_queue, NULL, 0, &tId3);
thread4 = CreateThread(NULL, 0, print_queue, NULL, 0, &tId4);

I then call the enumerate() function and for strings to the queue that will signal my threads to stop when those strings are reached:

for (int p = 0; p<4; p++)
{
    EnterCriticalSection(&crit);
    queue.push(done);
    LeaveCriticalSection(&crit);
    ReleaseSemaphore(semaphore, 1, NULL);
}

Those 4 strings are the stop condition for my threads. I then wait for the threads:

HANDLE * threadArray = new HANDLE[4];

threadArray[0] = thread1;
threadArray[1] = thread2;
threadArray[2] = thread3;
threadArray[3] = thread4;

WaitForMultipleObjects(4, threadArray, TRUE, INFINITE);

And close the semaphore and critical section:

CloseHandle(semaphore);
DeleteCriticalSection(&crit);

For some reason, the output is random garbage and I can't figure out why.

This is an example output:

te(L┤(L
┤(L 
 ╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠
╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠
╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠°┐*╧wM3╧weµFC4
╠╠╠╠╠

My logic was to start the semaphore on 0, enter the critical section whenever operations happened on a queue to protect my data, increment the semaphore in the enumerate() function and decrease it in print_queue().

What might be the problem?


Solution

  • enumerate() has MANY problems:

    • you are not using strcpy() and strcat() correctly, so you are trashing memory. You are not allocating enough memory to hold the result of strcpy(), which copies characters until it reaches a null terminator. You are allocating memory for 2 fewer characters than needed (the last char in the path, and the null terminator). You should be allocating strlen+1 characters instead of strlen-1 characters. And worse, you are using strcat() to concatenate a filename onto the allocated string without first reallocating the string to make room for the filename.

    • you are leaking the allocated string, as you never call delete[] for it.

    • the if inside the loop is missing != 0 when checking strcmp("..").

    • you are pushing pointers into queue to data that is local to enumerate() and gets overwritten on each loop iteration, and goes out of scope when enumerate() exits. Your threads are expecting pointers to data that are stable and do not disappear behind their backs. This is the root of your garbage output. Consider yourself lucky that your code is simply outputting garbage and not just crashing outright.

    • you are not testing the data.dwFileAttributes field correctly. You need to use the & (bitwise AND) operator instead of the == (equals) operator. Folders and files can have multiple attributes, but you are only interested in checking for one, so you have to test that specific bit by itself and ignore the rest.

    You really should be using std::string instead for string management, and let it handle memory allocations for you.

    Also, consider using std::filesystem or boost::filesystem to handle the enumeration.

    Also, there is no need to push "DONE" strings into the queue after enumerating. When a thread is signaled and goes to extract a string and sees the queue is empty, just exit the thread.

    Try something more like this instead:

    #include <windows.h>
    
    #include <iostream>
    #include <string>
    #include <queue>
    #include <thread>
    #include <mutex>
    #include <conditional_variable>
    
    std::queue<std::string> paths;
    std::mutex mtx;
    std::conditional_variable cv;
    bool done = false;
    
    void enumerate(const std::string &path)
    {
        std::string searchPath = path;
        if ((!searchPath.empty()) && (searchPath[searchPath.length()-1] != '\\'))
            searchPath += '\\';
    
        WIN32_FIND_DATA data;
        HANDLE hFind = FindFirstFileA((searchPath + "*").c_str(), &data);
        if (hFind != INVALID_HANDLE_VALUE)
        {
            do
            {
                if ((strcmp(data.cFileName, ".") != 0) && (strcmp(data.cFileName, "..") != 0))
                {
                    string fullpath = searchPath + data.cFileName;
    
                    {
                    std::lock_guard<std::mutex> lock(mtx);
                    paths.push(fullpath);
                    cv.notify_one();
                    }
    
                    if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
                        enumerate(fullpath);
                }
            }
            while (FindNextFileA(hFind, &data));
            FindClose(hFind);
        }
    }
    
    void print_queue()
    {
        std::unique_lock<std::mutex> lock(mtx);
        while (true)
        {
            cv.wait(lock, [](){ return (!paths.empty()) || done; });
            if (paths.empty())
                return;
    
            std::string rez = paths.front();
            paths.pop();
    
            std::cout << rez << std::endl;
        }
    }
    
    int main()
    {
        std::thread thread1(print_queue);
        std::thread thread2(print_queue);
        std::thread thread3(print_queue);
        std::thread thread4(print_queue);
    
        enumerate("C:\\");
    
        done = true;
        cv.notify_all();
    
        thread1.join();
        thread2.join();
        thread3.join();
        thread4.join();
    
        return 0;
    }