多线程上的信号量和临界区问题

Semaphore and Critical Section issue on multiple threads

我的多线程代码有问题,希望有人能帮助我。

我希望在控制台上打印所有文件和文件夹,从作为参数给出的文件夹开始。我将此函数用于枚举:

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

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

    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;
}

当我找到文件或文件夹时,我想将它添加到全局队列并让我的工作线程将它打印到控制台。我的工作线程函数是:

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;
}

main中,我初始化了信号量和临界区,这两个变量都是全局声明的:

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

然后创建4个线程:

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);

然后我调用 enumerate() 函数并将字符串发送到队列,这将在到达这些字符串时向我的线程发出停止信号:

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

这 4 个字符串是我线程的停止条件。然后我等待线程:

HANDLE * threadArray = new HANDLE[4];

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

WaitForMultipleObjects(4, threadArray, TRUE, INFINITE);

并关闭信号量和临界区:

CloseHandle(semaphore);
DeleteCriticalSection(&crit);

出于某种原因,输出是随机垃圾,我不明白为什么。

这是一个示例输出:

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

我的逻辑是在 0 上启动信号量,每当队列上发生操作时进入临界区以保护我的数据,在 enumerate() 函数中增加信号量并在 print_queue() 中减少它.

可能是什么问题?

enumerate()许多 个问题:

  • 你没有正确使用 strcpy()strcat(),所以你在浪费内存。您没有分配足够的内存来保存 strcpy() 的结果,它会复制字符直到到达空终止符。您正在为比需要少 2 个字符(路径中的最后一个字符和空终止符)分配内存。您应该分配 strlen+1 个字符而不是 strlen-1 个字符。更糟糕的是,您使用 strcat() 将文件名连接到分配的字符串上,而没有先重新分配字符串以为文件名腾出空间。

  • 您正在泄漏分配的字符串,因为您从未为它调用 delete[]

  • 检查strcmp("..")时,循环内的if丢失了!= 0

  • 您正在将指针推入 queue 以指向 enumerate() 的本地数据,并在每次循环迭代时被覆盖,并在 enumerate() 时超出范围出口。您的线程需要指向稳定且不会在其背后消失的数据的指针。这是垃圾输出的根源。认为自己很幸运,您的代码只是输出垃圾,而不是直接崩溃。

  • 您没有正确测试 data.dwFileAttributes 字段。您需要使用 &(按位与)运算符而不是 ==(等于)运算符。文件夹和文件可以有多个属性,但您只对检查其中一个感兴趣,因此您必须自己测试特定位并忽略其余部分。

您真的应该使用 std::string 来代替字符串管理,让它为您处理内存分配。

此外,考虑使用 std::filesystemboost::filesystem 来处理枚举。

此外,无需在枚举后将 "DONE" 个字符串推入队列。当一个线程收到信号并去提取一个字符串并看到队列为空时,就退出线程。

尝试更像这样的东西:

#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;
}

你没有写出你使用的是哪种队列,但我猜是queue<char*>。这意味着它只存储指向其他地方拥有的内存的指针。

当您现在执行 queue.push(data.cFileName); 时,您写入了一个指向队列的指针,该指针在下一次迭代后无效,因为 data 在那里发生了变化。在 enumerate 存在之后,数据指针(以及队列元素)甚至会指向未定义的内存,这将解释输出。

修复队列中文件名的存储副本,例如通过使用 queue<std::string>