Win32程序的执行单元

Win32程序的执行单元

线程的创建

线程函数的定义

1
DWORD WINAPI ThreadProc(LPVOID lpParam) //线程函数名ThreadProc可以随意的

WINAPI 是一个宏名

1
#define WINAPI __stdcall;

__stdcall是新标准C/C++函数的调用方法

__stdcall采用自动清栈的方式

__cdecl采用的是手工清栈方式

ThreadProc是一个回调函数

如果没有显式说明的话,函数的调用方法是__cdecl

lpParam参数由CreateTHread函数的第四个参数指定

创建新线程的函数是CreateThread

1
2
3
4
5
6
7
8
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,//线程的安全属性
DWORD dwStackSize, //指定线程堆栈的大小
LPTHREAD_START_ROUTINE lpStartAddress,//线程函数的起始地址
LPVOID IpParameter, //传递给线程函数的参数
DWORD dwCreationFlags, //指定创线程建后是否立即启动
DWORD* pThreadld //用于取得内核给新生成的线程分配的线程ID号
);

成功:返回新建线程的句柄

IpThreadAttributes参数:

如果指定为NULL表示默认安全属性,且不可继承

如果希望此线程对象句柄可以被继承,则必须设定一个SECURITY_ATTRIBUTES结构,将它的bInheritHandle成员初始化为TRUE

1
2
3
4
5
6
7
SECURITY ATTRIBUTES sa
sa.nLength =sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.binheritHandle=TRUE;
//使CreateThread返回的句柄可以被继承
//句柄h可以被子进程继承
HANDLE h=:CreateThread(&sa,....);

dwCreationFlags—创建标志:

如果是0,表示线程被创建后立即开始运行;如果指定为CREATE_SUSPENDED标志,表示线程被创建以后处于挂起(暂停)状态,直到使用ResumeThread函数


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <windows.h>

// 线程函数
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
int i = 0;
while(i < 20)
{
printf(" I am from a thread, count = %d \n", i++);
}
return 0;
}

int main(int argc, char* argv[])
{
HANDLE hThread;
DWORD dwThreadId;

// 创建一个线程
hThread = ::CreateThread (
NULL, // 默认安全属性
NULL, // 默认堆栈大小
ThreadProc, // 线程入口地址(执行线程的函数)
NULL, // 传给函数的参数
0, // 指定线程立即运行
&dwThreadId); // 返回线程的ID号
printf(" Now another thread has been created. ID = %d \n", dwThreadId);

// 等待新线程运行结束
::WaitForSingleObject (hThread, INFINITE);
::CloseHandle (hThread);
return 0;
}

INFINITE表示无限时间等待

WaitForSingleObject函数等待新线程运行结束

1
2
3
4
//等待新线程运行结束
::WaitForSingleObject(
hThread, //hHandle 要等待的对象的句柄
INFINITE); //dwMilliseconds要等待的时间(以毫秒为单位)

WaitForSingleObject函数用于等待指定的对象(hHandle)变成受信状态。参数dwMilliseconds给出了以毫秒为单位的要等待的时间,其值指定为INFINITE表示要等待无限长的时间。

当有下列一种情况发生时函数就会返回:
(1)要等待的对象变成受信(signaled)状态。
(2)参数dwMilliseconds指定的时间已过去。

一个可执行对象有两种状态,未受信(nonsignaled)和受信(signaled)状态。

线程对象只有当线程运行结束时才达到受信状态,此时"WaitForSingleObject(hThread,INFINITE)"语句才会返回。


### 内核句柄对象

线程内核对象就是一个包含了线程状态信息的数据结构。每一次对Create Thread函数的成
功调用,系统都会在内部为新的线程分配一个内核对象。系统提供的管理线程的函数其实就是
依靠访问线程内核对象来实现管理的。

线程内核对象(Thread Kernel Object)

1621938794206

1,线程上下文CONTEXT

每个线程都有它自己的一组CPU寄存器,称为线程的上下文。这组寄存器的值保存在一
个CONTEXT结构里,反映了该线程上次运行时CPU寄存器的状态。

2,使用计数Usage Count

Usage Count成员记录了线程内核对象的使用计数,这个计数说明了此内核对象被打开的
次数。

当这个值是0的时候,系统就认为已经没有任何进程在引用此内核对象了,于是线程内核对象就要从内存中撤销。
只要线程没有结束运行, Usage Count的值就至少为1。

在创建一个新的线程时,CreateThread函数返回线程内核对象的句柄,相当于打开一次新创建的内核对象,这也会促使Usage Count的值加1,所以创建一个新的线程后,初始状态下Usage Count的值是2。之后,只要有进程打开此内核对象,就会使Usage Count的值加1。比如当有一个进程调用OpenThread
函数打开这个线程内核对象后, Usage Count的值会再次加1.

1
2
3
4
5
HANDLE OpenThread(
DWORD dwDesiredAccess, //想要的访问权限,可以为THREAD ALL ACCESS等
BOOL bInheritHandle, //指定此函数返回的句柄是否可以被子进程继承
DWORD dwThreadld //目标线程ID号
); //注意, OpenThread函数是Windows 2000及其以上产品的新特性, Windows 98并不支持它。

由于对这个函数的调用会使Usage Count的值加1,所以在使用完它们返回的句柄后一定要调用CloseHandle函数进行关闭。关闭内核对象句柄的操作就会使Usage Count的值减1.

