Windows是一个多任务的操作系统,由于同时可以运行多个任务,因此Windows必须小心地管理系统的存储空间,以便一个应用程序对内存的要求得到满足的同时不会影响其他任务的执行。
尽管微处理机向物理内存写或读都是使用物理地址,但是程序员在程序设计中却是使用逻辑地址来对物理内存进行操作。逻辑地址包括两部分:
1、段标识符 用于说明存取的内存段的值;
2、偏移值 相对于段地址的字节数。
微处理器能把由段地址和偏移地址组成的逻辑地址转换成物理地址。
在Windows系统平台中,进程是系统内存管理的基础。通常,进程具有它自己私有的虚拟地址空间、代码、数据和其他操作系统资源。进程必须至少具有一个运行在该进程之内的执行线程。
每个运行于Windows下的进程和其他进程可以得到私有的32位虚拟地址空间,最多可访问 4GB内存。在 Windows 95中底层 IGB的内存空间是处于完全保护状态,顶层2GB的内存空间处于较差的安全状态;而在Windows NT中,系统保持整个4GB的内存空间是完全保护的。
内存分配归为两个主要类型:堆分配和栈分配。当用堆分配时,使用指向内存块的指针;当用栈分配时,与实际内存进行交互操作。在堆上分配的空间则要求应用程序显式地删除堆分配;在栈上分配对象时,程序分配给对象的内存空间将会自动被删除。
堆和栈都是和某一个进程相关的内存块。页是Windows对内存的分配单位,通过页可以实现对内存的访问。
一、堆: 堆是保留由进程分配的一个内存区。
Windows在全局堆或局部堆中为应用程序分配内存。全局堆对所有应用程序均可以使用,而局部堆只局限于为一个应用程序所使用。
全局堆是Windows操作系统控制的内存空间,全局堆以Windows装入内存的地方开始,包括了剩余的可用内存。当Windows操作系统启动时,代码和数据内存块首先被分配。分配完代码和数据内存块后余下的内存称为自由内存区,它可以由应用程序使用。全局堆往往用来分配大的内存块(它也可以用于分配任意大小的内存块,只是在小内存块的分配中很少使用全局堆)。
全局堆内存块的分配一般应按照以下原则进行:
1、固定段从底往上分配;
2、可废弃段从顶住下分配;
3、可移动段和不可废弃的数据段在固定段与可废弃段之间进行分配;
4、最大的自由内存块往往位于可废弃段下面。
与栈变量不同的是,当定义堆变量时,它们不存在,而是使用指针访问堆变量,并使用C或C+十内存分配函数、方法及操作码来动态分配内存。尽管最终可安全地使用Windows运行时库函数malloc和 free,但是在 C++程序设计中,更为常见的是使用 new操作符分配内存和delete操作符解除分配。new和delete操作符包括自动对内存泄漏的检查。Windows中,堆的规模仅受系统中有效虚拟内存总容量所限制。
下面是典型的堆分配的实例:
void fFunction()
{
CClass ObjectOfClass=new CClass;
...
delete CClass(ObjectOfClass);
}
二、栈
栈是保存函数参数和块局部变量的内存区,编译器为这些项自动进行内存分配,且只要有函数调用,就将建立栈。栈分配的两个特性是:
1、局部变量的自动分配,包括大型数组及数据结构;
2、当局部变量超出范围时,自动解除局部分配。
下面是典型的栈分配实例:
void fFunction()
{
CClass ObjectOfClass;
...
}
上面的程序中,语句CClass ObjectOfClass;将在栈中为对象ObjectOfClass。分配一个内存空间,当该函数的调用完成后,栈自动释放 ObjectOfClass所占据的内存空间。在应用程序执行时,只要对象定义一到达,立刻调用类的构造函数创建对象,在对象退出当前范围时,调用对象的析构函数。这样就能保证内存资源不会泄漏。
在栈上使用变量和分配对象的主要优点就是程序员不用担心由于栈变量的使用导致内存泄漏,其主要缺点是栈空间是有限的,处理大型的对象和数据结构时,栈可能被耗尽。
三、内存页管理
Windows API支持 32位寻址和页式内存。Windows 95内存管理使用了内存分页和32位线性寻址技术。整个32位地址空间被分为四个主要段,每一个段被称为Arena,每个Arena满足不同系统和进程的需求。四个主要段如下所示:
第一个Arena是从0~4MB,该段用在MS-DOS/Wndows3.x兼容层。
第二个Arena是从4MB~2GB,每个Windows进程使用该区作为它的私有地址空间。
第三个Arena是从2G~3GB,该地址空间由系统内所有进程共享,并包含内存映像文件及16位应用程序组件。
第四个 Arena从 3GB~4GB,由 Windows 95保留使用。
这一切都是由windows 95的虚拟内存管理程序来实现,它使用如下所示的全部虚拟地址空间布局。
系统保留Arena(4000M字节)
|
由Windows 95/98的所有进程使用的共享Arena(3000M字节)
|
保存当前执行的进程地址空间的私有Arena(2000M字节)
|
16位MSDOS兼容Arena(4M字节)
|
虚拟内存管理
进程所用的虚拟地址和物理内存地址并不一对应,而是为每一个进程保持页映像,页是一个数据结构,用于将虚拟地址转换为物理地址。由于进程所拥有的4GB地址大于一般个人PC内存总容量,Windows在硬盘上采用了分页文件技术来处理溢出。
Windows 3.l使用了交换文件技术将内存中的信息交换到硬盘上。此技术能够释放足够的RAM供应用程序使用,Windows中实现了相似的内存交换形式,用更合适的分页文件代替交换文件标记。这是通过使用处理分页任务的动态虚拟内存管理程序来实现。
分页技术为应用程序提供了比系统中实际RAM的物理容量大得多的内存空间。Windows利用硬盘空间模拟内存空间,也就是说,一个系统可以使用的全部内存容量等于RAM容量加上系统可以使用的分页文件空间总量。Windows使用了动态分页文件,当不需要时,可以缩小到OKB大小,假若需要时又可以长到硬盘上的全部自由空间。
在RAM和每个进程逻辑地址空间内,二者内存的存储是按页组织的。页的大小与系统硬件有关,对 Windows 95和 Windows NT而言,在 Intel兼容机上,每页大小为 4KB。
为使内存管理更加灵活,Windows内核将内存页面送入或送出磁盘上的分页文件,这些页的物理地址映像到进程地址空间,且页映像连续地更新。页由它的使用者做标记,当物理内存不够时,Windows内核可以将RAM中近期最少使用的页面送到盘上的分页文件。假定有足够的磁盘空间去处理系统要求,那么它使Windows有可能总是有足够的分页文件磁盘空间去处理内存分配。当应用程序需要访问内存时,它们只需要在自己的虚拟地址空间内进行,而不一定要使用物理地址。
Intel微处理机及其兼容机使用的32位寻址模型,通过这个模型将32位分为三段而有效地实现了地址映像。通过CR3专用寄存器,CPU直接引用页目录。操作系统控制此寄存器,此寄存器起了给Windows发送指针的作用,这个寄存器指明地址映像的首址。
Windows虚拟内存管理系统为应用程序使用保留内存页面提供了可能,进程的虚拟地址空间的页面状态可以是表所列值之一。
状 态
|
说 明
|
至闲
| 当前不访问的页,但对占用和保留是有效的 |
保留
| 保留供未来进程使用的页,进程不访问保留页,内存也不分配保留页 |
占用
| 在内存或磁盘上已分配的物理内存页,占用页可指定为不访问,只读访问和读/写访问 |
C++中可以通过new和delete运算符来管理动态内存,但有时为底层分配内存时,还需要通过内存管理函数来进行,Windows API中提供了两个为进程分配新页的两个函数:VirtualAlloc和MapViewOfFile。
VirtualAlloc函数的功能是在应用程序的虚拟内存空间内保留或占用一个页面。可以使用标准指针访问被VirtualAlloc分配的内存并自动初始化所有被分配的内存为0。
VirtualAlloc函数的原型定义如下:
LPVOID VirtualAlloc(
LPVOID lpAddress, //保留或者占用的内存起始地址
DWORD dwSize, //内存区的大小
DWORD flAllocationType, //分配的类型标志
DWORD flProtect //页具有的访问保护类型
);
函数VirtualAlloc可以使用的分配类型标志如表所示。
标 志
|
意 义
|
MEM_COMMIT | 为指定页区分配物理内存 |
MEM_RESERVE | 在进程的虚拟地址空间内保留一区域,但不分配任何物理内存;保留区直到释放后才能用于内存分配 |
MEM_TOP_DOWN | 在可能的最高地址上分配内存 |
函数VirtUalAlloc可以使用的访问保护标志如表所示
标 志
|
意 义
|
PAGE_READONLY | 指定只读访问页的占用区 |
PAGE_READWRITE | 指定读和写访问页的占用区 |
PAGE_EXECUTE | 指定执行访问页的占用区 |
PAGE_EXECUTE_READ | 指定执行和读访问页的占用区 |
PAGE_EXECUTE_READWRITE | 指定执行,读和写访问页的占用区 |
PAGE_GUARD | 指定该区的页成为保护页 |
PAGE_NOACCESS | 禁止页占用区的所有访问 |
PAGE_NOCATHE | 执行页占用区无缓存,主要用于设备驱动程序 |
除函数VirtualAlloc外,Windows API中还提供了一些用于辅助内存页分配的函数,这些函数的名称和功能如表所示。
Windows API中辅助页分配的函数:
函数名
|
功能简介
|
VirtualLock | 将进程的地址空间的指定区锁进内存,以保证连续访问该区不引起页故障 |
VirtualProtect | 在调用进程的地址空间内,改变一组占用页上的访问保护 |
VirtualProtectEx | 在任何指定进程的地址空间内,改变一组占用页上的访问保护 |
VirtualQHery | 提供有关调用进程地址空间内的页区信息 |
VirtualQueryEx | 提供有关指定进程地址空间的页区信息 |
VirtualUnlock | 对属于进程的页区解锁 |
内存映像文件
内存映像文件是Windows的新特征,它具有某些独特的优点,包括共享文件、内存空间和对文件输入输出的简化等。内存映像文件将磁盘文件映像至内存地址区。这种安排允许在内存直接处理文件,用Windows内核处理在内存和磁盘间的数据缓存。
为实现文件映像文件,需要有三个Windows核心对象:
1、文件对象 指向标准文件句柄的操作系统的标准文件对象;
2、文件映像对象 完成实际文件映像或内存共享对象;
3、文件视图对象 几个视图可以共享同一个文件。
文件对象由文件句柄所标识,通过调用 Windows API函数 CreateFile可以获取文件对象。此函数完成大量工作,其函数原型定义如下:
HANDLE CreateFile(LPCTSTR lpFileName, //指向文件名的指针
DWORD dwDesiredAccess, //指定访问模式(读或写)
DWORD dwShareMode, //共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, //指向安全属性的指针
DWORD dwCreationDisposition, //指定创建方式
DWORD dwFlagsAndAttributes, //指定文件属性
HANDLE hTemplateFile //文档模板句柄);
当创建一个新文件时,CreateFile函数执行以下操作:
1、绑定文件属性;
2、设置文件长度为0;
3、如果指定了参数hTemplateFile的值,复制文件的扩展属性到新建的文件中。当打开一个已经存在的文件时,函数CreateFile执行以下操作:
(1)、绑定由参数dwFlagsAndAttributes指定的标志到文件属性中;
(2)、根据dWCreationDisPosition的值设置文件长度;
(3)、忽略参数hTemplateFile的值;
(4)、忽略SECURITY_ATTRIBUTES结构体的成员变量lpSecurityDescriptor的值。
SECURITY_ATTRIBUTES结构体中其他成员变量的值不被忽略。成员blnheritHandle指定文件句柄能否被继承。
如果试图通过函数CreateFile在一个软盘上创建文件,而软驱内又没有软盘,这时就会弹出一个消息框提示用户插入软盘。可见CreateFile函数的功能是比较强的,它为用户创建文件对象提供了很多便利措施。
当使用内存映像文件时,文件映像对象执行底层工作。Windows函数CreateFileMapping用于创建命名和非命名文件映像对象。CreateFileMapping函数的原型定义如下:
HANDLE CreateFileMapping(
HANDLE hFile, //被映像的文件句柄
LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
//指向指定映像安全特性的SECURITY—ATTRIBUTES结构的指针
DWORD flProtect,//对被映像文件对象的保护设置
DWORD dwMaximumSizeHigh, //指定文件对象大小的高32位
DWORD dwMaximumSizeLow, //指定文件对象大小的低32位
LPCTSTR lpNaine //指向映像对象名的指针);
在一个文件映像对象创建后,文件对象的大小不允许超过文件映像对象的大小。而且并不是所有的文件内容都可以被共享。如果一个应用程序为文件映像对象指定了一个大于实际文件的尺寸,磁盘文件的大小将自动调整以与文件映像对象的尺寸相匹配。
通过由函数CreateFileMapping返回的句柄能够访问新建的文件映像对象。该句柄可以用于任何需要使用文件映像句柄的地方。
函数CreateFileMapping的使用可以参考下面的例子:
hMap=CreateFileMapping(…); //创建文件——映像对象
if(hMap != NULL && GetLastError()==ERROR_ALREADY_EXISTS)∥判断是否创建成功
{
CloseHandle(hMap); //创建不成功关闭hMap所指向的句柄
hMap=NULL;
}
return hMap; //创建成功返回新建的文件——映像对像句柄
当映像文件时,文件映像对调用进程是部分可视的。映像文件的多个视图可以同时访问文件的同一区或不同区。通过调用Windows中的DuplicateHandle函数,一个过程可以为另一进程创建文件映像对象句柄的副本。进程还可以通过调用OpenFileMapping函数来共享文件映像对象。
函数MapViewOfFile映像文件视图进入调用进程的地址空间。其函数原型定义如下:
LPVOID MapViewOfFile(
HANDLE hFileMappingObject, //指向一个文件——映像对象的句柄
DWORD dwDesiredAccess, //访问模式
DWORD dwFileOffsetHigh, //文件偏移的高32位部分
DWORD dwFileOffsetLow, //文件偏移的低32位部分
DWORD dwNumberOfBytesToMap //映像文件字节数);
其中参数dwDesiredAccess可以是表中所列值之一。
函数MapViewOfFile中可以指定的访问模式:
值
|
含 义
|
FILE_MAP_WRITE | 指定读写访问,并映像文件的读写视图 |
FILE_MAP_READ | 指定只读访问,并映像文件的只读视图 |
FILE_MAP_ALL_ACCESS | 指定读写访问,并映像文件的读写视图 |
FILE_MAP_COPY | 指定复制和写访问,它对任何类型文件视图都有效 |
内存管理应用实例
下面的例子通过一个定时器来控制内存句柄的分配,当系统能容纳的句柄数已经达到其最大值后,显示一段提示信息通知用户不能再进行内存分配,并通知用户当前的可用内存空间的大小。
例中,首先通过如下语句创建了一个定时器:
while(!SetTimer(hWnd,1,I,NULL))
if(IDCANCEL==MessageBox(hWnd,"太多定时器",szAppName,MB_RETRYCANCEL))
return FALSE;
如果当前系统中运行的定时器太多,则弹出一个消息框通知用户不能再创建定时器;如果用户选择了消息框中的Cancel按钮,则整个应用程序也就结束了。
例的消息处理函数中,首先在响应WM_CREATE消息时,通过函数CreateWindow创建了一个编辑框,这个编辑框用来显示需要通知给用户的信息。
例的核心在于对消息WM_TIMER的处理,应用程序通过如下代码来响应WM_TIMER消息:
case WM_TIMER: //定时器消息
for(i=0;i<10;i++)
{
if(GlobalAIloc(GMEM_MOVEABLE,1024*1024)==NULL) //分配内存
{
KillTimer(hWnd,1);
GlobalMemoryStatus(&mStatus); //获取内存状态
wsprintf(cText,"No more handles available!\n%lu bytes free",
mStatus.dwTotalPhys //输出空闲内存);
SetWindowText(hWndEdit,cText);
break;
}
else
{
Counter++;
wsprintf(cText,"There have been%d Handles,%lu bytes free",Counter,GetFreeSpace(0));
SetWindowText(hWndEdit,cText);
}
wsprintf(cText,"%d Handles",Counter);
SetWindowText(hWnd,cText);
break;
在上述代码中,函数 GlobalAlloc 用于从难中分配指定字节的内存空间。其函数原型定义如下:
HGLOBAL GlobalAlloc(UINT uFlags, //内存分配标志
DWORD dwBytes //分配的字节数);
如果没有足够的内存给函数GlobalAlloc进行分配,则函数GlobalAlloc将返回NULL值,应用程序就可通过检测其返回值是否为 NULL来判断是否继续响应WM_TIMER消息。通常在窗口中需要显示当前的空闲内存空间。获取系统空闲内存空间可以通过函数
GetFreeSpace来实现,这个函数的原型定义如下:
VOID GlobalMemoryStatus(LPMEMORYSTATUS lpBuffer);//指向MEMORYSTATUS结构体的指针
其中MEMORYSTATUS结构体的具体定义如下:
typedef struct MEMORYSTATUS
{
//mst
DWORD dwLength; //MEMORYSTATUS的大小
DWORD dwMemoryLoad; //正在使用的内存空间的大小
DWORD dwTotalPhys; //全部物理内存的大小
DWORD dwAvailPhys; //自由内存大小
DWORD dwTotalPageFile; //内存页尺寸
DWORD dwAvailPageFile; //页文件中的空闲空间
DWORD dwTotalVirtual; //总的虚拟内存空间
DWORD dwAvailVirtual; //空闲的虚拟内存空间
}MEMORYSTATUS,*LPMEMORYSTATUS;
有关系统的内存状态记录在结构体 lpBuffer中,通过 lpBuffer中的各个域可以得到各种具体的信息,程序中就是通过mStatus.dwTotalPhys来获取当前的剩余内存空间。
例的源程序如下:
#include <windows.h>
#include <string.h>
#include<winbase.h>
#define IDS_ERR_CLASS 20
#define IDS_ERR_WINDOW 21
char szAppName[] = "Memory" ;
char szString[256] ;
HANDLE hInst;
HINSTANCE hCurrInstance;
long WINAPI WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam);
//函数:WinMain
//作用:主函数
int WINAPI WinMain (HINSTANCE hInstance,HINSTANCE hPrevInstance,PSTR szCmdLine,int iCmdShow)
{
HWND hWnd ;
MSG msg ;
WNDCLASSEX wndclass ;
wndclass.cbSize= sizeof (wndclass);
wndclass.style= CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc= WndProc;
wndclass.cbClsExtra= 0;
wndclass.cbWndExtra= 0;
wndclass.hInstance= hInstance;
wndclass.hIcon= LoadIcon (NULL, IDI_APPLICATION);
wndclass.hCursor= LoadCursor (NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);
wndclass.lpszMenuName= szAppName;
wndclass.lpszClassName = szAppName;
wndclass.hIconSm= LoadIcon (NULL, IDI_APPLICATION);
RegisterClassEx (&wndclass);
hWnd = CreateWindow(szAppName, "内存管理程序",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
ShowWindow (hWnd, iCmdShow);
UpdateWindow (hWnd);
while(!SetTimer(hWnd,1,1,NULL))
if(IDCANCEL==MessageBox(hWnd,"太多定时器",szAppName,MB_RETRYCANCEL))
return FALSE;
hCurrInstance=hInstance;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg);
DispatchMessage (&msg);
}
return msg.wParam;
}
//函数:WndProc
//作用:消息处理函数
long WINAPI WndProc (HWND hWnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
static WORD Counter=0;
static HWND hWndEdit;
MEMORYSTATUS mStatus;
char cText[100]="Zero";
int i;
switch(iMsg)//消息处理
{
case WM_CREATE://处理创建窗口消息
hWndEdit=CreateWindow("EDIT", //创建编辑框
"", WS_CHILD|WS_VISIBLE,
50,50, 100,100,
hWnd,
NULL,
hCurrInstance,
NULL);
break;
case WM_SIZE://改变窗口大小
MoveWindow(hWndEdit,0,0,LOWORD(lParam),HIWORD(lParam),TRUE);
break;
case WM_TIMER: //定时器消息
for(i=0;i<10;i++)
{
if(GlobalAlloc(GMEM_MOVEABLE,1024*1024)==NULL)//分配内存
{
KillTimer(hWnd,1);
GlobalMemoryStatus(&mStatus);//获取内存状态
//输出分配的句柄数及剩余磁盘空间
wsprintf(cText,
"No more handles available! \n%lu bytes free",
mStatus.dwTotalPhys //输出空闲内存);
SetWindowText(hWndEdit,cText);//显示输出信息
break;
}
else
{
Counter++;
//输出分配的句柄数及剩余磁盘空间
wsprintf(cText,
"There have been %d Handles ,%lu bytes free",
Counter,GetFreeSpace(0));
SetWindowText(hWndEdit,cText);
}
wsprintf(cText,"%d Handles",Counter);
SetWindowText(hWnd,cText);
break;
}
case WM_COMMAND://命令消息
return(DefWindowProc(hWnd,iMsg,wParam,lParam));
break;
case WM_CLOSE://结束应用程序
KillTimer(hWnd,1);
DestroyWindow(hWnd);
PostQuitMessage(0);
break;
default://默认消息处理
return (DefWindowProc(hWnd,
iMsg,
wParam,lParam));
break;
}
return 0;
}
例的运行结果如图所示。
从图可以看出,当分配了306个内存句柄后(标题栏为:306 Handles),系统就不能再分配内存了,这时剩余的空闲内存空间为11048576字节。
进程和线程
32位的 Windows操作系统(Windows 95以上版本(包括)及 WinNT4.0,本节以后用Windows代表这类操作系统)的一个重要特色就是支持一个应用程序能执行多个线程,这也是32位的操作系统优越于16位的操作系统的优点之一。用户可以在一个应用程序中进行不同类型的处理,这样能充分发挥操作系统的潜力。现今的应用程序中使用线程技术的例子也很多,例如,Win95的删除文件操作,前台是一个动画线程显示一片纸被摔碎的情形,而主体的文件删除操作却是在另一个线程中进行。
线程允许一个应用程序同时做很多事情,但是所谓同时,是指给人的感觉而言,CPU不可能在同一时间内做几件事情,只是由于CPU的高速运算能力,使得CPU在一个时间段内分别做了几件事,这是操作系统在这几个事件中来回切换,挂起运行任务,然后唤醒下一个任务,让人觉得这几件事是在同一个时间同时完成的。
在介绍多线程的程序设计技术之前,必须弄清线程和进程的区别和联系。
一个进程是一个由Windows系统装载进内存的可执行单元,操作系统将为进程建一个虚拟地址空间;线程是一个用于运行代码的Windows对象,被操作系统当作准备执行的一个执行体。进程包括代码、数据和文件、管道之类的资源,以及该进程的线程能够存取的线程同步对象。一个应用程序可以有一个以上的进程,一个进程可以有多个线程。
Windows每创建一个新的进程时,都将为该进程创建一个线程来开始执行实际的程序代码,这个线程称为主线程,一个过程只能有一个主线程。一个进程可以创建一个或者多个线程,并且这些线程都使用同一虚拟地址空间。地址空间是由过程提供的,这使得线程之间共享数据变得容易、不同的线程可以执行同一代码块,也可以执行不同的代码块。当主线程结束时,整个进程也就结束了。
进程不同于线程之处在于一个过程有自己专有的虚拟地址空间。当一个过程内有一个以上的线程时,每个线程都和进程共享统一虚拟地址空间,都能获取进程的系统资源、访问全局变量。线程可以执行进程任何部分的代码,并且可以创建新的线程。
在Windows 3.X以下的16位操作系统中,拥有CPU时间的最小实体被称为一个任务。在Windows中,任务被称为进程,但是在32位的操作系统中,进程不再是拥有CPU的最小实体了,而是线程成为拥有CPU时间的最小实体。
线程的一个重要特征就是和创建自己的进程共享内存,它可以存取调用它的进程的代码和数据。每个线程拥有自己专有的栈和专有的执行上下文(专有执行上下文是一小块内存,它用于保存CPU处理寄存器的状态和一些与线程执行环境相关的数据)。一般说来线程不占用额外的内存空间。
注意:真正运行的程序单位是线程而不是进程,进程为运行的线程提供所要使用的数据、资源和地址空间。
一个主线程可以创建其他线程,创建出来的新线程又可以创建出其他的线程,以此类推。一个进程的所有线程可以同时进行,Windows操作系统自动在不同的线程之间切换。一般Windows操作系统对多个线程采用抢占式多任务方式,分配给线程的时间片取决于线程的优先权、线程是否正在等待同步信号等因素。
每个线程都在进程的内存空间执行,因而,每一个线程都可以访问全局数据,而且所有的线程都可以修改这些全局数据。因此,对多个线程的管理就存在一个线程之间的同步问题,如何解决线程的同步问题本章后续部分将详细介绍。
使用线程的主要益处在于:多线程使得进程能够同时执行多个操作。例如,一个应用程序可以使用一个线程来处理实时I/O,如在处理键盘和鼠标之类的用户输入的同时,应用程序使用其他较低的优先级的线程来处理那些不需要实时处理的任务(如计算或者后台打印等)。多线程在MDI应用程序中也很有用处,当一个MDI应用程序打开多个窗口的时候,可以指派不同的线程给各个窗口,用于处理各窗口的事务。
多线程在网络和通信方面也有很大用处,如一个命名管道服务程序可以为连接到该管道的每个客户创建一个线程。线程也可用来帮助应用程序处理来自多个通信设备的输入。
并不是说采用了线程技术就一定能大大提高程序的运行性能。应该说,线程技术也是有一定的局限性的,在有些情况下,采用了线程技术的应用程序其运行性能反不如没有采用线程技术的应用程序。一般,如果一个应用程序涉及到大量的I/O操作,则比较适宜采用线程技术,假设需要作一个大文件的排序操作,则可以考虑采用几个线程来实现,其中一个线程进行读写I/O操作,另外几个线程对读入的文件块进行排序操作。
注意:在线程之间进行切换也是要花费不少机时的,因此在采用多线程技术时,一定要避免过于频繁的线程切换。
线程管理
由于一个程序中有多个线程在同时运行,这些线程都有可能访问全局资源(如全局变量),同时,不同线程在执行上还存在一个时序问题,所以谁先发生,谁后发生,都应该统一管理。否则应用程序的运行就很有可能变得不可预测,甚至导致系统崩溃。
一、线程的优先权
当用户运行一个应用程序时,Windows创造相应的进程及其主线程,同时分配给该进程一个优先级类。优先级类是一个反映了该进程重要性的值。进程的优先级类对应着一个优先级值,当进程被创建时,这个对应的优先级值就分配给该进程。在进程内部的不同线程之间,每个线程还有一个优先级。某个线程的优先权由创建该线程的进程的优先级类和线程本身的优先权组合决定。操作系统负责调度各个线程,线程的优先权就决定了该线程调度的时间。系统将按优先级顺序调度所有进程中的线程,只有在没有高优先级线程在执行时,才能执行低优先级的线程。如果几个线程具有相同的优先级,则操作系统以先进先出循环的方式一个接一个地调度这些线程。
进程优先级类是用来描述系统中运行的某个进程的优先权。Windows支持如表所示的四种优先级类。
优先级
|
优先级类
|
优先级
|
描述
|
Idle | IDLE_PRIORITY_CLASS |
4
|
空闲类
|
Normal | NORMAL_PRIORITY_CLASS |
7-9
|
正常类
|
High | HIGH_PRIORITY_CLASS |
13
|
高级类
|
Realtime | REALTIME_PRIORITY_CLASS |
24
|
实时类
|
Windows API中通过GetPriorityClass和SetPriorityClass函数可以获取和设置给定进程的优先级类。函数GetPriorityClass的原型定义如下:
DWORD GetPriorityClass(HANDLE hProcess);//进程句柄
函数SetPriorityClass的原型定义如下:
BOOL SetPriorityClass(
HANDLE hProcess, //进程句柄
DWORD dwPriorityClass //优先级);
大多数情况下可以使用上述两个函数来访问进程的优先权。对当前的进程,可以通过函数GetCurrentProcess来获得其句柄:
HANDLE GetCurrentProcess(VOID);
函数GetCurrentProcess的返回值是当前进程的句柄,获得了进程句柄后就可以设置进程的优先权。例如,下面的程序段中设置当前进程的优先级为HIGH_PRIORITY_CLASS。
If(!SetPriorityClass(GetCurrentProcess(),HIGH_PRIORITY_CLASS)
ShowMessage("ErrorSetting Priority Class");
另一个决定线程总体优先权的因素是该线程的相关优先权。一个线程可以有如表所示的七种相关优先权。
线程的相关优先权:
线程相关优先权
|
标 志
|
优先级
|
tpldle | THREAD_PRIORITY_IDLE |
-15
|
tpLowcst | THREAD_PRIORITY_LOWSET |
-2
|
tpBelowNormal | THREAD_PRIORITY_BELOW_NORMAL |
-1
|
toNormal | THREAD_PRIORITY_NORMAL |
0
|
tpAboveNormal | THREAD_PRl0RJTY_ABOVE_NORMAL |
1
|
tpHighest | THREAD_PRJORJTY_HIGHEST |
2
|
tpTimeCritical | THREAD_PRIORITY_TIME_CRITICAL |
15
|
线程的总体优先权由进程的优先权类加上线程的相关优先权决定。整体优先权取值为 l--31。但是 tpIdle和 tpTimeCritical与其他相关优先权不同,它们并不直接加到优先级类上。
来决定线程的总体优先权。所有具有tpIdle相关优先权的线程,不论其进程的优先级类是什么,其总体优先权为肝这个规则例外的情况是,如果优先级类是 Realtime(实时),则与 tpIdle相关优先权组合时总体值为 16。对于优先权为 tpTimeCritical的线程,不论其优先级类是什么,总体优先为15;例外情况是Realtime优先级类与 tpTimeCritical相关优先权组合时,总体值为31。
二、线程同步
当使用多个线程的时候,经常需要同步线程对某块数据或资源的访问。例如,一个线程负责数据的输入,另一个线程负责数据的处理,显然,这两个线程应该有一个时间上的先后问题。但是,由于这些操作在各自的线程中执行,对操作系统来说,它们被认为是完全无关的任务。因此,要对线程进行同步就必须采用特定的机制,所有对全局变量的存取都应在一种保护方式下进行,线程代码必须对其要读取或修改的全局数据“加锁”,然后存取数据,最后对全局数据解锁,加锁在同一时刻只能授予一个线程。
Windows提供了许多方法来同步线程,几种典型的方法是使用临界区、互斥变量、信号量和事件。
1、临界区
临界区提供了一种最直接同步线程的方法。所谓临界区是一个特殊的Windows变量(实际上是一个记录结构)临界区使得一个代码段独占地执行而不受其他线程的干扰。
在使用临界区之前,必须使用InitializeCriticalsection函数对临界区进行初始化,该函数的声明如下:
VOID InitializeCriticalSection(
LPCRITICALSECTION lpCriticalSection //临界区指针);
其中,lpCritlcalSection是 CRITICAL_SECTION结构体的引用参数。调用该函数时,传递给该函数的是一个未被初始化的结构体变量,InitializeCriticalSection函数负责初始化该结构体变量。结构体变量初始化后,调用EntetoriticalSection和LevelCriticalSection函数就可以在用户自己的程序中创建一段临界区。
EntetoriticalSection函数的原型定义如下:
VOID EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection //临界区指针);
函数LeaveCriticalSection的原型声明如下:
VOID LeaveCriticalSection(
LPCRITICAL SECTION lpCriticalSection //临界区指针);
在使用完CRITICAL_SECTION结构体后,必须销毁该结构体。调用DeleteCriticalSection函数可以销毁CRITICAL SECTION结构体。DeleteCriticalSection函数的声明如下:
VOID DelctcCriticalSection(
LPCRITICAL_SECTION lpCriticalSection //临界区指针);
使用临界区有一个缺点,它们只可以在同一进程中使用,不同进程中的临界区不能互相存取。另外,临界区没有相关的时间限制,如果一个线程正在等待进入一个临界区,而已进入该临界区的线程没有离开它,那么第一个线程就被阻塞而不能运行。
2、互斥变量
互斥变量类似于临界区,它与临界区的不同点在于:互斥变量可以用于同步不在一个过程内的线程;互斥变量可以有一个字符串名,引用该名字可以创建已存在互斥变量对象的另一个句柄。创建互斥变量的函数CreateMutex原型定义如下:
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL blnitialOwrier,
LPCTSTR lpName);
其中:
lpMutexAttributes是一个SECUmTYNTmBUTES结构体的指针。通常为 NULL值,在这种情况下,使用的是默认的安全属性;
blnitialowner指定当线程创建互斥变量时是否拥有该互斥变量,如果是FALSE时,则互斥变量是无主的;
lpName是互斥变量的名字,如果不希望给互斥变量命名,可以将参数置为 NULL。如果该值不是NULL,函数将在系统中搜索已有的同名互斥变量;如果找到,则返回已存在的互斥变量的句柄;否则返回一个新互斥变量的句柄。使用完一个互斥变量后,必须关闭该互斥变量,通过调用函数CloseHandle就可以实现关闭该互斥变量。函数CloseHandle的原型定义如下:
BOOL CloseHandle(HANDLE hObject); //关闭对象句柄
函数WaitForSingleObject用于控制线程进入同步代码块。该函数的原型定义如下:
DWORD WaitForSingleObject(
HANDLE hHandle,//对象句柄
DWORD dwMilliseconds //时间片
);
函数 WaitForsillgleobjeCt实现的功能是让当前进程挂起 dwMIlliseconds毫秒,直到hHandle指定的对象变成Signaled。互斥变量的状态为Signaled,是指它不被进程或线程拥有。参数dwMIlliseconds可以指定为0,表示立即检查对象的状态;可以是twFINE,即一直等待到对象变成Singaled。WaitForSingleObject可能的返回值如表所示。
返回值
|
含 义
|
WAIT_ABANDONED | 指定的对象是互斥对象,拥有该互斥变量的线程在释放该互斥变量之前退出;此时,互斥变量对象的所有权被赋予调用的线程,互斥变量被设为非Signaled |
WAIT_OBJECT_0 | 指定对象的状态为Signaled |
WAIT_TIMEOUT | 等待时间超过,对象状态为非Signaled |
当互斥变量不被任何一个线程拥有时,处于Signaled状态,第一个调用WaitForSingleObject函数的线程获得该互斥变量的拥有权,互斥变量对象的状态被设置为非Signaled。当线程以互斥变量句柄作为参数调用RealeaseMutex函数时,互斥变量再次变为 Signale。
WaitForMultipleObjects和 MsgWaitForMultinleObjects函数用于等待一个或者多个对象变成 Signaled。其中函数 WaitForMultipleObjects的原型定义如下:
DWORD WaifforMultipleobjects(
DWORD nCount, //句柄数
CONSTHANDLE’lpHandles, //指向句柄数组的指针
BOOL fWaitAll, //等待标志
DwORD dwMilliseeonds //时间片
);
函数MsgWaitFofMultipleObjects的原型定义如下:
DWORD MsgWaitForMultipleObjects(
DWORD nCount, //句柄数
LPHANDLE pHandles, //指向旬柄数组的指针
BOOL fWaitAll, //等待类型
DWORD dwMilliseeonds, //时间片
DWORD dwWakeMask //等待的输入事件类型
);
3.信号量
线程同步的另一个技术就是使用信号量对象。信号量具有互斥变量的功能,但是在互斥变量的基础上增加了对资源计数的能力。因此,通过信号量可以允许在某一个时刻可以有指定数目的线程进入同步代码区。创建信号量函数CreateSamaphore原型定义如下:
HANDLE CreateSemaphorer(
LPSECURITY ATTRIBUTES lpSemaphoreAttributes, //SECURITY_ATTRIBUTES结构体指针
LONG llnitialCount, //信号量对象的初始计数值
LONG lMaximumCount, //信号量对象的最大计数值
LPCTSTR lpName //指向信号量名称的指针
);
其中:
lpSemphoreAttributes是一个SECURITY_ATTRIBUTES结构体的指针,当为 NULL时表示使用系统提供的默认值;
lInitialCount是信号量对象的初始计数值,取值范围为0-lMaximumCount。当 参数大于0时,信号是Signaled。当使用WaitForSingleObject或者其他的等待函数释放线程后,信号量的计数值减少;
lMaximumCount指定信号量对象的最大计数值。如果信号量用于统计某些资源,该数值应是所有的资源数;
lpNam是信号量的名称。
4.死锁
如果一个线程在独占了一个同步对象的前提下,正在等待另一个同步对象,同时另外一个线程已经独占了第一个线程等待的同步对象,此时该线程也试图独占第一个线程已经独占了的同步对象。这样第一个线程要运行,就必须等待第二个线程运行完成后释放独占的同步对象;但是,第二个线程要能运行完成,必须等待第一个线程运行完成后释放同步对象,这样就形成了死锁。
要避免死锁,在应用中使用同步对象时必须十分小心。如果应用程序有可能陷入死锁,则必须重新设计应用程序。