//线程函数
//创建线程
HANDLE CreateThread(
PSECURITY_ATTRIBUTES psa, //线程安全属性
DWORD cbStackSize, // 线程堆栈大小,默认是 1MB(Itanium芯片上,默认大小是4MB)
PTHREAD_START_ROUTINE pfnStartAddr, // 线程函数地址
PVOID pvParam, // 传入线程的参数
DWORD dwCreateFlags, // 控制线程的标志: CREATE_SUSPENDED: 创建之后挂起; 0: 创建之后立即执行
PDWORD pdwThreadID); // 返回值:线程ID
DWORD WINAPI ThreadFunc(PVOID pvParam)
{
DWORD dwResult = 0;
return dwResult;
}
线程可以通过以下4种方法来终止运行。
1. 线程函数返回(这是强烈推荐的)。
2. 线程通过调用ExitThread函数“杀死”自己
终止线程运行,并导致操作系统清理该线程的所有操作系统资源,但C/C++资源(如C++类对象)不会被销毁
3. 同一个进程或另一个进程中的线程调用TerminateThread函数
异步终止线程,线程终止时不会得到通知。ExitThread函数终止线程,线程的堆栈会被销毁,但是使用TerminateThread,除非拥有此线程的进程终止运行,否则系统不会销毁这个线程堆栈,Microsoft特意用这种方式来实现TerminateThread,其他还在运行的线程可引用被终止线程堆栈上的值。 动态链接库在线程终止运行时会收到通知,但被TerminateThread终止的线程,DLL不会收到这个通知,其结果不能执行正常的清理工作。
4. 包含线程的进程终止运行。
对CreateThread函数的一个调用系统创建一个线程内核对象,该对象最初的使用计数为2(只有线程终止同时从CreateThread返回的句柄关闭,否则线程内核对象不会被销毁)该线程内核对象的其他属性也被初始化:暂停计数被设为1,退出代码被设计为STILL_ACTIVE(0x103),内核对象被设为nonsignaled状态。一旦创建了内核对象,系统就分配内存,供线程的堆栈使用,此内存是从进程的地址空间内分配的,因为线程没有自己的地址空间。系统将CreateThread函数的pvParam参数压入堆栈,然后将传给CreateThread函数的pfnStartAddr值压入堆栈。
每个线程都有自己的一组CPU寄存器,称为线程的上下文(context)。上下文反映了当线程上一次执行时,线程的CPU寄存器的状态。线程的CPU寄存器全部保存在一个CONTEXT结构。CONTEXT结构本身保存在线程内核对象中。线程始终在进程的上下文中运行。 当线程内核对象被初始化的时候,CONTEXT结构的堆栈指针寄存器被设为 pfnStartAddr 在线程堆栈中的地址,而指令指针寄存器被设为 RtlUserThreadStart 函数:
VOID RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam) {
__try {
ExitThread((pfnStartAddr)(pvParam));
}
__except(UnhandledExceptionFilter(GetExceptionInformation())) {
ExitProcess(GetExceptionCode());
}
// NOTE: We never get here.
}
线程完全初始化好之后,系统将检查CREATE_SUSPENDED标志是否传给CreateThread函数,如果没有传递,系统将线程的暂停计数递减至0,线程开始运行。
Microsoft Visual Studio附带的C/C++库:
库名称 | 描述 |
---|---|
LibCMt.lib | 库的静态链接Release版本 |
LibCMtD.lib | 库的静态链接Debug版本 |
MSVCRt.lib | 导入库,用于动态链接MSVCR80.dll库的Release版本 |
MSVCRtD.lib | 导入库,用于动态链接MSVCR80D.dll库的Debug版本 |
MSVCMRt.lib | 导入库,用于托管/原生代码混合 |
MSVCURt.lib | 导入库,编译成百分之百纯MSIL代码 |
标准C运行库是1970年左右发明的,并没有线程的概念,标准C运行库的发明者也没有考虑到为多线程应用程序使用C运行库的问题。例如,标准C运行库一部分函数在出错时设置的是全局变量 errno,多线程环境出出问题的C/C++运行库变量和函数有:errno,_doserrno, strtok, _wcstok, strerror, _strerror, tmpnam, tmpfile, asctime, _wasctime, gmtime, _ecvt和_fcvt等等。
为了保证这些函数的正确执行,C/C++库必须在每个线程中为这些函数维护一个数据结构,并且还要保证此数据结构不会被其他线程修改,或是无意识的修改了其他线程的这个数据结构,使用C/C++运行库函数时,创建多线程用的函数版本是:_beginthreadex、_endthreadex ,该函数会为每个线程创建一个与线程挂钩的线程局部变量,这样线程之间就不会相互影响。
unsigned long _beginthreadex(
void *security,
unsigned stack_size,
unsigned (*start_address)(void *),
void *arglist,
unsigned initflag,
unsigned *thrdaddr)
{
_ptiddata ptd; // 线程数据块指针
uintptr_t thdl; // 线程句柄
//分配线程数据块
if ((ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL)
goto error_return;
// 初始化线程数据块
initptd(ptd);
//线程参数与线程函数
ptd->_initaddr = (void *) pfnStartAddr;
ptd->_initarg = pvParam;
ptd->_thandle = (uintptr_t)(-1);
// 调用Windows API创建线程
thdl = (uintptr_t) CreateThread((LPSECURITY_ATTRIBUTES)psa,
cbStackSize,
_threadstartex, //线程函数是 _threadstartex, 而非pfnStartAddr
(PVOID) ptd, // 线程参数是 tiddata, 而不是传入的线程参数
dwCreateFlags,
pdwThreadID);
if (thdl == 0) {
goto error_return;
}
//
return(thdl);
error_return:
_free_crt(ptd);
return((uintptr_t)0L);
}
static unsigned long WINAPI _threadstartex (void* ptd) { //新线程先执行 RtlUserThreadStart,然后再跳转到 _threadstartex.
// Note: ptd is the address of this thread's tiddata block.
// Associate the tiddata block with this thread so
// _getptd() will be able to find it in _callthreadstartex.
TlsSetValue(__tlsindex, ptd); //TlsSetValue Windows API 将一个值与线程关联起来,就是线程本地存储(Thread Local Storage, TLS)
// Save this thread ID in the _tiddata block.
((_ptiddata) ptd)->_tid = GetCurrentThreadId();
// Initialize floating-point support (code not shown).
// call helper function.
_callthreadstartex();
// We never get here; the thread dies in _callthreadstartex.
return(0L);
}
static void _callthreadstartex(void) {
_ptiddata ptd; /* pointer to thread's _tiddata struct */
// get the pointer to thread data from TLS
ptd = _getptd();
// Wrap desired thread function in SEH frame to
// handle run-time errors and signal support.
__try {
// Call desired thread function, passing it the desired parameter.
// Pass thread's exit code value to _endthreadex.
_endthreadex(
( (unsigned (WINAPI *)(void *))(((_ptiddata)ptd)->_initaddr) )
( ((_ptiddata)ptd)->_initarg ) ) ;
}
__except(_XcptFilter(GetExceptionCode(), GetExceptionInformation())){
// The C run-time's exception handler deals with run-time errors
// and signal support; we should never get it here.
_exit(GetExceptionCode());
}
}
void __cdecl _endthreadex (unsigned retcode) {
_ptiddata ptd; // Pointer to thread's data block
// Clean up floating-point support (code not shown).
// Get the address of this thread's tiddata block.
ptd = _getptd_noexit (); // _getptd_noexit函数在内部调用操作系统的 TlsGetValue函数,获取线程的tiddata内存块地址
// Free the tiddata block.
if (ptd != NULL)
_freeptd(ptd);
// Terminate the thread.
ExitThread(retcode);
}
对于 errno 之类的 C/C++库全局变量,其在标准库C headers中定义如下:
_CRTIMP extern int * __cdecl _errno(void);
#define errno (*_errno());
int* __cdecl _errno(void) {
_ptiddata ptd = _getptd_noexit(); //获得线程本地存储变量
if (!ptd) {
return &ErrnoNoMem;
} else {
return (&ptd->_terrno);
}
}
对于创建线程,早期C/C++运行库还包括一对函数:
unsigned long _beginthread(
void (__cdecl *start_address)(void *),
unsigned stack_size,
void *arglist);
void _endthread(void);
与新版本_beginthreadex和_endthreadex函数相比,_beginthread函数的参数较少,局限性比较大,不能创建具有安全属性的线程,不能创建可以挂起的线程,也不能获取线程ID值。
_endthread也不能设置退出代码,其退出码被硬编码位0,而且_endthread函数会在调用ExitThread前,会调用CloseHandle,向其传入新线程的句柄:
DWORD dwExitCode;
HANDLE hThread = _beginthread(...);
GetExitCodeThread(hThread, &dwExitCode); //如果新建线程已经运行结束,因为_endthread已关闭了线程的句柄,所以hThread是无效的,后面再调用CloseHandle函数时就会出错。
CloseHandle(hThread);
HANDLE hProc = GetCurrentProcess(); //进程句柄
HANDLE hThread = GetCurrentThread(); //线程句柄
UINT unPID = GetCurrentProcessId();//进程ID
UINT unTID = GetCurrentThreadId(); //线程ID
虽然在很多情况下获得本地的进程/线程的句柄比获取本地的PID/TID要有用得多,但是需要注意的是,使用GetCurrentXX获取的句柄并不是真正的句柄,而是伪句柄,这个伪句柄仅能在本地使用而不能在其他进程/线程中使用,每个线程/进程都有一个自己的伪句柄,这个伪句柄类似一个指针,它会指向真正句柄所在的位置,因此如果试图将本线程的伪句柄传递给其他线程使用,它指向的内容将会变成其他线程的句柄,这会导致句柄传递失败(因为伪句柄是一个指向句柄的指针,它相对于句柄的偏移都一样,所以当伪句柄传递给另一个线程时,会指向另一个线程本身)。
一段有问题的代码:
DWORD WINAPI ChildThread(PVOID pParam)
{
HANDLE hThreadParent = (HANDLE)pParam;
FILETIME stcCreationTime,stcExitTime;
FILETIME stcKernelTime, stcUserTime;
GetThreadTimes(hThreadParent, &stcCreationTime,
&stcExitTime, &stcKernelTime, &stcUserTime);
}
DWORD WINAPI ShowParentTime()
{
HANDLE hThreadParent = GetCurrentThread();
CreateThread(NULL, 0, ChildThread, (PVOID)hThreadParent, 0, NULL);
}
我们可以用DuplicateHandle()函数先将伪句柄转为真正的句柄后,然后在传入线程函数中使用:
HANDLE hThreadParent = GetCurrentThread();
DuplicateHandle(
GetCurrentProcess(), //拥有源句柄的进程句柄
GetCurrentThread(), //指定对象的现有句柄(伪句柄)
GetCurrentProcess(), //拥有新对象句柄的进程句柄
&hThreadParent, //用于保存新句柄
0, //安全访问级别
false, //是否可以被子进程继承
DUPLICATE_SAME_ACCESS//转换选项);
注意:在实际开发中,我们需要确保知道要挂起的线程正在做什么,否则会导致程序运行不稳定,例如当一个正在执行堆创建的线程被挂起时,此线程会将这个堆锁定,这会导致所有试图访问这个堆的线程被卡死,直到此线程恢复后并创建完这个堆为止。
VOID SuspendProcess(DWORD dwProcessID, BOOL fSuspend) {
// 获取进程内线程的快照
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwProcessID);
if (hSnapshot != INVALID_HANDLE_VALUE) {
//遍历线程
THREADENTRY32 te = { sizeof(te) };
BOOL fOk = Thread32First(hSnapshot, &te);
for(; fOk; fOk = Thread32Next(hSnapshot, &te)) {
if (te.th32OwnerProcessID == dwProcessID) {
HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, te.th32ThreadID);
if (hThread != NULL) {
if (fSuspend)
SuspendThread(hThread);
else
ResumeThread(hThread);
}
CloseHandle(hThread);
}
}
CloseHandle(hSnapshot);
}
}
可以通过SwitchToThread()函数切换到另一个线程
BOOL bReturn = SwitchToThread();
UINT64 unStartTime = GetTickCount64();
//Do Something...
UINT64 unEndTime = GetTickCount64();
BOOL WINAPI GetThreadTimes(
HANDLE hThread, //线程句柄
LPFILETIME lpCreationTime, //创建时间( 格林威治)
LPFILETIME lpExitTime, //退出时间(格林威治)
LPFILETIME lpKernelTime, //内核时间(绝对时间)
LPFILETIME lpUserTime); //用户时间(绝对时间)
// 格林威治时间指的是以100ns为单位,从1601年1月1日零时到现在的一个时间计数
我们可以使用以下代码精准的测量代码执行时间
UINT64 FileTimeToUint64(PFILETIME pFileTime)
{
return Int64ShllMod32(pFileTime->dwHighDateTime, 32) | pFileTime->dwLowDateTime;
}
FILETIME stcKernelStartTime, stcKernelEndTime;
FILETIME stcUserStartTime, stcUserEndTime;
FILETIME stcTemp;
GetThreadTimes(GetCurrentThread(), &stcTemp, &stcTemp,
&stcKernelStartTime, &stcUserStartTime);
// Do Something...
GetThreadTimes( GetCurrentThread(), &stcTemp, &stcTemp,
&stcKernelEndTime, &stcUserEndFile);
UINT64 unKernelTime = FileTimeToUint64(&stcKernelEndTime)
- FileTimeToUint64(&stcKernelStartTime);
UINT64 unUserTime = FileTimeToUint64(&stcUserEndTime)
- FileTimeToUint64(&stcUserStartTime);
UINT64 unAllTime = UNkERNELtIME + unUserTime;
//1.获取当前CPU的频率
LARGE_INTECER stcFrenquency;
QueryPerformanceFrequency(&stcFrequency);
//2.获取起始时钟周期
LARGE_INTEGER stcStartTime;
QueryPerformanceCounter(&stcStartTime);
// Do Something...
//3. 获取结束时钟周期
LARGE_INTEGER stcEndTime;
QueryPerformanceCounter(&stcEndTime);
//4.计算时钟周期差值
LARGE_INTEGER stcTime;
stcTime.QuadPart = stcEndTime.QuadPart - stcStartTime.QuadPart;
//5. 换算经过的微妙数(1s=1000000um)
stcTime.QuadPart *= 1000000;
stcTime.QuadPart /= stcFrequency.QuadPart;
系统为每个线程都维护了一个上下文(CONTEXT),这个上下文中记录了当前线程中各种CPU状态的值,其中包括通用寄存器、标志寄存器等我们可以使用如下方法获取/设置线程上下文
//获取线程上下文
CONTEXT stcCxt = (CONTEXT_FULL);
stcCxt.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER; //指定要获取线程上下文中的那些信息
if (!GetThreadContext(hTargetThread, &stcCrt))
return false;
//通过修改stcCxt来修改寄存器
//设置进程上下文
if (!SetThreadContext(hTargetThread, &stcCxt))
return false;
Windows支持6个优先级类,他们分别是idle、below normal、normal、above normal、high、real-time
进程优先级
优先级类型 | 级别 | 解释 |
---|---|---|
real-time | 实时 | 此进程的所有计算请求会实时被响应。操作系统内核的时间也会被尽可能的分配给此线程使用 |
high | 高 | 此进程内的事件必须被立即响应 |
above normal | 高于标准 | 比标准高一些 |
normal | 标准 | 无特殊调度需求(90%进程属于此情况) |
below normal | 低于标准 | 比标准低一点 |
idle | 低 | 此进程的事件在系统空闲时会被响应 |
线程优先级
优先级类型 | 级别 | 解释 |
---|---|---|
Time-critical | 实时 | 此线程的所有计算请求会实时响应,操作系统内核的时间也会被尽可能分配给此线程使用 |
highest | 高 | |
above normal | ||
normal | ||
below normal | ||
idle |
优先级映射表:
线程优先级 | Idle | below normal | normal | above normal | high | real-time |
---|---|---|---|---|---|---|
Time-critical | 15 | 15 | 15 | 15 | 15 | 31 |
highest | 6 | 8 | 10 | 12 | 15 | 26 |
above normal | 5 | 7 | 9 | 11 | 14 | 25 |
normal | 4 | 6 | 8 | 10 | 13 | 24 |
below normal | 3 | 5 | 7 | 9 | 12 | 23 |
lowest | 2 | 4 | 6 | 8 | 11 | 22 |
Idle | 1 | 1 | 1 | 1 | 1 | 16 |
优先级类型 | 级别 | 宏 |
---|---|---|
real-time | 实时 | REALTIME_PRIORITY_CLASS |
high | 高 | HIGH_PRIORITY_CLASS |
above_normal | 高于标准 | ABOVE_NORMAL_PRIORITY_CLASS |
normal | 标准 | NORMAL_PRIORITY_CLASS |
below normal | 低于标准 | BELOW_NORMAL_PRIORITY_CLASS |
idle | 低 | IDLE_PRIORITY_CLASS |
C:\>START /LOW CALC.EXE
START 命令共有 "/BELOWNORMAL"、"/NORMAL"、"/ABOVENORMAL"、
"/HIGH"、"/REALTIME"这几个优先级开关
// 如果程序已经运行起来了,那么我们最后还可以使用API修改其优先级,其API原型如下所示:
BOOL WINAPI SetPriorityClass(
_IN_ HANDLE hProcess, //进程句柄
_IN_ DWORD dwPriorityClass //进程优先级宏
);
除了进程以外,我们还可以为线程设定优先级,其API原型如下所示:
BOOL WINAPI SetThreadPriority(
_In_ HANDLE hThread, //线程句柄
_In_ DWORD dwPriority //线程优先级宏
);
优先级类型 | 级别 | 宏 |
---|---|---|
Time-critical | 实时 | THREAD_PRIORITY_TIME_CRITICAL |
highest | 高 | THREAD_PRIORITY_HIGHEST |
above normal | 高于标准 | THREAD_PRIORITY_ABOVE_NORMAL |
normal | 标准 | THREAD_PRIORITY_NORMAL |
below normal | 低于标准 | THREAD_PRIORITY_BELOW_NORMAL |
lowest | 低 | THREAD_PRIORITY_LOWEST |
idle | 空闲 | THREAD_PRIORITY_IDLE |