还有一些函数仅仅返回内核对象的伪句柄,并不会创建新的句柄,当然也就不会影响Usage Count的值。如果对这些伪句柄调用CloseHandle函数,那么CloseHandle就会忽略对自己的调用并返回FALSE,对进程和线程来说,这些函数有:

1
2
HANDLE GetCurrentProcess 0; //返回当前进程句柄
HANDLE GetCurrentThread 0; //返回当前线程句柄

如果线程结束后Usage Count不为0,会造成内存泄露 当然,线程所在的进程结束后,该进程占用的所有资源都要释放

暂停次数Suspend Count

线程内核对象中的Suspend Count用于指明线程的暂停计数。

当调用CreateProcess (创建进程的主线程)或CreateThread函数时,线程的内核对象就被创建了,它暂停计数被初始化为1 (即处于暂停状态),这可以阻止新创建的线程被调度到CPU中。

因为线程的初始化需要时间,当线程完全初始化好了之后, CreateProcess或CreateThread检查是否传递了CREATE_SUSPENDED标志。如果传递了这个标志,那么这些函数就返回,同时新线程处于暂停状态。

如果尚未传递该标志,那么线程的暂停计数将被递减为0。当线程的暂停计数是0的时候,该线程就处于可调度状态。

暂停次数为1:暂停状态

暂停次数为0:可调度状态

ResuneThread唤醒一个线程

1
DWORD ResuneThread (HANDLE hThread);      //唤醒一个挂起的线程

该函数减少线程的暂停计数,当计数值减到0的时候,线程被恢复运行。如果调用成功ResumeThread函数返回线程的前一个暂停计数,否则返回OxFFFFFFFF (-1)。
单个线程可以被暂停若干次。如果一个线程被暂停了3次,它必须被唤醒3次才可以分配给一个CPU

SuspendThread函数挂起一个线程。

1
2
3
DWORD WINAPI SuspendThread(
  _In_HANDLE hThread
  );

可调度的(没有处于暂停状态)

大约每经20ms, Windows查看一次当前存在的所有线程内核对象。在这些对象中,只有
一少部分是可调度的(没有处于暂停状态), Windows选择其中的一个内核对象,将它的
CONTEXT (上下文)装入CPU的寄存器,这一过程称为上下文转换

退出代码Exit Code

成员Exit Code指定了线程的退出代码,也可以说是线程函数的返回值。在线程运行期间,线程函数还没有返回, Exit Code的值是STILL_ACTIVE。线程运行结束后,系统自动将Exit Code设为线程函数的返回值。可以用GetExitCodeThread函数得到线程的退出代码。

1
2
3
4
5
6
7
8
9
10
DWORD dwExitCode;
if(GetExitCodeThread(hThread, &dwExitCode))
{
if(dwExitCode == STILL ACTIVE)
{ } //目标线程还在运行
else
{ } //目标线程已经中止,退出代码为dwExitCode

}
........

GetExitCodeThread得到线程的退出代码

1
BOOL GetExitCodeThread( HANDLE hThread, LPDWORD lpExitCode);

是否受信Signaled

成员Signaled指示了线程对象是否为“受信”状态。

线程在运行期间, Signaled的值永远是FALSE,即“未受信”,只有当线程结束以后,系统才把Signaled的值置为TRUE,此时,针对此对象的等待函数就会返回,如上一小节中的WaitForSingleObject函数。

线程结束后,会变成受信状态

线程的终止

一当线程正常终止时,会发生下列事件:
在线程函数中创建的所有C++对象将通过它们各自的析构函数被正确地销毁。
该线程使用的堆栈将被释放。
系统将线程内核对象中Exit Code (退出代码)的值由STILL_ ACTIVE设置为线程函数的返回值。
系统将递减线程内核对象中Usage Code (使用计数)的值。

终止线程的执行有4种方法:

(1)线程函数自然退出。当函数执行到return语句返回时, Windows将终止线程的执行。
建议使用这种方法终止线程的执行。
(2)使用ExitThread函数来终止线程,原型如下:

1
void ExitThread( DWORD dwExiCode )//线程的退出代码

ExitThread函数会中止当前线程的运行,促使系统释放掉所有此线程使用的资源。但是,
CCt资源却不能得到正确地清除。

(3)使用TerminateThread函数在一个线程中强制终止另一个线程的执行,原型如下:

1
2
3
4
BOOL TerminateThread(
HANDLE hThread, //目标线程句柄
DWORD dwExitCode //目标线程的退出代码
);

(4)使用ExitProcess函数结束进程,这时系统会自动结束进程中所有线程的运行。用这
种方法相当于对每个线程使用TerminateThread函数,所以也应当避免这种情况。

1
DECLSPEC_NORETURN VOID ExitProcess(UINT uExitCode);	//进程退出代码

线程的优先级

每个线程都要被赋予一个优先级号,取值为0(最低)到31 (最高)。

调用WaitForSingleObject函数就会导致主线程处于不可调度状态,还有在第4章要讨论的GetMessage函数,也会使线程暂停运行。

Windows支持6个优先级类: idle, below normal, normal, above normal,high和real-time.

线程刚被创建时,他的相对优先级总是被设置为normal,若要改变线程的优先级,必须
使用下面这个函数:

1
BOOL SetThreadPriority(HANDLE hThread,int nPriority );

hThread参数是目标线程的句柄, nPriority参数定义了线程的优先级,取值如下所示:

THREAD_PRIORITY_TIME_CRITICAL Time-critical (实时)

THREAD_PRIORITY_HIGHEST_Highest (最高)

