线程池
由于上一节滥用多线程,导致扫描时间变长,并且结果也不正确,原因就在于开启的线程太多太多了,导致很多时候开启线程失败,并且我们那样做的方式也不对,因为一个文件夹中也就几十个文件,扫描一个文件夹也就是1um,而开启一个线程就需要2us,关闭一个线程也需要2us。一个C盘有好几万个文件夹,也就是说我们需要开启好几万个线程,天哪!!!这简直就是一个天文数字!!!这样一来,开启新线程反而降低了效率,所以我们不能频繁的开启、关闭线程。那么不这样做,那怎么做呢?这就是我们要说的线程池。
我们在程序扫描运行之前就开启一定数量的线程,在扫描完成之后再把这些线程关闭,这样就大大节省了线程创建和销毁的时间。
需要注意的是:并不是说前面那种创建线程的方式就一定不好,只不过不适用于我们目前的文件扫描,如果需要一个文件夹中有几十万个文件,每个文件夹的扫描需要很长的时间(相对于线程的创建和销毁时间),这样就适合遇到一个文件夹就创建一个线程的方式来扫描了。
对于线程池,我们创建一个多大的线程池呢?一般来说,线程池中线程的数量一般是CPU个数的两倍。比如CPU的个数是8个,那么一般就创建一个含有16个线程的线程池。
代码实现
#include <string>
#include <stdio.h>
#include <process.h>
#include <Windows.h>
#include <vector>
HANDLE g_hExitEvent, g_hPushDirEvent;
CRITICAL_SECTION g_cs;
long g_lFindFileNumber = 0, g_lSearchFileNumber = 0, g_lSearchDirectoryNumber = 0, g_lWaitThreadNum = 0;
DWORD g_dwThreadNum = 0;
std::vector<std::wstring> g_vecDirNames;
std::wstring MakeStandardDir(std::wstring wstrDir)
{
if (wstrDir.back() != L'\\')
{
wstrDir += L'\\';
}
return wstrDir;
}
unsigned int __stdcall ThreadFunc(void *lParam)
{
std::wstring *pWstrSerach = static_cast<std::wstring *>(lParam);
std::wstring wstrDirName;
BOOL bRunnable = TRUE;
while (true)
{
EnterCriticalSection(&g_cs);
if (g_vecDirNames.empty())
{
bRunnable = FALSE;
}
else
{
wstrDirName = g_vecDirNames.back();
g_vecDirNames.pop_back();
}
LeaveCriticalSection(&g_cs);
if (!bRunnable)
{
ResetEvent(g_hPushDirEvent);
InterlockedAdd(&g_lWaitThreadNum, 1);
if (g_lWaitThreadNum == g_dwThreadNum)
SetEvent(g_hExitEvent);
WaitForSingleObject(g_hPushDirEvent, INFINITE);
InterlockedAdd(&g_lWaitThreadNum, -1);
bRunnable = TRUE;
continue;
}
// 扫描
WIN32_FIND_DATA find_data = { 0 };
HANDLE hFile = FindFirstFileW((MakeStandardDir(wstrDirName) + L"*.*").c_str(), &find_data);
if (INVALID_HANDLE_VALUE != hFile)
{
do
{
if (wcscmp(find_data.cFileName, L".") == 0)
{
continue;
}
if (wcscmp(find_data.cFileName, L"..") == 0)
{
continue;
}
if (find_data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
EnterCriticalSection(&g_cs);
g_vecDirNames.push_back(MakeStandardDir(wstrDirName) + find_data.cFileName);
InterlockedAdd(&g_lSearchDirectoryNumber, 1);
SetEvent(g_hPushDirEvent);
LeaveCriticalSection(&g_cs);
}
else
{
if (wcsstr(find_data.cFileName, pWstrSerach->c_str()) != nullptr)
{
printf("%ls\r\n", (MakeStandardDir(wstrDirName) + find_data.cFileName).c_str());
InterlockedAdd(&g_lFindFileNumber, 1);
}
InterlockedAdd(&g_lSearchFileNumber, 1);
}
} while (FindNextFileW(hFile, &find_data));
if (!FindClose(hFile))
printf("error no:%d\r\n", errno);
}
}
return 0;
}
int main()
{
InitializeCriticalSection(&g_cs);
g_hExitEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
g_hPushDirEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
do
{
if (NULL == g_hExitEvent)
{
printf("创建 g_hExitEvent 失败\r\n");
break;
}
if (NULL == g_hPushDirEvent)
{
printf("创建 g_hPushDirEvent 失败\r\n");
break;
}
std::vector<HANDLE> vecHandles;
SYSTEM_INFO system_info = { 0 };
GetSystemInfo(&system_info);
g_dwThreadNum = system_info.dwNumberOfProcessors * 2;
std::wstring wstrSearch = L"ntdll";
g_vecDirNames.push_back(L"C:");
DWORD dwPrevTime = GetTickCount();
for (DWORD i = 0; i < g_dwThreadNum; ++i)
{
HANDLE handle = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, &wstrSearch, 0, nullptr);
if (NULL == handle)
--i;
else
vecHandles.push_back(handle);
}
WaitForSingleObject(g_hExitEvent, INFINITE);
DWORD dwTimeUse = (GetTickCount() - dwPrevTime) / 1000;
for (auto handle : vecHandles)
{
//_endthreadex((unsigned)handle);
CloseHandle(handle);
}
// msdn的解释
// https://msdn.microsoft.com/zh-cn/library/kdzttdcb.aspx
printf("共找到 %lu 个 ntdll 相关的文件!\r\n", g_lFindFileNumber);
printf("共查找了 %lu 个文件夹, %lu 个文件!\r\n", g_lSearchDirectoryNumber, g_lSearchFileNumber);
printf("用时 %lu 秒\r\n", dwTimeUse);
}
while (false);
if (NULL != g_hExitEvent)
{
CloseHandle(g_hExitEvent);
}
if (NULL != g_hPushDirEvent)
{
CloseHandle(g_hPushDirEvent);
}
DeleteCriticalSection(&g_cs);
system("pause");
return 0;
}
扫描结果如下:
仅仅用了7秒钟,较之于递归的37秒,已经提高了很大的效率。这个程序没有问题,有几点需要我们注意:
- g_hExitEvent,主要是用来判断程序合适退出的事件内核对象;
- g_hPushDirEvent,主要是用来判断是否新加入了一个文件夹;
- g_lWaitThreadNum,等待线程的个数,这是个很重要的变量,通过它才能正确的判断文件是否扫描完成。
其它的就没有什么特别需要注意了。
回顾时间内核对象的手动和自动方式:
手动方式当创建事件内核对象为手动方式的时候,在SetEvent的时候,所有的线程都会被激活,都会直接往下运行,也就是说一个线程池中一次可以启动多个线程;
自动方式当创建事件内核对象为自动方式的时候,一个线程池中一次只能启动多个线程。
当然,对于我们当前的情况下,设置手动和设置自动的效果都是一样的。但是我们在设计线程池的时候,一定要设置为手动方式,因为有些逻辑可能一下要同时开启多个线程。