线程
# 深入理解 windows 内核对象与句柄
# 内核对象
- Windows 中每个内核对象都只是一个内存块,它由操作系统内核分配,并只能由操作系统内核进行访问,应用程序并不能操作或修改内核对象。
- 这个内存块是一个数据结构,其成员维护着与对象相关的信息。
- 少数成员(安全描述符和使用计数)是所有内核对象都有的,但大多数成员都是不同类型对象特有的。
如:file 文件对象、event 事件对象、process 进程、thread 线程、 iocompletationport 完成端口(windows 服务器)、mailslot 邮槽、mutex 互斥量 和 registry 注册表等
# 内核对象的使用计数与生命期
- 内核对象的所有者是操作系统内核,而非进程。换言之也就是说当进程退出,内核对象不一定会销毁。
- 操作系统内核通过内核对象的使用计数,知道当前有多少个进程正在使用一个特定的内核对象。
- 初次创建内核对象,使用计数为 1。当另一个进程获得该内核对象的访问权之后,使用计数加 1。如果内核对象的使用计数递减为 0,操作系统内核就会销毁该内核对象。也就是说内核对象在当前进程中创建,但是当前进程退出时,内核对象有可能被另外一个进程访问。这时,进程退出只会减少当前进程对引用的所有内核对象的使用计数,而不会减少其他进程对内核对象的使用计数(即使该内核对象由当前进程创建)。那么内核对象的使用计数未递减为 0,操作系统内核不会销毁该内核对象。
# 操作内核对象
Windows 提供了一组函数进行操作内核对象。成功调用一个创建内核对象的函数后,会返回一个句柄,它表示了所创建的内核对象,可由进程中的任何线程使用。在 32 位进程中,句柄是一个 32 位值,在 64 位进程中句柄是一个 64 位值。我们可以使用唯一标识内核对象的句柄,调用内核操作函数对内核对象进行操作。
# 内核对象与其他类型的对象
Windows 进程中除了内核对象还有其他类型的对象,比如窗口,菜单,字体等,这些属于用户对 象和 GDI 对象。要区分内核对象与非内核对象,最简单的方式就是查看创建这个对象的函数,几 乎所有创建内核对象的函数都有一个允许我们指定安全属性的参数。
注意:
1 一个对象是不是内核对象,通常可以看创建此对象 API 的参数中是否需要: PSECURITY_ATTRIBUTES 类型的参数。
2 内核对象只是一个内存块,这块内存位于操作系统内核的地址空间,内存块中存 放一个数据结构(此数据结构的成员有如:安全描述符、使用计数等)。
3 每个进程中有一个句柄表(handle table),这个句柄表仅供内核对象使用
4 调用
hThread = CreateThread(... , &threadId);
当调用了 CreateThread CreateFile 等创建内核对象的函数后, 就是相当于操作系统多了一个内存块,这个内存块就是内核对象也是此时内核对象被创建,其数据结构中的引用计数初始为 1(这样理解:只要内核对象被创建,其引用计数被初始化为 1),这里实则发生两件事:创建了一个内核对象和创建线程的函数打开(访问)了此对象,所以内核对象的引用计数加 1, 这时引用计数就为 2 了。 调用 API CreateThread 的时候,不仅仅是创建了一个内核对象,引用计数+1,还打开了内核对象 +1,所以引用计数变为 2
当调用 CloseHandle(hThread)时发生这样的事情:系统通过 hThread 计算出此句柄在句柄表中的索引,然后把那一项处理后标注为空闲可用的项,内核对象的引用计数减 1 即此时此内核对象的引用计数为 1,之后这个线程句柄与创建时产生的内核对象已经没有任何关系了。不能通过 hThread 句柄去访问内核对象了,只有当内核对象的引用计数为 0 时,内核对象才会被销毁,而此时它的引用计数为 1,那它什么时候会被销毁? 当此线程结束的时候,它的引用计数再减 1 即为 0,内核对象被销毁。此时又有一个新问题产生:我们已经关闭了线程句柄,也就是这个线程句柄已经和内核对象没 有瓜葛了,那么那个内核对象是怎么又可以和此线程联系起来了呢? 其实是创建线程时产生的那个线程ID
# 线程创建函数
# CreateThread
CreateThread 是一种微软在 Windows API 中提供了建立新的线程的函数,该函数在主线程的基础上创建一个新线程。线程终止运行后,线程对象仍然在系统中,必须通过 CloseHandle 函数来关闭该线程对象。
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD
SIZE_T dwStackSize,//initialstacksize
LPTHREAD_START_ROUTINE lpStartAddress,//threadfunction
LPVOID lpParameter,//threadargument
DWORD dwCreationFlags,//creationoption
LPDWORD lpThreadId//threadidentifier
)
- 第一个参数 lpThreadAttributes 表示线程内核对象的安全属性,一般传入 NULL 表示使用默认设置。
- 第二个参数 dwStackSize 表示线程栈空间大小。传入 0 表示使用默认大小 (1MB)。
- 第三个参数 lpStartAddress 表示新线程所执行的线程函数地址,多个线程 可以使用同一个函数地址。
- 第四个参数 lpParameter 是传给线程函数的参数。
- 第五个参数 dwCreationFlags 指定额外的标志来控制线程的创建,为 0 表 示线程创建之后立即就可以进行调度,如果为 CREATE_SUSPENDED 则表 示线程创建后暂停运行,这样它就无法调度,直到调用 ResumeThread()。
- 第六个参数 lpThreadId 将返回线程的 ID 号,传入 NULL 表示不需要返回 该线程 ID 号
# _beginthreadex
unsigned long _beginthreadex(
void *security, // 安全属性, 为 NULL 时表示默认安全性
unsigned stack_size, // 线程的堆栈大小, 一般默认为 0
unsigned(_stdcall *start_address)(void *), // 线程函数
void *argilist, // 线程函数的参数
unsigned initflag, // 新线程的初始状态,0 表示立即 执行,//CREATE_SUSPENDED 表示创建之后挂起
unsigned *threaddr // 用来接收线程 ID
);
//返回值 :
// 成功返回新线程句柄, 失败返回 0
//__stdcall 表示
//1.参数从右向左压入堆栈
//2.函数被调用者修改堆栈
# 创建线程案例
#include <stdio.h>
#include <windows.h>
#include <process.h>
DWORD WINAPI ThreadFun(LPVOID p)
{
int iMym = *((int*)p);
printf("我是子线程,PID = %d,iMym = %d\n", GetCurrentThreadId(), iMym);
return 0;
}
int main()
{
printf("main begin\n");
HANDLE hThread;
DWORD dwThreadID;
int m = 100;
hThread = CreateThread(NULL, 0, ThreadFun, &m, 0, &dwThreadID);
printf("我是主线程,PID = %d\n", GetCurrentThreadId());
CloseHandle(hThread);
Sleep(2000);
system("pause");
return 0;
}
# 主线程等待子线程执行结束
WaitForSingleObject 来等待一个内核对象变为已通知状态,即等待所指内核对象执行完毕
WaitForSingleObject(
_In_ HANDLE hHandle, //指明一个内核对象的句柄
_In_ DWORD dwMilliseconds //等待时间
);
WaitForMultipleObjects(
_In_ DWORD nCount, // 要监测的句柄的组的句柄的个数
_In_reads_(nCount) CONST HANDLE* lpHandles, //要监测的句柄的组
_In_ BOOL bWaitAll, // TRUE 等待所有的内核对象发出信号, FALSE 任意一个内核对象发出信号
_In_ DWORD dwMilliseconds //等待时间
);
# 线程同步
# 互斥对象Mutex
- 互斥对象(mutex)属于内核对象,它能够确保线程拥有对单个资源的互斥访问权。
- 互斥对象包含一个使用数量,一个线程 ID 和一个计数器。其中线程 ID 用于标识系统中的哪个线程当前拥有互斥对象,计数器用于指明该线程拥有互斥对象的次数。
- 创建互斥对象:调用函数 CreateMutex。调用成功,该函数返回所创建的互斥对象的句柄。
- 请求互斥对象所有权:调用函数 WaitForSingleObject 函数。线程必须主动请求共享对象的所有权才能获得所有权。
- 释放指定互斥对象的所有权:调用 ReleaseMutex 函数。线程访问共享资源结束后,线程要主动释放对互斥对象的所有权,使该对象处于已通知状态。
HANDLE WINAPI CreateMutexW(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes, //指向安全属性
_In_ BOOL bInitialOwner, //初始化互斥对象的所有者 TRUE 立即拥有互斥体
_In_opt_ LPCWSTR lpName //指向互斥对象名的指针 L“Bingo”
);
#include <stdio.h>
#include <windows.h>
#include <process.h>
#define NUM_THREAD 50
unsigned WINAPI threadInc(void * arg);
unsigned WINAPI threadDes(void * arg);
long long num=0;
HANDLE hMutex;
int main(int argc, char *argv[])
{
HANDLE tHandles[NUM_THREAD];
int i;
hMutex=CreateMutex(NULL, FALSE, NULL);
for(i=0; i<NUM_THREAD; i++)
{
if(i%2)
tHandles[i]=(HANDLE)_beginthreadex(NULL, 0, threadInc, NULL, 0, NULL);
else
tHandles[i]=(HANDLE)_beginthreadex(NULL, 0, threadDes, NULL, 0, NULL);
}
WaitForMultipleObjects(NUM_THREAD, tHandles, TRUE, INFINITE);
CloseHandle(hMutex);
printf("result: %lld \n", num);
return 0;
}
unsigned WINAPI threadInc(void * arg)
{
int i;
WaitForSingleObject(hMutex, INFINITE);
for(i=0; i<500000; i++)
num+=1;
ReleaseMutex(hMutex);
return 0;
}
unsigned WINAPI threadDes(void * arg)
{
int i;
WaitForSingleObject(hMutex, INFINITE);
for(i=0; i<500000; i++)
num-=1;
ReleaseMutex(hMutex);
return 0;
}
# 事件对象
事件对象也属于内核对象,它包含以下三个成员:
● 使用计数;
● 用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值;
● 用于指明该事件处于已通知状态还是未通知状态的布尔值。
事件对象有两种类型:人工重置的事件对象和自动重置的事件对象。这两种事件对象的区别在于当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;而当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程变为可调度线程。
- 创建事件对象 调用 CreateEvent 函数创建或打开一个命名的或匿名的事件对象。
- 设置事件对象状态 调用 SetEvent 函数把指定的事件对象设置为有信号状态。
- 重置事件对象状态 调用 ResetEvent 函数把指定的事件对象设置为无信号状态。
- 请求事件对象 线程通过调用 WaitForSingleObject 函数请求事件对象。
//创建事件对象的函数原型如下:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性
BOOL bManualReset, // 复位方式 TRUE 必须用 ResetEvent 手动复原 FALSE 自动还原为无信号状态
BOOL bInitialState, // 初始状态 TRUE 初始状态为有信号状态 FALSE 无信号状态
LPCTSTR lpName //对象名称 NULL 无名的事件对象
);
# 信号量
内核对象的状态:
触发状态(有信号状态),表示有可用资源。
未触发状态(无信号状态),表示没有可用资源
工作原理
以一个停车场是运作为例。假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。这个停车系统中,每辆车就好比一个线程,看门人就好比一个信号量,看门人限制了可以活动的线程。假如里面依然是三个车位,但是看门人改变了规则,要求每次只能停两辆车,那么一开始进入两辆车,后面得等到有车离开才能有车进入,但是得保证最多停两辆车。对于 Semaphore 而言,就如同一个看门人,限制了可活动的线程数。
信号量的组成
- 计数器:该内核对象被使用的次数
- 最大资源数量:标识信号量可以控制的最大资源数量(带符号的 32 位)
- 当前资源数量:标识当前可用资源的数量(带符号的 32 位)。即表示当前开放资源的个数(注意不是剩下资源的个数),只有开放的资源才能被线程所申请。但这些开放的资源不一定被线程占用完。比如,当前开放 5 个资源,而只有 3 个线程申请,则还有 2 个资源可被申请,但如果这时总共是 7 个线程要使用信号量,显然开放的资源 5 个是不够的。这时还可以再开放 2 个,直到达到最大资源数量。
信号量的规则如下: (1)如果当前资源计数大于 0,那么信号量处于触发状态(有信号状态),表示有 可用资源。 (2)如果当前资源计数等于 0,那么信号量属于未触发状态(无信号状态),表 示没有可用资源。 (3)系统绝对不会让当前资源计数变为负数 (4)当前资源计数绝对不会大于最大资源计数 信号量与互斥量不同的地方是,它允许多个线程在同一时刻访问同一资源,但是需 要限制在同一时刻访问此资源的最大线程数目。信号量对象对线程的同步方式与前 面几种方法不同,信号允许多个线程同时使用共享资源。
//创建信号量
HANDLE WINAPI CreateSemaphoreW(
_In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // Null 安全属性
_In_ LONG lInitialCount, //初始化时,共有多少个资源是可以用的。 0:未触发状//态(无信号 状态),表示没有可用资源
_In_ LONG lMaximumCount, //能够处理的最大的资源数量 3
_In_opt_ LPCWSTR lpName //NULL 信号量的名称
);
//增加信号量
WINAPI ReleaseSemaphore(
_In_ HANDLE hSemaphore, //信号量的句柄 _In_ LONG lReleaseCount, //将lReleaseCount值加到信号量的当前资源计数上面 0-> 1
_Out_opt_ LPLONG lpPreviousCount //当前资源计数的原始值
);
//关闭句柄
CloseHandle(
_In_ _Post_ptr_invalid_ HANDLE hObject
);
# 关键代码段
关键代码段,也称为临界区,工作在用户方式下。它是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。通常把多线程中访问同一种资源的那部分代码当做关键代码段。
- 初始化关键代码段 调用 InitializeCriticalSection 函数初始化一个关键代码段。 InitializeCriticalSection( Out LPCRITICAL_SECTION lpCriticalSection ); 该函数只有一个指向 CRITICAL_SECTION 结构体的指针。在调用 InitializeCriticalSection 函数之前,首先需要构造一个 CRITICAL_SCTION 结构体类型的对象,然后将该对象的地址传递给 InitializeCriticalSection 函数。
- 进入关键代码段 VOID WINAPI EnterCriticalSection( Inout LPCRITICAL_SECTION lpCriticalSection ); 调用 EnterCriticalSection 函数,以获得指定的临界区对象的所有权,该函数等待指定的临界区对象的所有权,如果该所有权赋予了调用线程,则该函数就返回;否则该函数会一直等待,从而导致线程等待。
- 退出关键代码段 VOID WINAPI LeaveCriticalSection( Inout LPCRITICAL_SECTION lpCriticalSection ); 线程使用完临界区所保护的资源之后,需要调用 LeaveCriticalSection 函数,释放指定的临界区对象的所有权。之后,其他想要获得该临界区对象所有权的线程就可以获得该所有权,从而进 入关键代码段,访问保护的资源。
- 删除临界区 WINBASEAPI VOID WINAPI DeleteCriticalSection( Inout LPCRITICAL_SECTION lpCriticalSection ); 当临界区不再需要时,可以调用 DeleteCriticalSection 函数释放该对象,该函数将释放一 个没有被任何线程所拥有的临界区对象的所有资源。
//卖票系统
#include <stdio.h>
#include <windows.h>
#include <process.h>
int iTickets = 5000;
CRITICAL_SECTION g_cs;
// A窗口 B窗口
DWORD WINAPI SellTicketA(void* lpParam)
{
while (1)
{
EnterCriticalSection(&g_cs);//进入临界区
if (iTickets > 0)
{
Sleep(1);
iTickets--;
printf("A remain %d\n", iTickets);
LeaveCriticalSection(&g_cs);//离开临界区
}
else
{
LeaveCriticalSection(&g_cs);//离开临界区
break;
}
}
return 0;
}
DWORD WINAPI SellTicketB(void* lpParam)
{
while (1)
{
EnterCriticalSection(&g_cs);//进入临界区
if (iTickets > 0)
{
Sleep(1);
iTickets--;
printf("B remain %d\n", iTickets);
LeaveCriticalSection(&g_cs);//离开临界区
}
else
{ LeaveCriticalSection(&g_cs);//离开临界区
break;
}
}
return 0;
}
int main()
{
HANDLE hThreadA, hThreadB;
hThreadA = CreateThread(NULL, 0, SellTicketA, NULL, 0, NULL); //2
hThreadB = CreateThread(NULL, 0, SellTicketB, NULL, 0, NULL); //2
CloseHandle(hThreadA); //1
CloseHandle(hThreadB); //1
InitializeCriticalSection(&g_cs); //初始化关键代码段
Sleep(40000);
DeleteCriticalSection(&g_cs);//删除临界区
system("pause");
return 0;
}