THREAD_PRIORITY_ABOVE_NORMAL Above normal (高于正常, Windows 98不支持)

THREAD_PRIORITY_NORMAL Nornal (正常)

THREAD_PRIORITY_BELOW_NORMAL Below normal (低于正常, Windows 98不支持)

THREAD_PRIORITY_LOWEST Lowest (最低)

THREAD_PRIORITY_IDLE Idle (空闲)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#include <windows.h>

DWORD WINAPI ThreadIdle(LPVOID lpParam)
{
int i = 0;
while(i++<10)
printf("Idle Thread is running \n");

return 0;
}

DWORD WINAPI ThreadNormal(LPVOID lpParam)
{
int i = 0;
while(i++<10)
printf(" Normal Thread is running \n");

return 0;
}
int main(int argc, char* argv[])
{
DWORD dwThreadID;
HANDLE h[2];

// 创建一个优先级为Idle的线程
h[0] = ::CreateThread(NULL, 0, ThreadIdle, NULL,
CREATE_SUSPENDED, &dwThreadID);
::SetThreadPriority(h[0], THREAD_PRIORITY_IDLE);
::ResumeThread(h[0]);

// 创建一个优先级为Normal的线程
h[1] = ::CreateThread(NULL, 0, ThreadNormal, NULL,
0, &dwThreadID);

// 等待两个线程内核对象都变成受信状态
::WaitForMultipleObjects(
2, // DWORD nCount 要等待的内核对象的数量
h, // CONST HANDLE *lpHandles 句柄数组
TRUE, // BOOL bWaitAll 指定是否等待所有内核对象变成受信状态
INFINITE); // DWORD dwMilliseconds 要等待的时间

::CloseHandle(h[0]);
::CloseHandle(h[1]);

return 0;
}

/*

*/

创建线程时可以给参数指定CREATE_SUSPENDED,让线程挂起

ResumeThread函数恢复线程运行。

WaitForMultipleObjects函数

1
2
3
4
5
6
DWORD WaitForMultipleObjects(
DWORD nCount, //数组个数
const HANDLE* lpHandles, //句柄数组
BOOL bWaitAll, //指定是否等待所有内核对象变成受信状态
DWORD dwMilliseconds //要等待的时间
);

用于等待多个内核对象,前两个参数分别为要等待的内核对象的个数和句柄数组指针。

如果将第三个参数bWaitAll的值设为TRUE,等待的内核对象全部变成受信状态以后此函数才返回。否则, bWaitAll为0的话,只要等待的内核对象中有一个变成了受信状态, WaitForMultipleObjects就返回,返回值指明了是哪一个内核对象变成了受信状态。

参数bWaitAll为FALSE的时候, WaitForMultpleObjects函数从索引0开始扫描整个句柄
数组,第一个受信的内核对象将终止函数的等待,使函数返回。

下面的代码说明了函数返回值的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
HANDLE h[2];
h[0] = hThread1;
h[1] = hThread2;
DWORD dw = ::WaitForMultipleObjects(2, h, FALSE, 5000);
switch(dw)
{
case WAIT_FAILED:
// 调用WaitForMultipleObjects函数失败(句柄无效?)
break;
case WAIT_TIMEOUT:
// 在5秒内没有一个内核对象受信
break;
case WAIT_OBJECT_0 + 0:
// 句柄h[0]对应的内核对象受信
break;
case WAIT_OBJECT_0 + 1:
// 句柄h[1]对应的内核对象受信
break;
}

C/C++运行期库

#include <process.h>

在实际的开发过程中,一般不直接使用windows系统提供的CreateThread函数创建线程,
而是使用C/C++运行期函数_beginthreadex。

_beginthreadex的参数与CreateThread函数是对应的,只是参数名和类型不完全相同,使用的
时候需要强制转化。

1
2
3
4
5
6
7
8
unsigned long _beginthreadex(
void *security,
unsigned stack_size,
unsigned (__stdcall *start_address) ( void *),
void *arglist,
unsigned initflag,
unsigned *thrdaddr
);

相应地, C/C++运行期库也提供了另一个版本的结束当前线程运行的函数,用于取代
ExitThread函数。

1
void  _endthreadex(unsigned retval ); //指定退出代码

这个函数会释放_beginthreadex为保持线程同步而申请的内存空间,然后再调用ExitThread
函数来终止线程。

使用_beginthreadex来创建一个线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>
#include<windows.h>
#include <process.h>

using namespace std;

DWORD WINAPI my(LPVOID lpParam)
{
cout << 132 << endl;
cout << 132 << endl;
cout << 132 << endl;
return 0;
}

int main(char *argv[], int argc)
{
DWORD threadId;
HANDLE h = (HANDLE) _beginthreadex(NULL,NULL,(_beginthreadex_proc_type)my,NULL,0, (unsigned*)&threadId);
WaitForSingleObject(h, INFINITE);
CloseHandle(h);
return 0;
}

线程同步

临界区对象

当多个线程在同一个进程中执行时,可能有不止一个线程同时执行同一段代码,访问同一段内存中的数据。多个线程同时读共享数据没有问题,但如果同时读和写,情况就不同了。

使用临界区对象

临界区对象是定义在数据段中的一个CRITICAL_SECTION结构, Windows内部使用这个结构记录一些信息,确保在同一时间只有一个线程访问该数据段中的数据。

编程的时候,要把临界区对象定义在想保护的数据段中,然后在任何线程使用此临界区对象之前对它进行初始化。

