本节书摘来自异步社区出版社《C++多线程编程实战》一书中的第2章,第2.9节,作者: 【黑山共和国】Milos Ljumovic(米洛斯 留莫维奇),更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.9 在用户空间实现线程
既可以在用户空间也可以在内核中实现线程包。具体选择在哪里实现还存在一些争议,在一些实现中可能会混合使用内核线程和用户线程。
我们将讨论在不同地方实现线程包的方法及优缺点。第1种方法是,把整个线程包放进用户空间,内核完全不知道。就内核而言,它管理着普通的单线程进程。这种方法的优点和最显著的优势是,可以在不支持线程的操作系统中实现用户级线程包。
过去,传统的操作系统就采用这种方法,甚至沿用至今。用这种方法,线程可以通过库来实现。所有这些实现都具有相同的通用结构。线程运行在运行时系统的顶部,该系统是专门管理线程的过程集合。我们在前面见过一些例子(CreateThread
、TerminateThread
等),以后还会见到更多。
下面的程序示例演示了在用户空间中的线程用法。我们要复制大型文件,但是不想一开始就读取整个文件的内容,或者更优化地一部分一部分地读取,而且不用在文件中写入数据。这就涉及2.5节中提到的生产者-消费者问题。
准备就绪
确定安装并运行了Visual Studio。
操作步骤
1. 创建一个新的Win32应用程序项目,并命名为ConcurrentFileCopy
。
2. 打开【解决方案资源管理器】,添加一个新的头文件,命名为ConcurrentFileCopy.h
。打开ConcurrentFileCopy.h
,并输入下面的代码:
#pragma once
#include <windows.h>
#include <commctrl.h>
#include <memory.h>
#include <tchar.h>
#include <math.h>
#pragma comment ( lib, "comctl32.lib" )
#pragma comment ( linker, "\"/manifestdependency:type='win32' \
name='Microsoft.Windows.Common-Controls' \
version='6.0.0.0' processorArchitecture='*' \
publicKeyToken='6595b64144ccf1df' language='*'\"" )
ATOM RegisterWndClass(HINSTANCE hInstance);
HWND InitializeInstance(HINSTANCE hInstance, int nCmdShow, HWND& hWndPB);
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
DWORD WINAPI ReadRoutine(LPVOID lpParameter);
DWORD WINAPI WriteRoutine(LPVOID lpParameter);
BOOL FileDialog(HWND hWnd, LPTSTR szFileName, DWORD
dwFileOperation);
DWORD GetBlockSize(DWORD dwFileSize);
#define BUTTON_CLOSE 100
#define FILE_SAVE 0x0001
#define FILE_OPEN 0x0002
#define MUTEX_NAME _T("__RW_MUTEX__")
typedef struct _tagCOPYDETAILS
{
HINSTANCE hInstance;
HWND hWndPB;
LPTSTR szReadFileName;
LPTSTR szWriteFileName;
} COPYDETAILS, *PCOPYDETAILS;```
3.现在,打开【解决方案资源管理器】,并添加一个新的源文件,命名为`ConcurrentFileCopy.cpp`。打开`ConcurrentFileCopy.c`,并输入下面的代码:
include "ConcurrentFileCopy.h"
TCHAR* szTitle = _T("Concurrent file copy");
TCHAR* szWindowClass = T(" _CFC_WND_CLASS_ _");
DWORD dwReadBytes = 0;
DWORD dwWriteBytes = 0;
DWORD dwBlockSize = 0;
DWORD dwFileSize = 0;
HLOCAL pMemory = NULL;
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrev, LPTSTR
szCmdLine, int iCmdShow)
{
UNREFERENCED_PARAMETER(hPrev);
UNREFERENCED_PARAMETER(szCmdLine);
RegisterWndClass(hInstance);
HWND hWnd = NULL;
HWND hWndPB = NULL;
if (!(hWnd = InitializeInstance(hInstance, iCmdShow, hWndPB)))
{
return 1;
}
MSG msg = { 0 };
TCHAR szReadFile[MAX_PATH];
TCHAR szWriteFile[MAX_PATH];
if (FileDialog(hWnd, szReadFile, FILE_OPEN) && FileDialog(hWnd,
szWriteFile, FILE_SAVE))
{
COPYDETAILS copyDetails = { hInstance, hWndPB, szReadFile,
szWriteFile };
HANDLE hMutex = CreateMutex(NULL, FALSE, MUTEX_NAME);
HANDLE hReadThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)ReadRoutine, ©Details, 0, NULL);
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
CloseHandle(hReadThread);
CloseHandle(hMutex);
}
else
{
MessageBox(hWnd, _T("Cannot open file!"),
_T("Error!"), MB_OK);
}
LocalFree(pMemory);
UnregisterClass(szWindowClass, hInstance);
return (int)msg.wParam;
}
ATOM RegisterWndClass(HINSTANCE hInstance)
{
WNDCLASSEX wndEx;
wndEx.cbSize = sizeof(WNDCLASSEX);
wndEx.style = CS_HREDRAW | CS_VREDRAW;
wndEx.lpfnWndProc = WndProc;
wndEx.cbClsExtra = 0;
wndEx.cbWndExtra = 0;
wndEx.hInstance = hInstance;
wndEx.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
wndEx.hCursor = LoadCursor(NULL, IDC_ARROW);
wndEx.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wndEx.lpszMenuName = NULL;
wndEx.lpszClassName = szWindowClass;
wndEx.hIconSm = LoadIcon(wndEx.hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
return RegisterClassEx(&wndEx);
}
HWND InitializeInstance(HINSTANCE hInstance, int iCmdShow, HWND& hWndPB)
{
HWND hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPED
| WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, 200, 200, 440, 290,
NULL, NULL, hInstance, NULL);
RECT rcClient = { 0 };
int cyVScroll = 0;
if (!hWnd)
{
return NULL;
}
HFONT hFont = CreateFont(14, 0, 0, 0, FW_NORMAL, FALSE, FALSE,
FALSE, BALTIC_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY, DEFAULT_PITCH | FF_MODERN,
_T("Microsoft Sans Serif"));
HWND hButton = CreateWindow(_T("BUTTON"), _T("Close"), WS_CHILD
| WS_VISIBLE | BS_PUSHBUTTON | WS_TABSTOP, 310, 200, 100, 25,
hWnd, (HMENU)BUTTON_CLOSE, hInstance, NULL);
SendMessage(hButton, WM_SETFONT, (WPARAM)hFont, TRUE);
GetClientRect(hWnd, &rcClient);
cyVScroll = GetSystemMetrics(SM_CYVSCROLL);
hWndPB = CreateWindow(PROGRESS_CLASS, (LPTSTR)NULL, WS_CHILD |
WS_VISIBLE, rcClient.left, rcClient.bottom - cyVScroll,
rcClient.right, cyVScroll, hWnd, (HMENU)0, hInstance, NULL);
SendMessage(hWndPB, PBM_SETSTEP, (WPARAM)1, 0);
ShowWindow(hWnd, iCmdShow);
UpdateWindow(hWnd);
return hWnd;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_COMMAND:
{
switch (LOWORD(wParam))
{
case BUTTON_CLOSE:
{
DestroyWindow(hWnd);
break;
}
}
break;
}
case WM_DESTROY:
{
PostQuitMessage(0);
break;
}
default:
{
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
}
return 0;
}
DWORD WINAPI ReadRoutine(LPVOID lpParameter)
{
PCOPYDETAILS pCopyDetails = (PCOPYDETAILS)lpParameter;
HANDLE hFile = CreateFile(pCopyDetails->szReadFileName,
GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == (HANDLE)INVALID_HANDLE_VALUE)
{
return FALSE;
}
dwFileSize = GetFileSize(hFile, NULL);
dwBlockSize = GetBlockSize(dwFileSize);
HANDLE hWriteThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)WriteRoutine, pCopyDetails, 0, NULL);
size_t uBufferLength = (size_t)ceil((double) dwFileSize / (double)dwBlockSize);
SendMessage(pCopyDetails->hWndPB, PBM_SETRANGE, 0,
MAKELPARAM(0, uBufferLength));
pMemory = LocalAlloc(LPTR, dwFileSize);
void* pBuffer = LocalAlloc(LPTR, dwBlockSize);
int iOffset = 0;
DWORD dwBytesRed = 0;
do
{
ReadFile(hFile, pBuffer, dwBlockSize, &dwBytesRed, NULL);
if (!dwBytesRed)
{
break;
}
HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE,
MUTEX_NAME);
WaitForSingleObject(hMutex, INFINITE);
memcpy((char*)pMemory + iOffset, pBuffer, dwBytesRed);
dwReadBytes += dwBytesRed;
ReleaseMutex(hMutex);
iOffset += (int)dwBlockSize;
} while (true);
LocalFree(pBuffer);
CloseHandle(hFile);
CloseHandle(hWriteThread);
return 0;
}
DWORD WINAPI WriteRoutine(LPVOID lpParameter)
{
PCOPYDETAILS pCopyDetails = (PCOPYDETAILS)lpParameter;
HANDLE hFile = CreateFile(pCopyDetails->szWriteFileName,
GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == (HANDLE)INVALID_HANDLE_VALUE)
{
return FALSE;
}
DWORD dwBytesWritten = 0;
int iOffset = 0;
do
{
int iRemainingBytes = (int)dwFileSize - iOffset;
if (iRemainingBytes <= 0)
{
break;
}
Sleep(10);
if (dwWriteBytes < dwReadBytes)
{
DWORD dwBytesToWrite = dwBlockSize;
if (!(dwFileSize / dwBlockSize))
{
dwBytesToWrite = (DWORD)iRemainingBytes;
}
HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, MUTEX_NAME);
WaitForSingleObject(hMutex, INFINITE);
WriteFile(hFile, (char*)pMemory + iOffset, dwBytesToWrite,
&dwBytesWritten, NULL);
dwWriteBytes += dwBytesWritten;
ReleaseMutex(hMutex);
SendMessage(pCopyDetails->hWndPB, PBM_STEPIT, 0, 0);
iOffset += (int)dwBlockSize;
}
} while (true);
CloseHandle(hFile);
return 0;
}
BOOL FileDialog(HWND hWnd, LPTSTR szFileName, DWORD dwFileOperation)
{
ifdef _UNICODE
OPENFILENAMEW ofn;
else
OPENFILENAMEA ofn;
endif
TCHAR szFile[MAX_PATH];
ZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = hWnd;
ofn.lpstrFile = szFile;
ofn.lpstrFile[0] = '0';
ofn.nMaxFile = sizeof(szFile);
ofn.lpstrFilter = _T("All0.0Text0*.TXT0");
ofn.nFilterIndex = 1;
ofn.lpstrFileTitle = NULL;
ofn.nMaxFileTitle = 0;
ofn.lpstrInitialDir = NULL;
ofn.Flags = dwFileOperation == FILE_OPEN ? OFN_PATHMUSTEXIST |
OFN_FILEMUSTEXIST : OFN_SHOWHELP | OFN_OVERWRITEPROMPT;
if (dwFileOperation == FILE_OPEN)
{
if (GetOpenFileName(&ofn) == TRUE)
{
_tcscpy_s(szFileName, MAX_PATH - 1, szFile);
return TRUE;
}
}
else
{
if (GetSaveFileName(&ofn) == TRUE)
{
_tcscpy_s(szFileName, MAX_PATH - 1, szFile);
return TRUE;
}
}
return FALSE;
}
DWORD GetBlockSize(DWORD dwFileSize)
{
return dwFileSize > 4096 ? 4096 : 512;
}`
示例分析
我们创建了一个和哲学家就餐示例非常像的UI。例程MyRegisterClass
、InitIn
stance和WndProc
几乎都一样。我们在程序中添加FileDialog
来询问用户读写文件的路径。为了读和写,分别启动了两个线程。
操作系统的调度十分复杂。我们根本不知道是调度算法还是硬件中断使得某线程被调度在CUP中执行。这意味着写线程可能在读线程之前执行。出现这种情况会导致一个异常,因为写线程没东西可写。
因此,我们在写操作中添加了if
条件,如下代码所示:
if ( dwBytesWritten < dwBytesRead )
{
WriteFile(hFile, pCharTmp, sizeof(TCHAR) * BLOCK_SIZE, &dwFileSize, NULL);
dwBytesWritten += dwFileSize;
SendMessage( hProgress, PBM_STEPIT, 0, 0 );
}```
线程在获得互斥量后,才能执行写操作。尽管如此,系统仍然有可能在读线程之前调度写线程,此时缓冲区是空的。因此,每次读线程获得一些内容,就要把读取的字节数加给dwBytesRed变量,只有写线程的字节数小于读线程的字节数,才可以执行写操作。否则,本轮循环将跳过写操作,并释放互斥量供其他线程使用。
更多讨论