3.1多线程

多线程

CreateProcess 函数创建了进程,同时也创建了进程的主线程。这也就是说系统中的每个进程都至少有一个线程,这个线程从入口地址 main 处开始执行,直到 return 语句返回,主线程结束,该进程也就从内存中卸载了。

主线程在运行过程中还可以创建新的线程,既所谓的多线程。在同一进程中运行不同的线程的好处是这些线程可以共享进程的资源,如全局变量、句柄等。当然各个线程也可以有自己的私有堆栈用于保存私有数据。本节将具体介绍线程的创建和线程内核对象对程序的影响。

线程的创建

线程描述了进程内代码的执行路径。进程中同时可以有多个线程在执行,为了使它们能够“同时”运行,操作系统为每个线程轮流分配 CPU 时间片。为了充分地利用 CPU,提高软件产品的性能,一般情况下,应用程序使用主线程接受用户的输入,显示运行结果,而创建新的线程(称为辅助线程)来处理长时间的操作,比如读写文件、访问网络等。这样,即便是在程序忙于繁重的工作时也可以由专门的线程响应用户命令。

每个线程必须拥有一个进入点函数,线程从这个进入点开始运行。主线程的进入点是函数main,如果想在进程中创建一个辅助线程,则必须为该辅助线程指定一个进入点函数,这个函数称为线程函数。线程函数的定义如下。

DWORD WINAPI ThreadProc(LPVOID lpParam); //线程函数名称ThreadProc 可以是任意的

WINAPI 是一个宏名,在 windef.h 文件中有如下的声明。

#define WINAPI __stdcall;

__stdcall 是新标准 C/C++函数的调用方法。从底层上说,使用这种调用方法参数的进栈顺序和标准 C 调用(_cdecl 方法)是一样的,都是从右到左,但是__stdcall 采用自动清栈的方式,而_cdecl 采用的是手工清栈方式。Windows 规定,凡是由它来负责调用的函数都必须定义为__stdcall 类型。ThreadProc 是一个回调函数,即由 Windows 系统来负责调用的函数,所以此函数应定义为__stdcall 类型。注意,如果没有显式说明的话,函数的调用方法是_cdecl。

可以看到这个函数有一个参数 lpParam,它的值是由下面要讲述的 CreateTread 函数的第四个参数 lpParameter 指定的。

创建新线程的函数是 CreateThread,由这个函数创建的线程将在调用者的虚拟地址空间内执行。函数的用法如下。

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

此函数执行成功后,将返回新建线程的线程句柄。lpStartAddress 参数指定了线程函数的地址,新建线程将从此地址开始执行,直到 return 语句返回,线程运行结束,把控制权交给操作系统。

下面是一个简单的例子(03ThreadDemo 工程下)。在这个的例子中,主线程首先创建了一个辅助线程,打印出辅助线程的 ID 号,然后等待辅助线程运行结束;辅助线程仅打印出几行字符串,以模拟真正的工作。程序代码如下。
ThreadDemo.cpp

#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;
}

程序执行后,CreateThread 函数会创建一个新的线程,此线程的入口地址为 ThreadProc。
在这里插入图片描述
上面的例子使用 CreateThread 函数创建了一个新线程。

CreateThread(NULL, NULL, ThreadProc, NULL, 0, NULL);

创建新线程后 CreateThread 函数返回,新线程从 ThreadProc 函数的第一行执行。主线程继续运行,打印出新线程的一些信息后,调用 WaitForSingleObject 函数等待新线程运行结束。

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

WaitForSingleObject 函数用于等待指定的对象(hHandle)变成受信状态。 参数dwMilliseconds 给出了以毫秒为单位的要等待的时间,其值指定为 INFINITE 表示要等待无限长的时间。当有下列一种情况发生时函数就会返回:
(1)要等待的对象变成受信(signaled)状态。
(2)参数 dwMilliseconds 指定的时间已过去。
一个可执行对象有两种状态,未受信(nonsignaled)和受信(signaled)状态。线程对象只有当线程运行结束时才达到受信状态,此时“WaitForSingleObject(hThread, INFINITE)”语句才会返回。