1
2
void InitializeCriticalSection(LPCRITICAL_SECTION IpCriticalSection);
//指向数据段中定义的CRITICAL_SECTION结构

线程访问临界区中数据的时候,必须首先调用EnterCriticalSection函数,申请进入临界区(文叫关键代码段),在同一时间内, Windows只允许一个线程进入临界区。

所以在申请的时候,如果有另一个线程在临界区的话, EnterCriticalSection函数会一直等待下去,直到其他线程离开临界区才返回。EnterCriticalSection函数用法如下:

1
void EnterCriticalSection( LPCRITICAL_SECTION IpCriticalSection);

当操作完成的时候,还要将临界区交还给Windows,以便其他线程可以申请使用。这个工作由LeaveCriticalSection函数来完成。

1
void LeaveCriticalSection( LPCRITICAL_SECTION IpCriticalSection);

当程序不再使用临界区对象的时候,必须使用DeleteCriticalSection函数将它删除。

1
void DeleteCriticalSection( LPCRITICAL_SECTION IpCriticalSection);

现在使用临界区对象来改写上面有同步问题的计数程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <windows.h>
#include <process.h>

BOOL g_bContinue = TRUE;
int g_nCount1 = 0;
int g_nCount2 = 0;
CRITICAL_SECTION g_cs; // 对存在同步问题的代码段使用临界区对象

UINT __stdcall ThreadFunc(LPVOID);

int main(int argc, char* argv[])
{
UINT uId;
HANDLE h[2];

// 初始化临界区对象
::InitializeCriticalSection(&g_cs);

h[0] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
h[1] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);

// 等待1秒后通知两个计数线程结束,关闭句柄
Sleep(1000);
g_bContinue = FALSE;
::WaitForMultipleObjects(2, h, TRUE, INFINITE);
::CloseHandle(h[0]);
::CloseHandle(h[1]);

// 删除临界区对象
::DeleteCriticalSection(&g_cs);

printf("g_nCount1 = %d \n", g_nCount1);
printf("g_nCount2 = %d \n", g_nCount2);

return 0;
}

UINT __stdcall ThreadFunc(LPVOID)
{
while(g_bContinue)
{
::EnterCriticalSection(&g_cs);
g_nCount1++;
g_nCount2++;
::LeaveCriticalSection(&g_cs);
}
return 0;
}

互锁函数

互锁函数为同步访问多线程共享变量提供了一个简单的机制。如果变量在共享内存,不同
进程的线程也可以使用此机制。

用于互锁的函数有InterlockedIncrement. InterlockedDecrement.
InterlockedExchangeAdd, InterlockedExchangePointer等.

InterlockedIncrement函数递增(加1)指定的32位变量。这个函数可以阻止其他线程同
时使用此变量,函数原型如下:

1
2
LONG InterlockedIncrement( LONG volatile* Addend);
//指向要递增的变量

InterlockedDecrement函数同步递减(减1)指定的32位变量,原型如下:

1
2
LONG InterlockedDecrement( LONG volatile* Addend);
//指向要递减的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
///////////////////////////////////////////////////////////////
// InterlockDemo.cpp文件


#include <stdio.h>
#include <windows.h>
#include <process.h>

int g_nCount1 = 0;
int g_nCount2 = 0;
BOOL g_bContinue = TRUE;

UINT __stdcall ThreadFunc(LPVOID);

int main(int argc, char* argv[])
{
UINT uId;
HANDLE h[2];

h[0] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
h[1] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);


// 等待1秒后通知两个计数线程结束,关闭句柄
Sleep(1000);
g_bContinue = FALSE;
::WaitForMultipleObjects(2, h, TRUE, INFINITE);
::CloseHandle(h[0]);
::CloseHandle(h[1]);

printf("g_nCount1 = %d \n", g_nCount1);
printf("g_nCount2 = %d \n", g_nCount2);

return 0;
}

UINT __stdcall ThreadFunc(LPVOID)
{
while(g_bContinue)
{
::InterlockedIncrement((long*)&g_nCount1);
::InterlockedIncrement((long*)&g_nCount2);
}
return 0;
}

事件内核对象

多线程程序设计大多会涉及线程间相互通信。

事件对象(event)是一种抽象的对象,它也有未受信(nonsignaled)和受信(signaled)两种状态,编程人员也可以使用WaitForSingleObject函数等待其变成受信状态。

事件对象包含3个成员: nUsageCount (使用计数)、bManualReset (是否人工重置)和
bSignaled (是否受信)。

成员nUsagecount记录当前的使用计数,当使用计数为0的时候,Windows就会销毁此内核对象占用的资源;

成员bManualReset指定在一个事件内核对象上等待的函数返回之后, Windows是否重置这个对象为未受信状态;

成员bsignaled指定当前事件内核对象是否受信。

如果想使用事件对象,需要首先用CreateEvent函数去创建它,初始状态下, nUsageCount
的值为1.

1
2
3
4
5
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES IpEventAttributes, //用来定义事件对象的安全属性
BOOL bManualReset, //指定是否需要手动重置事件对象为未受信状态。
BOOL bInitialState,//指定事件对象创建时的初始状态
LPCWSTR IpName);//事件对象的名称

参数bManualReset对应着内核对象中的bManualReset成员。

自动重置(auto-reset)和人工重置(manual-reset)是事件内核对象两种不同的类型。

当一个人工重置的事件对象受信以后,所有等待在这个事件上的线程都会变为可调度状态(暂停次数为0);

