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

Posted

技术标签:

【中文标题】多个线程上的信号量和临界区问题【英文标题】:Semaphore and Critical Section issue on multiple threads 【发布时间】:2018-09-09 21:30:45 【问题描述】:

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

我希望在控制台上打印从作为参数给出的文件夹开始的所有文件和文件夹。我使用这个函数进行枚举:

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;

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

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 ╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠ ╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠╠ / ╠╠╠╠╠

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

可能是什么问题?

【问题讨论】:

队列的声明是什么样子的? 我强烈建议放弃 STL 的 winapi 内容 -- std::mutexconditon_variableasyncqueue... 至少,将 CRITICAL_SECTION 包装在 RAII 类中. 文件夹不仅可以包含FILE_ATTRIBUTE_DIRECTORY 属性集。请改用if (data.dwFileAttributes &amp; FILE_ATTRIBUTE_DIRECTORY) 为什么你不使用内置的 Windows 队列实现 - iocp - 有了这个你就不需要任何关键部分、信号量等。 与您的代码的大量问题无关,我不相信在此设置中使用工作线程会给您带来任何开销。 【参考方案1】:

enumerate()很多个问题:

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

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

在检查strcmp("..") 时,循环内的if 缺少!= 0

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

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

你真的应该使用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;

【讨论】:

感谢您的帮助,我已经解决了我的问题,通过您的详细解释学到了一些东西!主要问题是没有使用 std::string !【参考方案2】:

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

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

要修复队列中文件名的此存储副本,例如通过使用queue&lt;std::string&gt;

【讨论】:

是的,它是一个队列,我相信这是我的问题的主要原因。您的回答是对我问题的简洁解决方案!谢谢!

以上是关于多个线程上的信号量和临界区问题的主要内容,如果未能解决你的问题,请参考以下文章

临界数据临界区和原子操作

用互斥锁保护临界区

线程同步的方式和机制

进程之间的通信

Linux内核自旋锁

win32下多线程同步方式之临界区,互斥量,事件对象,信号量