线程是进程中的一个执行单元(每个进程必须有一个主线程),一个进程可以有多个线程,一个线程只存在于一个进程中。在数据关系上,进程和线程是一对多的关系。线程不拥有系统资源,线程使用的所有资源都是由进程向系统申请的,线程拥有CPU时间片。在单处理器(或单核处理器)上,同一个进程中的不同线程交替获取CPU的时间片。在多处理器(或多核处理器)上,不同的线程可以同时运行在不同的CPU上,这样可以提高程序运行的效率。此外,还有一些方面必须使用多线程。比如扫描磁盘,同时在程序界面上同步显示当前扫描位置,就必须使用多线程。因为程序界面上的显示和磁盘的扫描工作在同一个线程中,不断的重新显示界面,会导致软件出现卡顿的现象。在这种情况下,可以通过分成两个线程来解决问题。界面的显示由主线程完成,扫描磁盘的工作由另一个线程完成。两个线程协同工作,这样就可以实现当前扫描的实时显示。状态效果。Firstunderstandthecreationofthreads.线程的创建使用CreateThread()函数,该函数的原型如下:HANDLECreateThread(LPSECURITY_ATTRIBUTESlpThreadAttributes,//SDDWORDdwStackSize,//initialstacksizeLPTHREAD_START_ROUTINElpStartAddress,//threadfunctionLPVOIDlpParameter,//threadargumentDWORDdwCreationFlags,//creationoptionLPDWORDlpThreadId//threadidentifier);lpThreadAttributes:表示创建的线程的安全属性,是指向SECURITY_ATTRIBUTES结构的指针,该参数一般设置为NULL。dwStackSize:指定线程使用默认的栈大小,如果为NULL,则与进程的主线程栈相同。lpStartAddress:指定线程函数,线程从函数入口开始运行,当函数返回时,表示线程终止,该函数属于回调函数。线程函数的定义形式如下:DWORDWINAPIThreadProc(LPVOIDlpParameter//threaddata);线程函数的返回值为DWORD类型,线程函数只有一个参数,在CreateThread()函数中给出。这个函数的函数名可以任意给定。在很多情况下,并不能保证线程会在CreateThread()函数执行后立即启动。线程的启动需要等待CPU的调度。只有当CPU给线程一个时间片时,线程才会执行。当然,这个时间短得可以忽略不计。.lpParameter:该参数表示传递给线程函数的参数,可以是指向任何数据类型的指针。这里是一个指针,可以很方便的通过结构体等方式一次给线程函数传递多个参数。dwCreationFlags:该参数表示线程创建后的线程状态。线程创建后,可以立即执行线程(这里的立即执行是指不会人为的让其进入等待状态),也可以挂起线程。如果需要立即执行,该参数设置为0;如果线程要处于挂起状态,则设置该参数为CREATE_SUSPENDED,当线程需要执行时,调用ResumeThread()函数将线程状态调整为等待运行状态,以及然后CPU在分配一个时间片后执行。lpThreadId:该参数用于返回新创建线程的线程ID。如果线程创建成功,函数返回线程句柄,否则返回NULL。创建新线程后,线程开始执行。但如果在dwCreationFlags中使用了CREATE_SUSPENDED参数,则线程不会立即执行,而是先挂起,直到调用ResumeThread后线程才会启动。线程的句柄需要通过CloseHandle()关闭才能释放资源。写一个简单的多线程例子,代码如下:#include#includeDWORDWINAPIThreadProc(LPVOIDlpParam){printf("ThreadProc\r\n");return0;}intmain(){HANDLEhThread=CreateThread(NULL,0,ThreadProc,NULL,0,NULL);printf("main\r\n");CloseHandle(hThread);return0;}代码在主线程打印一行“main”,在新建的线程中会打印一行“ThreadProc”。编译运行,查看运行结果,如图1所示。图1多线程程序的输出结果从图1可以看出程序的输出与预期的结果不一样。程序的问题在哪里?每个线程都有自己的CPU时间片。当主线程创建新线程时,主线程的CPU时间片还没有结束,会继续往下执行。由于主线程的代码很小,主线程在CPU分配的时间片内执行和退出。由于主线程结束意味着进程结束并退出。所以代码中创建的线程虽然创建了,但是根本没有机会执行。那么在这么短的代码中,如何保证新创建的线程能够在主线程结束前执行完呢?也就是说,主线程的操作需要等待新线程完成后才能执行。这里需要用到WaitForSingleObject()函数。该函数的原型如下:DWORDWaitForSingleObject(HANDLEhHandle,//handletoobjectDWORDdwMilliseconds//time-outinterval);参数说明如下。hHandle:这个参数是要等待的对象的句柄。dwMilliseconds:此参数指定等待超时的毫秒数。如果设置为0,它将立即返回。如果设置为INFINITE,表示一直在等待线程函数的返回。INFINITE是系统定义的宏,其定义如下。#defineINFINITE0xFFFFFFFF如果函数失败,将返回WAIT_FAILED;如果等待对象程序被激活,则返回WAIT_OBJECT_0;如果在等待对象激活之前等待时间结束,它将返回WAIT_TIMEOUT。修改以上代码,在CreateThread()函数后添加如下代码:WaitForSingleObject(hThread,INFINITE);添加WaitForSingleObject()函数后,主线程会等待新创建的线程结束,然后再继续执行主线程的后续代码。控制台的输出如图2所示。图2主线程等待子线程的执行WaitForSingleObject()只能等待一个线程,但是在程序中往往需要创建多个线程来执行,所以如果需要等待几个线程的完成状态,WaitForSingleObject()函数就无能为力了。不过除了WaitForSingleObject()函数之外,系统还提供了另一个函数WaitForMultipleObjects(),可以等待多个线程的完成状态。该函数的定义如下:handlearrayBOOLfWaitAll,//waitoptionDWORDdwMilliseconds//time-outinterval);该函数的参数比WaitForSingleObject()函数多了2个参数,下面分别介绍这些参数。nCount:此参数用于指示您希望函数等待的线程数。该参数的取值范围在1到MAXIMUM_WAIT_OBJECTS之间。lpHandles:该参数是一个数组指针,指向等待线程的句柄。fWaitAll:该参数表示是否等待所有线程完成的状态,如果设置为TRUE,则等待所有。dwMilliseconds:该参数与WaitForSingleObject()函数中的dwMilliseconds相同。WaitForSingleObject()和WaitForMultipleObjects()这两个函数不仅可以等待线程,还可以等待用于多线程同步互斥的内核对象。在使用多线程时往往需要考虑和注意的问题很多。比如多个线程同时操作一个共享资源,线程需要按照一定的顺序执行。看一个简单的多线程例子:intg_Num_One=0;DWORDWINAPIThreadProc(LPVOIDlpParam){intnTmp=0;for(inti=0;i<10;i++){nTmp=g_Num_One;nTmp++;//Sleep(1)是让OutofCPU//让其他线程被调度运行Sleep(1);g_Num_One=nTmp;}return0;}每个线程都有一个CPU时间片,当自己的时间片完成后,CPU会停止该线程运行,并切换到另一个线程运行。当多个线程同时对一个共享资源进行操作时,这样的切换会造成无形的问题。这里的代码比较短,肯定会在一个CPU时间片内完成,无法体现线程切换带来的错误。为了实现线程切换导致的错误,在代码中加入了Sleep(1),让线程主动让出CPU,让CPU进行线程切换。代码中,线程处理的共享资源是全局变量g_Num_One变量。创建线程的main函数代码如下:intmain(){HANDLEhThread[10]={0};inti;for(i=0;i<10;i++){hThread[i]=CreateThread(NULL,0,ThreadProc,NULL,0,NULL);}WaitForMultipleObjects(10,hThread,TRUE,INFINITE);for(i=0;i<10;i++){CloseHandle(hThread[i]);}printf("g_Num_One=%d\r\n",g_Num_One);return0;}在main函数中,通过CreateThread()创建了10个线程,每个线程将g_Num_One递增10次,每次递增1。然后10个线程将结果g_Num_One变成100编译运行上面的代码,查看输出结果,如图3所示。图3.多线程操作共享资源的错误结果这个结果和预测的结果不一样。为什么会有这样的差异?下面进行模拟分析。为了分析方便,线程数减少为两个线程,即A线程和B线程。①g_Num_One初始值为0。②当线程A执行nTmp=g_Num_One和nTmp++时(此时nTmp值为1),由于Sleep(1)发生线程切换,g_Num_One初始值为此时还是0。③当nTmp=g_Num_One和nTmp++在线程B中执行时(此时nTmp的值也为1),由于Sleep(1)再次发生线程切换。④线程A执行g_Num_One=nTmp,此时g_Num_One的值为1,然后在下一个循环执行nTmp=g_Num_One和nTmp++的操作,再次切换。⑤线程B执行g_Num_One=nTmp,此时g_Num_One的值为1。到了第5步,不用继续分析了,已经可以看出原因了。g_Num_One的值是nTmp最后一次赋值后的值(线程中的局部变量在线程内是私有的,虽然是同一个线程函数,但是nTmp在每个线程内都是私有的)。为了解决这个问题,这里使用了临界区。临界区对象是一个CRITICAL_SECTION数据结构,Windows操作系统就是利用这个数据结构来保护关键代码,以保证多线程下的共享资源。Windows一次只允许一个线程进入临界区。临界区有4个函数,分别是初始化临界区对象(InitializeCriticalSection())、进入临界区(EnterCriticalSection())、离开临界区(LeaveCriticalSection())和删除临界区对象(DeleteCriticalSection())).临界区很好地保护了共享资源,现实生活中类似临界区的例子还有很多。例如体检时,一个体检室只有一个体检医师,体检医师会叫病人进来体检。这个时候,其他人是不能进去的。当病人离开时,下一个病人可以进入。在这里,体检医师是一个共享资源,每次体检的患者都是多个不同的线程。关键部分以类似的方式保护共享资源免受损坏。下面依次来看一下这四个函数关于临界区的函数的定义,分别如下:VOIDInitializeCriticalSection(LPCRITICAL_SECTIONlpCriticalSection//criticalsection);VOIDEnterCriticalSection(LPCRITICAL_SECTIONlpCriticalSection//criticalsection);VOIDLeaveCriticalSection(LPCRITICAL_SECTIONlpCriticalSection//criticalsection);VOIDDeleteCriticalSection(LPCRITICAL_SECTIONlpCriticalSection//criticalsection);TheparametersofthesefourAPIfunctionsareallpointerstotheCRITICAL_SECTIONstructure.修改上面有问题的代码,修改后的代码如下:#include#includeintg_Num_One=0;CRITICAL_SECTIONg_cs;DWORDWINAPIThreadProc(LPVOIDlpParam){intnTmp=0;for(inti=0;i<10;i++){//进入临界区EnterCriticalSection(&g_cs);nTmp=g_Num_One;nTmp++;Sleep(1);g_Num_One=nTmp;//离开临界区LeaveCriticalSection(&g_cs);}return0;}intmain(){InitializeCriticalSection(&g_cs);HANDLEhThread[10]={0};inti;for(i=0;i<10;i++){hThread[i]=CreateThread(NULL,0,ThreadProc,NULL,0,NULL);}WaitForMultipleObjects(10,hThread,TRUE,INFINITE);printf("g_Num_One=%d\r\n",g_Num_One);for(i=0;i<10;i++){CloseHandle(hThread[i]);}DeleteCriticalSection(&g_cs);return0;}编译运行上面的代码,输出的结果是想要的正确结果,即g_Num_One的值为100。除了使用临界区外,线程同步和相互还有其他方法排斥,这里不做介绍。在开发多线程程序时,要注意多线程的同步和互斥。临界区对象只能用于多线程互斥。