可是当一个自动重置的事件对象受信以后, Windows仅允许一个等待在该事件上的线程变成可调度状态,然后就自动重置此事件对象为未受信状态。

blnitialState参数对应着bSignaled成员。

将它设为TRUE,则表示事件对象创建时的初始化状态为受信(结束)(bSignaled =TRUE);设为FALSE时,状态为未受信(未结束)(bSignaled =FALSE)。

IpName参数用来指定事件对象的名称。为事件对象命名是为了在其他地方(比如,其他
进程的线程中)使用OpenEvent或CreateEvent函数获取此内核对象的句柄。

1
2
3
4
HANDLE OpenEvent (
DWORD dwDesiredAccess, //指定想要的访问权限
BOOL blnheritHandle, //指定返回句柄是否可被继承
LPCWSTR IpName); //要打开的事件对象的名称

系统创建或打开一个事件内核对象后,会返回事件的句柄。当编程人员不使用此内核对象的时候,应该调用CloseHandle函数释放它占用的资源。

事件对象被建立后,程序可以通过SetEvent和ResetEvent函数来设置它的状态。

1
2
BOOL SetEvent(HANDLE hEvent ); //将事件状态设为"受信(sigaled) ";
BOOL ResetEvent(HANDLE hEvent); //将事件状态设为"未受信(nonsigaled) ";

通常情况下,为一个自动重置类型的事件对象调用ResetEvent函数是不必要的,因为Windows会自动重置此事件对象。

下面例子中,主线程通过将事件状态设为“受信”来通知子线程开始工作。这是事件内核对
象一个很重要的用途,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <windows.h>
#include <process.h>

HANDLE g_hEvent;
UINT __stdcall ChildFunc(LPVOID);

int main(int argc, char* argv[])
{
HANDLE hChildThread;
UINT uId;

// 创建一个自动重置的(auto-reset events),未受信的(nonsignaled)事件内核对象
g_hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);

hChildThread = (HANDLE)::_beginthreadex(NULL, 0, ChildFunc, NULL, 0, &uId);

// 通知子线程开始工作
printf("Please input a char to tell the Child Thread to work: \n");
getchar();
::SetEvent(g_hEvent);

// 等待子线程完成工作,释放资源
::WaitForSingleObject(hChildThread, INFINITE);
printf("All the work has been finished. \n");
::CloseHandle(hChildThread);
::CloseHandle(g_hEvent);
return 0;
}

UINT __stdcall ChildFunc(LPVOID)
{
::WaitForSingleObject(g_hEvent, INFINITE);
printf(" Child thread is working...... \n");
::Sleep(5*1000); // 暂停5秒,模拟真正的工作
return 0;
}

信号量内核对象

信号量(Semaphore)内核对象对线程的同步方式与前面几种方法不同,它允许多个线程
在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。

在用CreateSemaphore函数创建信号量时,即要同时指出允许的最大资源计数和当前可用资源计数。

一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可
用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。

但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能再允许
其他线程的进入,此时的信号量信号将无法发出。

线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore函数将当前可用资源计数加1,在任何时候当前可用资源计数决不可能大于最大资源计数。

1622071965844

以箭头和白色箭头表示共享资源所允许的最大资源计数和当前可用资源计数。

黑色箭头表示已经访问的资源个数。当可用资源为0时,其他线程不能进入,直到可用资源大于0时,其他线程才可访问

信号量也被称作Dikstrait数器。

使用信号量内核对象进行线程同步主要会用到CreateSemaphore,OpenSemaphore、ReleaseSemaphore、 WaitForSingleObject和WaitForMultipleObjects等函数。

其中CreateSemaphore用来创建一个信号量内核对象,其函数原型为:

1
2
3
4
5
6
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES IpSemaphoreAttributes,//安全属性指针
LONG IInitialCount,//初始计数
LONG IMaximumCount,//最大计数
LPCTSTR IpName//对象名指针
);

参数IMaximumCount是一个有符号32位值,定义了允许的最大资源计数,最大取值不能
超过4294967295, IpName参数可以为创建的信号量定义一个名字,由于其创建的是一个内核
对象,因此在其他进程中可以通过该名字而得到此信号量。

OpenSemaphore ()函数即可用来根据信号量名打开在其他进程中创建的信号量,函数原型如下:

1
2
3
4
5
HANDLE OpenSemaphore(
DWORD dwDesiredAccess, //访问标志
BOOL bInheritHandle, //继承标志
LPCTSTR IpName //信号量名
);

在线程离开对共享资源的处理时,必须通过ReleaseSemaphore来增加当前可用资源计数。否则,将会出现当前正在处理共享资源的实际线程数并没有达到要限制的数值,而其他线程却因为当前可用资源计数为0而仍无法进入的情况。

ReleaseSemaphore的函数原型为:

1
2
3
4
5
BOOL ReleaseSemaphore(
HANDLE hSemaphore, //信号量句柄
LONG IReleaseCount, //计数递增数量
LPLONG IpPreviousCount //先前计数
);

该函数将IReleaseCount中的值添加给信号量的当前资源计数,一般将IReleaseCount设置
为1,如果需要也可以设置其他的值。

WaitForSingleObject和WaitForMultipleObjects主要用在试图进入共享资源的线程函数入口处,主要用来判断信号量的当前可用资源计数是否允许本线程的进入。

只有在当前可用资源计数值大于0时,被监视的信号量内核对象才会得到通知。