CreateThread 函数的 lpThreadAttributes 和 dwCreationFlags 参数的作用在本节的例子中没有体现出来,下面详细说明一下。

lpThreadAttributes——一个指向 SECURITY_ATTRIBUTES 结构的指针,如果需要默认的安全属性,传递 NULL 就行了。如果希望此线程对象句柄可以被子进程继承的话,必须设定一个 SECURITY_ATTRIBUTES 结构,将它的 bInheritHandle 成员初始化为 TRUE,如下面的代码所示。

SECURITY_ATTRIBUTES sa; 
sa.nLength = sizeof(sa); 
sa.lpSecurityDescriptor = NULL; 
sa.bInheritHandle = TRUE; //使 CreateThread 返回的句柄可以被继承
//句柄h 可以被子进程继承
HANDLE h = ::CreateThread (&sa, ......);

当创建新的线程时,如果传递 NULL 做为 lpThreadAttributes 参数的值,那么返回的句柄是不可继承的;如果定义一个 SECURITY_ATTRIBUTES 类型的变量 sa,并像上面一样初始化 sa 变量的各成员,最后传递 sa 变量的地址做为 lpThreadAttributes 参数的值,那么 CreateThread 函数返回的句柄就是可继承的。

这里的继承是相对于子进程来说的。当创建子进程时,如果为 CreateProcess 函数的 bInheritHandles 参数传递 TRUE,那么子进程就可以继承父进程的可继承句柄。

dwCreationFlags——创建标志。如果是 0,表示线程被创建后立即开始运行,如果指定为 CREATE_SUSPENDED 标志,表示线程被创建以后处于挂起(暂停)状态,直到使用 ResumeThread 函数(见下一小节)显式地启动线程为止。

线程内核对象

