《C++多線程編程實戰》——2.9 在用戶空間實現線程
本節書摘來自異步社區出版社《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("All\0*.*\0Text\0*.TXT\0");
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;
}``
MyRegisterClass
示例分析
我們創建了一個和哲學家就餐示例非常像的UI。例程、
InitInstance和
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變量,隻有寫線程的字節數小於讀線程的字節數,才可以執行寫操作。否則,本輪循環將跳過寫操作,並釋放互斥量供其他線程使用。
更多討論
生產者-消費者問題也稱為有界緩衝問題。兩個進程共享一個固定大小的公共緩衝區。生產者把信息放入緩衝區,消費者把信息從緩衝區中取出來。該問題也可擴展為m個生產者和n個消費者的問題。不過,這裏我們簡化了問題,隻考慮一個生產者和一個消費者。當生產者往緩衝區放入新項目時緩衝區滿了,就會產生問題。解決的方案是,讓生產者睡眠,並在消費者已經移除一個或多個項時喚醒生產者。同理,如果消費者要從緩衝區取項目時緩衝區為空,也會產生問題。解決的方案是,讓消費者睡眠,等生產者把項目放入緩衝區後再喚醒它。這種方法看起來很簡單,但是會導致競態條件。讀者可以用學過的知識,嚐試解決類似的情況。
最後更新:2017-06-01 13:01:17