信号量的使用特点使甚更适用于对Socket (套接字)程序中线程的同步。

例如,网络上的HTTP服务器要对同一时间内访问同一页面的用户数加以限制,这时可以为没一个用户对服务
器的页面请求设置一个线程,而页面则是待保护的共享资源,通过使用信号量对线程的同步作用可以确保在任一时刻无论有多少用户对某一页面进行访问,只有不大于设定的最大用户数目的线程能够进行访问,而其他的访问企图则被挂起,只有在有用户退出对此页面的访问后才有可能进入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include<iostream>
#include<windows.h>
#include<process.h> //使用_beginthreadex创建线程需要包含此文件
using namespace std;

//信号量对象句柄
HANDLE hSemaphore;

UINT WINAPI myThread(LPVOID pParam)
{
//试图进入信号量关口
WaitForSingleObject(hSemaphore, INFINITE);
for (int i = 0; i < 5; i++)
{
cout << i << endl;
}
//释放信号量计数
ReleaseSemaphore(hSemaphore,1,NULL);

return 0;
}

int main(char *argv[], int argc)
{
//创建信号量对象
hSemaphore = CreateSemaphore(NULL, 1, 1, NULL);

HANDLE h1 = (HANDLE) _beginthreadex(NULL, NULL, (_beginthreadex_proc_type)myThread, NULL, 0, NULL);
HANDLE h2 = (HANDLE)_beginthreadex(NULL, NULL, (_beginthreadex_proc_type)myThread, NULL, 0, NULL);
HANDLE h3 = (HANDLE)_beginthreadex(NULL, NULL, (_beginthreadex_proc_type)myThread, NULL, 0, NULL);
HANDLE h4 = (HANDLE)_beginthreadex(NULL, NULL, (_beginthreadex_proc_type)myThread, NULL, 0, NULL);
WaitForSingleObject(h1, INFINITE);
WaitForSingleObject(h2, INFINITE);
WaitForSingleObject(h3, INFINITE);
WaitForSingleObject(h4, INFINITE);

CloseHandle(h1);
CloseHandle(h2);
CloseHandle(h3);
CloseHandle(h4);
return 0;
}

互斥内核对象

互斥(Mutex)是一种用途非常广泛的内核对象。

能够保证多个线程对同一共享资源的互斥访问。

同临界区有些类似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。

当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。

与其他几种内核对象不同,互斥对象在操作系统中拥有特殊代码,并由操作系统来管理,操作系统
甚至还允许其进行一些其他内核对象所不能进行的非常规操作。

1622074362650

黑点表示令牌,只有拿到令牌的线程才能进入访问资源,访问结束后要交出令牌,不然其他线程会一直无法访问该资源。

以互斥内核对象来保持线程同步可能用到的函数主要有CreateMutex、OpenMutex.
ReleaseMutex、 WaitForSingleObject和WaitForMultipleObjects等。

在使用互斥对象前,首先要通过CreateMutex或OpenMutex创建或打开一个互斥对象。

CreateMutex函数原型如下:

1
2
3
4
5
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES IpMutexAttributes, //安全属性指针
BOOL bInitialOwner, //初始拥有者
LPCTSTR IpName //互斥对象名
)

参数blnitialowner主要用来控制互斥对象的初始状态。一般多将其设置为FALSE,以表
明互斥对象在创建时并没有为任何线程所占有。

如果在创建互斥对象时指定了对象名,那么可以在本进程其他地方或是在其他进程通过OpenMutex函数得到此互斥对象的句柄。

OpenMutex函数原型为:

1
2
3
4
5
HANDLE OpenMutex(
DWORD dwDesiredAccess,//访问标志
BOOL bInheritHandle, //继承标志
LPCTSTR IpName //互斥对象名
)

当目前对资源具有访问权的线程不再需要访问此资源而要离开时,必须通过ReleaseMutex函数来释放其拥有的互斥对。

ReleaseMutex函数原型为:

1
BOOL ReleaseMutex(HANDLE hMutex);

其惟一的参数hMutex为待释放的互斥对象句柄。

至于WaitForSingleObject和WaitForMultipleObjects等待函数在互斥对象保持线程同步中所起的作用与在其他内核对象中的作用是基本一致的,也是等待互斥内核对象的通知。

但是这里需要特别指出的是:在互斥对象通知引起调用等待函数返回时,等待函数的返回值不再是通常的WAIT_OBJECT_0 (对于WaitForSingleObject函数)或是在WAIT_OBJECT_0WAIT_OBJECT_0+nCount-1之间的一个值(对于WaitForMultipleObiects函数)

而是将返回一个WAIT_ABANDONED_0 (对于WaitForSingleObject函数)或是在WAIT_ABANDONED_0WAIT_ABANDONED_0+nCount-1之间的一个值(对于WaitForMultipleObjects函数) ,以此来表明线程正在等待的互斥对象由另外一个线程所拥有,而此线程却在使用完共享资源前就已经终止。

除此之外,使用互斥对象的方法在等待线程的可调度性上同使用其他几种内核对象的方法也有所不同,其他内核对象在没有得到通知时,受调用等待函数的作用,线程将会挂起,同时失去可调度性,而使用互斥的方法却可以在等待的同时仍具有可调度性,这也正是互斥对象所能完成的非常规操作之一

关于WAIT_ABANDONED或WAIT_ABANDONED_0返回值

