本节书摘来自异步社区出版社《C++多线程编程实战》一书中的第2章,第2.7节,作者: 【黑山共和国】Milos Ljumovic(米洛斯 留莫维奇),更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.7 线程模型的实现
我们可以把进程看作是一个对象,它的任务就是把相关资源分组。每个进程都有一个地址空间,如图2.10所示。

图2.10 进程的地址空间
这个所谓的进程图像必须在初始化CreateProcess
时加载至物理内存中。所有的资源(如文件句柄、子进程的信息、信号处理器等)都被储存起来。把它们以进程的形式分组在一起,更容易管理。
除进程外,还有一个重要的概念是线程。线程是CPU可执行调度的最小单位。也就是说,进程本身不能获得CPU时间,只有它的线程才可以。线程通过它的工作变量和栈来储存CPU寄存器的信息。栈包含与函数调用相关的数据,在每个函数被调用但尚未返回时,为其创建一个框架。线程可以在CPU上执行,而进程则不行。但是,进程至少必须有一个线程,通常把这个线程称为主线程。因此,当我们说在CPU上执行的进程时,指的是进程中的主线程。
进程用于分组资源,线程是在CPU上调度执行的实体。在同一个进程环境中可以执行多个线程,理解这点很重要。多线程并行运行在一个进程上下文,与在一个计算机中并行运行的多个进程相同。术语“多线程”指的是在单进程上下文中运行的多线程。
如图2.11所示,有3个进程,每个进程中都有一个线程。

图2.11 3个进程中各有1个线程
图2.12演示了一个有3个线程的进程。虽然这两种情况中都有3个线程,但是在图2.11中,每个线程都在不同的地址空间中运行,而图2.12中的3个线程共享同一个地址空间。

图2.12 有3个线程的进程
在单核CPU系统中运行多线程的进程时,各线程轮流运行。系统通过快速切换多个进程,营造并行处理的假象。多线程也以这样的方式运行。一个有3个线程的进程,其各线程表现为并行运行。单核CPU每次运行一个线程,花费CUP调度处理该进程时间的1/3(大概是这样,CPU时间取决于操作系统、调度算法等)。在多处理器系统中,情况类似。只有单核CPU执行线程时才与本书描述的方式相同。多核的好处是,可以并行运行更多的线程,充分发挥本地硬件的并行处理能力和多线程的执行能力。
下面的例子用两个线程实现一个简单的数组排序,演示了线程的基本用法。
准备就绪
确定安装并运行了Visual Studio。
操作步骤
1.创建一个新的默认Win32控制台应用程序,名为MultithreadedArraySort
。
2.打开MultithreadedArraySort.cpp
,并输入下面的代码:
#include "stdafx.h"
#include <Windows.h>
#include <iostream>
#include <tchar.h>
using namespace std;
#define THREADS_NUMBER 2
#define ELEMENTS_NUMBER 200
#define BLOCK_SIZE ELEMENTS_NUMBER / THREADS_NUMBER
#define MAX_VALUE 1000
typedef struct _tagARRAYOBJECT
{
int* iArray;
int iSize;
int iThreadID;
} ARRAYOBJECT, *PARRAYOBJECT;
DWORD WINAPI ThreadStart(LPVOID lpParameter);
void PrintArray(int* iArray, int iSize);
void MergeArrays(int* leftArray, int leftArrayLenght, int*
rightArray, int rightArrayLenght, int* mergedArray);
int _tmain(int argc, TCHAR* argv[])
{
int iArray1[BLOCK_SIZE];
int iArray2[BLOCK_SIZE];
int iArray[ELEMENTS_NUMBER];
for (int iIndex = 0; iIndex < BLOCK_SIZE; iIndex++)
{
iArray1[iIndex] = rand() % MAX_VALUE;
iArray2[iIndex] = rand() % MAX_VALUE;
}
HANDLE hThreads[THREADS_NUMBER];
ARRAYOBJECT pObject1 = { &(iArray1[0]), BLOCK_SIZE, 0 };
hThreads[0] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)
ThreadStart, (LPVOID)&pObject1, 0, NULL);
ARRAYOBJECT pObject2 = { &(iArray2[0]), BLOCK_SIZE, 1 };
hThreads[1] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)
ThreadStart, (LPVOID)&pObject2, 0, NULL);
cout << "Waiting execution..." << endl;
WaitForMultipleObjects(THREADS_NUMBER, hThreads, TRUE, INFINITE);
MergeArrays(&iArray1[0], BLOCK_SIZE, &iArray2[0], BLOCK_SIZE, &iArray[0]);
PrintArray(iArray, ELEMENTS_NUMBER);
CloseHandle(hThreads[0]);
CloseHandle(hThreads[1]);
cout << "Array sorted..." << endl;
return 0;
}
DWORD WINAPI ThreadStart(LPVOID lpParameter)
{
PARRAYOBJECT pObject = (PARRAYOBJECT)lpParameter;
int iTmp = 0;
for (int iIndex = 0; iIndex < pObject->iSize; iIndex++)
{
for (int iEndIndex = pObject->iSize - 1; iEndIndex > iIndex; iEndIndex--)
{
if (pObject->iArray[iEndIndex] < pObject->iArray[iIndex])
{
iTmp = pObject->iArray[iEndIndex];
pObject->iArray[iEndIndex] = pObject->iArray[iIndex];
pObject->iArray[iIndex] = iTmp;
}
}
}
return 0;
}
void PrintArray(int* iArray, int iSize)
{
for (int iIndex = 0; iIndex < iSize; iIndex++)
{
cout << " " << iArray[iIndex];
}
cout << endl;
}
void MergeArrays(int* leftArray, int leftArrayLenght, int*
rightArray, int rightArrayLenght, int* mergedArray)
{
int i = 0;
int j = 0;
int k = 0;
while (i < leftArrayLenght && j < rightArrayLenght)
{
if (leftArray[i] < rightArray[j])
{
mergedArray[k] = leftArray[i];
i++;
}
else
{
mergedArray[k] = rightArray[j];
j++;
}
k++;
}
if (i >= leftArrayLenght)
{
while (j < rightArrayLenght)
{
mergedArray[k] = rightArray[j];
j++;
k++;
}
}
if (j >= rightArrayLenght)
{
while (i < leftArrayLenght)
{
mergedArray[k] = leftArray[i];
i++;
k++;
}
}
}```
示例分析
这个程序示例很简单,演示了线程的基本用法。该示例背后的思想是,为了节省执行时间而添加并行,把问题划分为几个小问题,并分配给几个线程(分而治之)。我们在前面提到过,把问题划分成若干更小的单元,更容易在实现中创建并行逻辑。同时,在并行中使用系统资源能优化应用程序并提高其运行速度。
更多讨论
如前所述,每个应用程序都有一个主线程。使用`CreateThreadWin32 API`创建其他线程:
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter, DWORD dwFlags, LPDWORD lpThreadId );`
设置线程的开始地址(lpStartAddress
)和设置传给线程例程的值(lpParameter
)很重要。lpParameter
是一个预定义例程(函数)指针,如下代码所示:
typedef DWORD ( WINAPI *PTHREAD_START_ROUTINE )( LPVOID lpThreadParameter );`
我们的`ThreadStart`方法与指定的原型匹配,这也是开始执行线程的地方。`CreateThreadAPI`的第4个参数是一个要传递给线程例程的指针。如果要传递更多参数,可以创建一个结构或类,然后传递相应对象的地址。欲详细了解`CreateThreadAPI`,请查阅MSDN(http://msdn.microsoft.com/en-us/library/windows/desktop/ms682453%28v=vs.85%29.aspx)。