Windows线程基础使用
内核对象
在Windows中,有许多不同的资源,如进程、线程、事件、文件,信号量等等,其中大部分都是通过不同的函数请求创建的。系统管理这些不同资源的方式也不尽相同,如使用CreateFile创建文件时,我们需要传入文件的打开模式,若是线程,我们则应该注册并维护线程ID,线程所属进程等信息。
系统为了以记录相关信息的方式来管理各种资源,在其内部生成数据块,就是一个数据结构,用里面的成员来记录对象的相关信息。
这个数据块是由系统内核分配的,也只有内核才能访问。Windows提供了一些函数以使我们来访问各种内核对象,调用这些函数后会返回一个HANDLE句柄,以此句柄来标识创建的对象。
HANDLE其实也是一个指针,只是被刻意模糊了,这样就可以防止用户直接操纵关键性的系统资源。一般它是一个整数值,这个值作为一个隐藏的指针表格的索引,这个表格使我们来间接访问内部系统,虽然这个HANDLE并未提供operator->,但在管理和语义上都很像指针。
系统内核也以引用计数的那种方式来管理一个内核对象(关于引用计数,可参考上篇文章),创建对象时,初始计数,另一进程对他进行访问后,计数加1,进程终止时,自动递减,当为0时才销毁该内核对象。
系统使用一个安全描述符来保护内核对象,这个安全描述符记录着谁拥有此对象,谁可以访问它,使用它。
安全描述符的结构如下:
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES;
在所有创建内核对象的函数中,基本都会有一个参数指向安全描述符,如CreateFile,其原型如下:
HANDLE CreateFile(
LPCTSTR lpFileName, //欲打开或创建的文件名
DWORD dwDesiredAccess, //访问模式
DWORD dwShareMode, //共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
//指向安全描述符
DWORD dwCreationDisposition, //创建方式
DWORD dwFlagsAndAttributes, //指定新建文件的属性和对文件操作的方式
HANDLE hTemplateFile //文件模块句柄,系统会Copy该文件模板的所有属性到当前创建的文件中。
);
其中lpSecurityAttributes就指向这个安全描述符。
有创建自然就有关闭,我们需要使用Close()函数来表示以结束使用此内核对象。此时计数会减1,若为0,系统便会销毁此内核对象。
CloseHandle()的原型如下:
BOOL CloseHandle(
HANDLE hObject //handle to object
);
线程(Thread)
一个多线程程序有多个执行点,在多处理器的机器上,不同的线程会真的存在;而在单处理器的机器上,系统采用一种分时技术来将每个线程切割成较短的时间间隔,一个线程执行时便暂停其它线程,给每个线程一些处理时间,这些时间是非常短的,在用户看来就像是程序支持多线程操作。
因为有了多线程,所以我们可以一边听音乐,一边编辑文档,或是一边聊微信。
若一个程序不支持多线程,当用户执行一个操作,若有别的操作还在运行,程序便会出现无响应,程序不应该允许这种情况发生。
但是线程一旦共享资源就会出现同步问题,因为任何线程都会被其它线程中断,所以使用线程并不难,重点是是如何同步对象来安全的共享资源。
Windows线程
Windows创建线程使用的API原型如下:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, //安全属性
DWORD dwStackSize, //初始线程栈大小
LPTHREAD_START_ROUTINE lpStartAddress, //指向线程函数
LPVOID lpParameter, //新线程参数
DWORD dwCreationFlags, //创建的额外标志
LPDWORD lpThreadId //存储系统分配给新线程的ID
);
参数虽多,但一般我们只会用到lpStartAddress和lpParameter这两个,剩余的填0或NULL即可。
线程的终止有4种方式:
- 正常返回
- ExitThread
- TerminateThread
- 终止进程
我们应该尽力使用第一种方式。
ExitThread函数的原型如下:
VOID ExitThread(
DWORD dwExitCode // 退出码
);
ExitThread()会终止主线程的运行,执行后,操作系统会清理该线程使用的所有系统资源,我们所使用的C++对象不会被析构。
TerminateThread函数的原型如下:
BOOL TerminateThread(
HANDLE hThread, // 要终止的线程句柄
DWORD dwExitCode // 退出码
);
从原型可以看出,TerminateThread()能杀死任何线程。这个函数是异步的,也就是说,在函数返回时并不保证线程已经终止了。这是因为,若是程序中还有其它的线程要引用被杀死的这个线程堆栈上的数据,而它已被销毁,便会出现访问错误。
可以使用WaitForSingleObject函数来保证线程终止。
终止进程的函数也可以终止线程,它们会终止该进程中的所有线程,如ExitProcess和TerminateProcess函数。这样也会影响程序的清理工作,像C++析构操作就不会调用。
安全的C/C++线程函数
在C/C++中,并不推荐使用CreateThread函数,这是由于C要早于Windows操作系统,当时的开发者并未考虑到线程问题。我们应该使用更安全的_beginthreadex函数,其原型如下:
#include <process.h>
uintptr_t _beginthreadex(
void * security, // 线程安全相关信息,使用默认设置时传递NULL
unsigned stack_size, // 要分配给线程的栈大小,传递0时生成默认大小的栈
unsigned (* start_address)(void *), // 传递线程的main函数信息
void * arglist, // 调用main时传递的参数信息
unsigned initflag, // 用于指定线程创建后的行为,传递0时,线程创建后立即进入可执行状态
unsigned * thrdaddr // 用于保存线程ID的变量地址值
);
//成功返回线程句柄,失败返回0
这个函数的参数个数和意义以及顺序都和CreateThread相同,只是将类型替换为了C类型,这样就不会依赖于Windows类型了。
C/C++的运行库函数会为每一个新线程准备一个独立的内存块,当使用_beginthreadex时会分配和初始化这个内存块,然后将其与新线程关联。
内存块中有一个数据结构和使用了C/C++运行库的每个线程关联,当使用这些函数时,它们可以去查找所调用线程的内存块,以不影响其它线程。
还有一个函数是_beginthread函数,使用简单,但它会让创建线程时返回的句柄失效,以防止访问内核对象,不应使用这个函数,所以也便不讨论了。
我们可以使用_beginthreadex来开一个线程:
#include <windows.h>
#include <iostream>
#include <cassert>
#include <process.h>
//线程函数
unsigned WINAPI ThreadFunc(LPVOID arg);
int main()
{
HANDLE hThread; // 线程句柄
unsigned uThreadID; // 线程ID
int iParam = 5; // 线程参数
hThread = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, (void*)iParam, 0, &uThreadID);
assert(hThread != 0);
Sleep(3000); // 等待3秒
std::cout << "end of main" << std::endl;
std::cin.get();
return 0;
}
unsigned WINAPI ThreadFunc(LPVOID arg)
{
int n = (int)arg;
for (int i = 0; i < n; ++i)
{
Sleep(1000);
std::cout << i << " running thread" << std::endl;
}
return 0;
}
输出如下:
10 running thread
21 running thread
3end of main
42 running thread
53 running thread
64 running thread
在内核对象中有个标识使用状态的信息Signaled,当还有地方在引用此线程时,这个状态便为true,直到变为false时线程才退出。
若要保证线程终止再执行返回函数,可以使用WaitForSingleObject或WaitForMultipleObjects等待线程执行完毕,原型如下:
/*用于验证单个内核对象的Signaled状态*/
DWORD WaitForSingleObject(
HANDLE hHandle, // 要等待线程的句柄
DWORD dwMilliseconds // 超时时间
);
//返回值:进入signaled状态返回WAIT_OBJECT_0,超时返回WAIT_TIMEOUT
/*用于验证多个内核对象的Signaled状态*/
DWORD WaitForMultipleObjects(
DWORD nCount, // 句柄数
CONST HANDLE *lpHandles, // 指向句柄数组
BOOL fWaitAll, // 等待标志
DWORD dwMilliseconds // 超时时间
);
更改上面的程序使线程返回再执行后续操作:
#include <windows.h>
#include <iostream>
#include <cassert>
#include <process.h>
unsigned WINAPI ThreadFunc(LPVOID arg);
int main()
{
HANDLE hThread; // 线程句柄
unsigned uThreadID; // 线程ID
int iParam = 5; // 线程参数
hThread = (HANDLE)_beginthreadex(nullptr, 0, ThreadFunc, (void*)iParam, 0, &uThreadID);
assert(hThread != 0);
// 传递INFINITE时函数会直到内核对象变为Signaled状态都会返回
DWORD dwSignal;
dwSignal = WaitForSingleObject(hThread, INFINITE);
assert(dwSignal != WAIT_FAILED);
std::cout << "wait result:" << ((dwSignal == WAIT_OBJECT_0) ? "Signed" : "time-out") << std::endl;
std::cout << "end of main" << std::endl;
std::cin.get();
return 0;
}
unsigned WINAPI ThreadFunc(LPVOID arg)
{
int n = (int)arg;
for (int i = 0; i < n; ++i)
{
Sleep(1000);
std::cout << i << " running thread" << std::endl;
}
return 0;
}
现在输出如下:
10 running thread
21 running thread
32 running thread
43 running thread
54 running thread
6wait result:Signed
7end of main