假设有A、B两个线程和一个互斥量hMutex。如果A线程调用WaitForSingleObject获取到互斥量后,并没有调用ReleaseMutex来释放互斥量就终止了(如调用了ExitThread,TerminateThread)。然后线程B调用WaitForSingleObject就会返回WAIT_ABANDONED,并且线程B获取到互斥量,线程B使用完成后应该调用ReleasMutex释放互斥量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include<iostream>
#include<windows.h>
#include<process.h>
using namespace std;

//信号量对象句柄
HANDLE hMutex;
HANDLE h1;
HANDLE h2;
HANDLE h3;
HANDLE h4;

UINT WINAPI myThread(LPVOID pParam)
{
WaitForSingleObject(hMutex, INFINITE);
cout << "调用了" << (char *)pParam << "线程" << endl;
ReleaseMutex(hMutex);

//试图进入信号量关口
DWORD dw = WaitForSingleObject(hMutex, INFINITE);
switch (dw)
{
case WAIT_FAILED:
// 调用WaitForMultipleObjects函数失败(句柄无效?)
cout << (char *)pParam << "线程" << "调用WaitForMultipleObjects函数失败" << endl;
return 0;
break;
case WAIT_TIMEOUT:
// 在10毫秒后没有一个内核对象受信
cout << (char *)pParam << "线程" << "等待10ms后Mutex内核对象未受信,所以结束本线程" << endl;
return 0;
break;
case WAIT_OBJECT_0:
// hMutex句柄对应的内核对象受信
cout << (char *)pParam << "线程" << " hMutex句柄对应的内核对象受信,本线程会正常工作" << endl;
break;
case WAIT_ABANDONED_0:
cout << "上一个线程在使用完之后,没有使用ReleaseMutex释放" << endl;
cout << (char *)pParam << ": ";
cout << 123 << endl;
ReleaseMutex(hMutex);
return 0;
}
cout << (char *)pParam << ": ";
cout << 123 << endl;
//释放信号量计数
//ReleaseMutex(hMutex);

return 0;
}

int main(char *argv[], int argc)
{
//创建信号量对象
hMutex = CreateMutex(NULL,FALSE,NULL);

h1 = (HANDLE) _beginthreadex(NULL, NULL, (_beginthreadex_proc_type)myThread, "a", 0, NULL);
h2 = (HANDLE)_beginthreadex(NULL, NULL, (_beginthreadex_proc_type)myThread, "b", 0, NULL);
h3 = (HANDLE)_beginthreadex(NULL, NULL, (_beginthreadex_proc_type)myThread, "c", 0, NULL);
h4 = (HANDLE)_beginthreadex(NULL, NULL, (_beginthreadex_proc_type)myThread, "d", 0, NULL);

WaitForSingleObject(h1, INFINITE);
WaitForSingleObject(h2, INFINITE);
WaitForSingleObject(h3, INFINITE);
WaitForSingleObject(h4, INFINITE);
return 0;
}

在编写程序时,互斥对象多用在对那些为多个线程所访问的内存块的保护上,可以确保任
何线程在处理此内存块时都对其拥有可靠的独占访问权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include<iostream>
#include<windows.h>
#include<process.h>
using namespace std;

//信号量对象句柄
HANDLE hMutex;
HANDLE h1;
HANDLE h2;
HANDLE h3;
HANDLE h4;

UINT WINAPI myThread(LPVOID pParam)
{
cout << "调用了" << (char *)pParam << "线程" << endl;

//试图进入信号量关口
DWORD dw = WaitForSingleObject(hMutex, 10);
switch (dw)
{
case WAIT_FAILED:
// 调用WaitForMultipleObjects函数失败(句柄无效?)
cout << (char *)pParam << "线程" << "调用WaitForMultipleObjects函数失败" << endl;
return 0;
break;
case WAIT_TIMEOUT:
// 在10毫秒后没有一个内核对象受信
cout << (char *)pParam << "线程" << "等待10ms后Mutex内核对象未受信,所以结束本线程" << endl;
return 0;
break;
case WAIT_OBJECT_0:
// hMutex句柄对应的内核对象受信
cout << (char *)pParam << "线程" << " hMutex句柄对应的内核对象受信,本线程会正常工作" << endl;
break;
}
Sleep(500);
cout << (char *)pParam << ": ";
cout << 123 << endl;
//释放信号量计数
ReleaseMutex(hMutex);

return 0;
}

int main(char *argv[], int argc)
{
//创建信号量对象
hMutex = CreateMutex(NULL,FALSE,NULL);

h1 = (HANDLE) _beginthreadex(NULL, NULL, (_beginthreadex_proc_type)myThread, "a", 0, NULL);
h2 = (HANDLE)_beginthreadex(NULL, NULL, (_beginthreadex_proc_type)myThread, "b", 0, NULL);
h3 = (HANDLE)_beginthreadex(NULL, NULL, (_beginthreadex_proc_type)myThread, "c", 0, NULL);
h4 = (HANDLE)_beginthreadex(NULL, NULL, (_beginthreadex_proc_type)myThread, "d", 0, NULL);

WaitForSingleObject(h1, INFINITE);
WaitForSingleObject(h2, INFINITE);
WaitForSingleObject(h3, INFINITE);
WaitForSingleObject(h4, INFINITE);
return 0;
}

线程局部存储

线程局部存储(thread-local storage, TLS)是一个使用很方便的存储线程局部数据的系统。
利用TLS机制可以为进程中所有的线程关联若干个数据,各个线程通过由TLS分配的全局索引来访问与自己关联的数据。