线程内核对象就是一个包含了线程状态信息的数据结构。每一次对 CreateThread 函数的成功调用,系统都会在内部为新的线程分配一个内核对象。系统提供的管理线程的函数其实就是依靠访问线程内核对象来实现管理的。下面列出了这个结构的基本成员:
![在这里插入图片描述](https://img-blog.csdnimg.cn/d156418bfd5f4b在这里插入图片描述
创建线程内核对象的时候,系统要对它的各个成员进行初始化,上表中每一项括号里面的值就是该成员的初始值。本节主要讨论内核对象各成员的作用,以及系统如何管理这些成员。

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

使用计数 Usage Count
Usage Count 成员记录了线程内核对象的使用计数,这个计数说明了此内核对象被打开的次数。线程内核对象的存在与 Usage Count 的值息息相关,当这个值是 0 的时候,系统就认为已经没有任何进程在引用此内核对象了,于是线程内核对象就要从内存中撤销。

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

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

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

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

HANDLE GetCurrentProcess (); //返回当前进程句柄
HANDLE GetCurrentThread (); //返回当前线程句柄

前面提到,新创建的线程在初始状态下 Usage Count 的值是 2。此时如果立即调用CloseHandle 函数来关闭 CreateThread 返回的句柄的话,Usage Count 的值将减为 1,但新创建的线程是不会被终止的。

在上一小节那个简单的例子中,Usage Count 值的变化情况是这样的:调用 CreateThread函数后,系统创建一个新的线程,返回其句柄,并将 Usage Count 的值初始化为 2。线程函数一旦返回,线程的生命周期也就到此为止了,系统会使 Usage Count 的值由 2 减为 1。接下来调用 CloseHandle 函数又会使 Usage Count 减 1。这个时候系统检查到 Usage Count 的值已经为0,就会撤销此内核对象,释放它占用的内存。如果不关闭句柄的话,Usage Count 的值将永远不会是 0,系统将永远不会撤销它占用的内存,这就会造成内存泄漏(当然,线程所在的进程结束后,该进程占用的所有资源都要释放)。

暂停次数 Suspend Count
线程内核对象中的 Suspend Count 用于指明线程的暂停计数。当调用 CreateProcess(创建进程的主线程)或 CreateThread 函数时,线程的内核对象就被创建了,其暂停计数被初始化为1(即处于暂停状态),这可以阻止新创建的线程被调度到 CPU 中。因为线程的初始化需要时间 , 当 线 程 完 全 初 始 化 好 了 之 后 , CreateProcess 或 CreateThread 检 查 是 否 传 递 了CREATE_SUSPENDED 标志。如果传递了这个标志,那么这些函数就返回,同时新线程处于暂停状态。如果尚未传递该标志,那么线程的暂停计数将被递减为 0。当线程的暂停计数是 0的时候,该线程就处于可调度状态。

创建线程的时候指定 CREATE_SUSPENDED 标志,就可以在线程有机会在执行任何代码之前改变线程的运行环境(如下面讨论的优先级等)。一旦达到了目的,必须使线程处于可调度状态。进行这项操作,可以使用 ResumeThread 函数。

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

该函数减少线程的暂停计数,当计数值减到 0 的时候,线程被恢复运行。如果调用成功,ResumeThread 函数返回线程的前一个暂停计数,否则返回 0xFFFFFFFF(-1)。

单个线程可以被暂停若干次。如果一个线程被暂停了 3 次,它必须被唤醒 3 次才可以分配给一个 CPU。暂停一个线程的运行可以用 SuspendThread 函数。

DWORD SuspendThread (HANDLE hThread); //挂起一个线程

任何线程都可以调用该函数来暂停另一个线程的运行。和 ResumeThread 相反,SuspendThread 函数会增加线程的暂停计数。

大约每经20ms,Windows 查看一次当前存在的所有线程内核对象。在这些对象中,只有一少部分是可调度的(没有处于暂停状态),Windows 选择其中的一个内核对象,将它的CONTEXT(上下文)装入 CPU 的寄存器,这一过程称为上下文转换。但是这样做的前提是,所有的线程具有相同的优先级。在现实环境中,线程被赋予许多不同的优先级,这会影响到调度程序将哪个线程取出来做为下一个要运行的线程。

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

……
DWORD dwExitCode; 
if(::GetExitCodeThread(hThread, &dwExitCode)) 
{
    
     
   if(dwExitCode == STILL_ACTIVE) 
   {
    
     // 目标线程还在运行 } 
   else 
   {
    
     // 目标线程已经中止,退出代码为dwExitCode } 
} 
……

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

线程的终止

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

线程结束后的退出代码可以被其他线程用 GetExitCodeThread 函数检测到,所以可以当做自定义的返回值来表示线程的执行结果。终止线程的执行有 4 种方法。
(1)线程函数自然退出。当函数执行到 return 语句返回时,Windows 将终止线程的执行。建议使用这种方法终止线程的执行。
(2)使用 ExitThread 函数来终止线程,原型如下。

void ExitThread( DWORD dwExitCode); //线程的退出代码

ExitThread 函数会中止当前线程的运行,促使系统释放掉所有此线程使用的资源。但是,C/C++资源却不能得到正确地清除。例如,在下面一段代码中,theObject 对象的析构函数就不会被调用。
StdAfx.h

// stdafx.h : include file for standard system include files,
//  or project specific include files that are used frequently, but
//      are changed infrequently
//

#if !defined(AFX_STDAFX_H__9C88BEEE_5C7F_4140_B411_3B326A3986F1__INCLUDED_)
#define AFX_STDAFX_H__9C88BEEE_5C7F_4140_B411_3B326A3986F1__INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

#define WIN32_LEAN_AND_MEAN		// Exclude rarely-used stuff from Windows headers

#include <stdio.h>

// TODO: reference additional headers your program requires here

//{
    
    {AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.

#endif // !defined(AFX_STDAFX_H__9C88BEEE_5C7F_4140_B411_3B326A3986F1__INCLUDED_)

StdAfx.cpp

// stdafx.cpp : source file that includes just the standard includes
//	02ProcessList.pch will be the pre-compiled header
//	stdafx.obj will contain the pre-compiled type information

#include "stdafx.h"

// TODO: reference any additional headers you need in STDAFX.H
// and not in this file

02ExitThread.cpp

#include "stdafx.h"
#include <windows.h>
class CMyClass
{
    
    
public:
	CMyClass() {
    
     printf(" Constructor\n"); }
	~CMyClass() {
    
     printf(" Destructor\n"); }
};

void main()
{
    
    
	CMyClass theObject;
	::ExitThread(0); //ExitThread 函数使线程立刻中止,theObject 对象的析构函数得不到机会被调用

	// 在这个函数的结尾,编译器会自动添加一些必要的代码,
	// 来调用theObject的析构函数
}

运行上面的代码,将会看到程序的输出。
在这里插入图片描述一个对象被创建,但是永远也看不到 Destructor 这个单词出现。theObject 这个 C++对象没有被正确地销毁,原因是 ExitThread 函数强制该线程立刻终止,C/C++运行期没有机会执行清除代码。

所以结束线程最好的方法是让线程函数自然返回。如果在上面的代码中删除了对ExitThread 的调用,再次运行程序产生的输出结果如下。
在这里插入图片描述
(3)使用 TerminateThread 函数在一个线程中强制终止另一个线程的执行,原型如下。

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

这是一个被强烈建议避免使用的函数,因为一旦执行这个函数,程序无法预测目标线程会在何处被终止,其结果就是目标线程可能根本没有机会来做清除工作,如线程中打开的文件和申请的内存都不会被释放。另外,使用 TerminateThread 函数终止线程的时候,系统不会释放线程使用的堆栈。所以建议读者在编程的时候尽量让线程自己退出,如果主线程要求某个线程结束,可以通过各种方法通知线程,线程收到通知后自行退出。只有在迫不得已的情况下,才使用 TerminateThread 函数终止线程。

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

总之,始终应该让线程正常退出,即由它的线程函数返回。通知线程退出的方法很多,如使用事件对象、设置全局变量等,这是下一节的话题。

线程的优先级

每个线程都要被赋予一个优先级号,取值为 0(最低)到 31(最高)。当系统确定哪个线程需要分配 CPU 时,它先检查优先级为 31 的线程,然后以循环的方式对他们进行调度。如果有一个优先级为 31 的线程可调度,它就会被分配到一个 CPU 上运行。在该线程的时间片结束时,系统查看是否还有另一个优先级为 31 的线程,如果有,就安排这个线程到 CPU 上运行。

Windows 调度线程的原则就是这样的,只要优先级为 31 的线程是可调度的,就绝对不会将优先级为 0~30 的线程分配给 CPU。大家可能以为,在这样的系统中,低优先级的线程永远得不到机会运行。事实上,在任何一段时间内,系统中的线程大多是不可调度的,即处于暂停状态。比如 3.1.1 小节的例子中,调用 WaitForSingleObject 函数就会导致主线程处于不可调度状态,还有在第 4 章要讨论的 GetMessage 函数,也会使线程暂停运行。

Windows 支持 6 个优先级类:idle、below normal、normal、above normal、high 和 real-time。从字面上也可以看出,normal 是被绝大多数应用程序采用的优先级类。其实,进程也是有优先级的,只是在实际的开发过程中很少使用而已。进程属于一个优先级类,还可以为进程中的线程赋予一个相对线程优先级。但是,一般情况下并不改变进程的优先级(默认是 nomal),所以可以认为,线程的相对优先级就是它的真实优先级,与其所在的进程的优先级类无关。

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

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 Normal(正常)
● THREAD_PRIORITY_BELOW_NORMAL Below normal(低于正常,Windows 98 不支持)
● THREAD_PRIORITY_LOWEST Lowest(最低)
● THREAD_PRIORITY_IDLE Idle(空闲)

下面的小例子说明了优先级的不同给线程带来的影响。它同时创建了两个线程,一个线程的优先级是“空闲”,运行的时候不断打印出“Idle Thread is running”;另一个线程的优先级为“正常”,运行的时候不断打印出“Normal Thread is running”字符串。源程序代码如下。
PriorityDemo.cpp

#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;
}

/*
	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;
	}
*/

可以看到,只要有优先级高的线程处于可调度状态,Windows是不允许优先级相对低的线程占用 CPU 的。
在这里插入图片描述
创建第一个线程时,将 CREATE_SUSPENDED 标记传给了 CreateThread 函数,这可以使新线程处于暂停状态。在将它的优先级设为 THREAD_PRIORITY_IDLE 后 , 再 调 用ResumeThread 函数恢复线程运行。这种改变线程优先级的方法在实际编程过程中经常用到。

WaitForMultipleObjects 函数用于等待多个内核对象,前两个参数分别为要等待的内核对象的个数和句柄数组指针。如果将第三个参数 bWaitAll 的值设为 TRUE,等待的内核对象全部变成受信状态以后此函数才返回。否则,bWaitAll 为 0 的话,只要等待的内核对象中有一个变成了受信状态,WaitForMultipleObjects 就返回,返回值指明了是哪一个内核对象变成了受信状态。下面的代码说明了函数返回值的作用。

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;
}

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

有的时候使用高优先级的线程是非常必要的。比如,Windows Explorer 进程中的线程就是在高优先级下运行的。大部分时间里,Explorer 的线程都处于暂停状态,等待接受用户的输入。当 Explorer 的线程被挂起的时候,系统不给它们安排 CPU 时间片,使其他低优先级的线程占用 CPU。但是,一旦用户按下一个键或组合键,例如 Ctrl+Esc,系统就唤醒 Explorer 的线程(用户按 Ctrl+Esc 时,开始菜单将出现)。如果该时刻有其他优先级低的线程正在运行的话,系统会立刻挂起这些线程,允许 Explorer 的线程运行。这就是抢占式优先操作系统。

C/C++运行期库

在实际的开发过程中,一般不直接使用 Windows 系统提供的 CreateThread 函数创建线程,而是使用 C/C++运行期函数_beginthreadex。本小节主要来分析一下_beginthreadex 函数的内部实现。

事实上,C/C++运行期库提供另一个版本的 CreateThread 是为了多线程同步的需要。在标准运行库里面有许多的全局变量,如 errno、strerror 等,它们可以用来表示线程当前的状态。但是在多线程程序设计中,每个线程必须有惟一的状态,否则这些变量记录的信息就不会准确了。比如,全局变量 errno 用于表示调用运行期函数失败后的错误代码。如果所有线程共享一个 errno 的话,在一个线程产生的错误代码就会影响到另一个线程。为了解决这个问题,每个线程都需要有自己的 errno 变量。

要想使运行期为每个线程都设置状态变量,必须在创建线程的时候调用运行期提供的_beginthreadex,让运行期设置了相关变量后再去调用 Windows 系统提供的 CreateThread 函数。_beginthreadex 的参数与 CreateThread 函数是对应的,只是参数名和类型不完全相同,使用的时候需要强制转化。

unsigned long _beginthreadex( 
  void *security, 
  unsigned stack_size, 
  unsigned ( __stdcall *start_address )( void * ), 
  void *arglist, 
  unsigned initflag, 
  unsigned *thrdaddr 
);

VC++默认的 C/C++运行期库并不支持_beginthreadex 函数。这是因为标准 C 运行期库是在 1970 年左右问世的,那个时候还没有多线程这个概念,也就没有考虑到将 C 运行期库用于多线程应用程序所出现的问题。要想使用_beginthreadex 函数,必须对 VC 进行设置,更换他默认使用的运行期库。

选择菜单命令“Project/Settings…”,打开标题为“Project Settings”的对话框,如图所示。选中 C/C++选项卡,在 Category 对应的组合框中选择 Code Generation 类别。从 Use run-time library 组合框中选定 6 个选项中的一个。默认的选择是第一个,即 Single-Threaded,此选项对应着单线程应用程序的静态链接库。为了使用多线程,选中 Multithreaded DLL 就可以了。后两节的例子就使用_beginthreadex 函数来创建线程。
在这里插入图片描述
相应地,C/C++运行期库也提供了另一个版本的结束当前线程运行的函数,用于取代ExitThread 函数。

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

这个函数会释放_beginthreadex 为保持线程同步而申请的内存空间,然后再调用 ExitThread函数来终止线程。同样,还是建议让线程自然退出,而不要使用_endthreadex 函数。

おすすめ

転載: blog.csdn.net/qq_36314864/article/details/121263924