这样,每个线程都可以有线程局部的静态存储数据。
用于管理TLS的数据结构是很简单的, Windows仅为系统中的每一个进程维护一个位数组,再为该进程中的每一个线程申请一个同样长度的数组空间。

1622081496611

运行在系统中的每一个进程都有一个位数组。

位数组的成员是一个标志,每个标志的值被设为FREE或INUSE,指示了此标志对应的数组索引是否在使用中。

Windodws保证至少有TLS_MINIMUM_AVAILABLE(定义在WinNTh文件中)个标志位可用。

(1)主线程调用TIsAlloc函数为线程局部存储分配索引,函数原型为:

1
DWORD TIsAlloc(void);		//返回一个TLS索引

系统为每一个进程都维护着一个长度为TLS_MINIMUM_AVAILABLE的位数组, TIsAlloc的返回值就是数组的一个下标(索引)。

这个位数组的惟一用途就是记忆哪一个下标在使用中。

初始状态下,此位数组成员的值都是FREE,表示未被使用。

当调用TIsAlloc的时候,系统会挨个检查这个数组中成员的值,直到找到一个值为FREE的成员。把找到的成
员的值由FREE改为INUSE后, TIsAlloc函数返回该成员的索引。

如果不能找到一个值为FREE的成员, TIsAlloc函数就返回TLS_OUT_OF_INDEXES (在WinBase.h文件中定义为-1),意味着失败。

当一个线程被创建时, Windows就会在进程地址空间中为该线程分配一个长度为TLS_MINIMUM_AVAILABLE的数组,数组成员的值都被初始化为0。
在内部,系统将此数组与该线程关联起来,保证只能在该线程中访问此数组中的数据。
每个线程都有它自己的数组,数组成员可以存储任何数据。

(2)每个线程调用TIsSetValue和TIsGetValue设置或读取线程数组中的值,TIsSetValue函数原型为:

1
2
3
4
5
BOOL TIsSetValue(
DWORD dwTisindex, //TLS索引
LPVOID IpTIsValue //要设置的值
);
LPVOID TIsGetValue(DWORD dwTIslIndex);//TLS索引

TlsSetValue函数将参数IpTIsValue指定的值放入索引为dwTIsIndex的线程数组成员中。

这样,IpTisValue的值就与调用TIisSetValue函数的线程关联了起来。

此函数调用成功,会返回TRUE

调用TIsSetValue函数,一个线程只能改变自己线程数组中成员的值,而没有办法为另
个线程设置TLS值。

到现在为止,将数据从一个线程传到另一个线程的惟一方法是在创建线
程时使用线程函数的参数。

TIsGetValue函数的作用是取得线程数组中索引为dwTIsIndex的成员的值。

TlsSetValue和TIsGetValue分别用于设置和取得线程数组中的特定成员的值,而它们使用
的索引就是TIsAlloc函数的返回值。

这就充分说明了进程中惟一的位数组和各线程数组的关系。

例如, TIsAlloc返回3,那就说明索引3被此进程中的每一个正在运行的和以后要被创建的线程保存起来,用以访问各自线程数组中对应的成员的值。

(3)主线程调用TIsFree释放局部存储索引。函数的惟一参数是TIsAlloc返回的索引。
利用TLS可以给特定的线程关联一个数据。比如下面的例子将每个线程的创建时间与该
线程关联了起来,这样,在线程终止的时候就可以得到线程的生命周期。整个跟踪线程运行时
间的例子的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
///////////////////////////////////////////////////////////////
// 02UseTLS.cpp.cpp文件


#include <stdio.h>
#include <windows.h>
#include <process.h>

// 利用TLS记录线程的运行时间

DWORD g_tlsUsedTime;
void InitStartTime();
DWORD GetUsedTime();


UINT __stdcall ThreadFunc(LPVOID)
{
int i;

// 初始化开始时间
InitStartTime();

// 模拟长时间工作
i = 10000*10000;
while(i--) { }

// 打印出本线程运行的时间
printf(" This thread is coming to end. Thread ID: %-5d, Used Time: %d \n",
::GetCurrentThreadId(), GetUsedTime());
return 0;
}

int main(int argc, char* argv[])
{
UINT uId;
int i;
HANDLE h[10];

// 通过在进程位数组中申请一个索引,初始化线程运行时间记录系统
g_tlsUsedTime = ::TlsAlloc();

// 令十个线程同时运行,并等待它们各自的输出结果
for(i=0; i<10; i++)
{
h[i] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
}
for(i=0; i<10; i++)
{
::WaitForSingleObject(h[i], INFINITE);
::CloseHandle(h[i]);
}

// 通过释放线程局部存储索引,释放时间记录系统占用的资源
::TlsFree(g_tlsUsedTime);
return 0;
}

// 初始化线程的开始时间
void InitStartTime()
{
// 获得当前时间,将线程的创建时间与线程对象相关联
DWORD dwStart = ::GetTickCount();
::TlsSetValue(g_tlsUsedTime, (LPVOID)dwStart);
}

// 取得一个线程已经运行的时间
DWORD GetUsedTime()
{
// 获得当前时间,返回当前时间和线程创建时间的差值
DWORD dwElapsed = ::GetTickCount();
dwElapsed = dwElapsed - (DWORD)::TlsGetValue(g_tlsUsedTime);
return dwElapsed;
}

GetTickCount函数可以取得Windows从启动开始经过的时间,其返回值是以毫秒为单位
的已启动的时间