-
多线程
CreateProcess函数创建了进程,同时也创建了进程的主线程
主线程在运行过程中可以创建新的线程,在同一进程中运行不同线程的好处是可以共享进程的资源,如全局变量、句柄等。
当然各个线程也可以有自己的私有堆栈用于保存私有数据。 -
线程的创建
主线程的进入点是函数main
辅助线程的进入点函数是线程函数ThreadProc -
线程函数ThreadProc
DWORD WINAPI ThreadProc(LPVOID lpParam); //线程函数名称ThreadProc可以是任意的
windef.h头下定义
#define WINAPI __stdcall;
__stdcall #从右到左传参,自动清栈
__cdecl #从右到左传参,自动清栈
windows如何没有显示声明函数,则为__cdecl传参
- CreateThread
用来创建新线程
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程的安全属性
DWORD dwStackSize, //指定线程堆栈的大小
LPTHREAD_START_ROUTINE lpStartAddress, //线程函数的起始地址
LPVOID lpParaneter, //传递给线程函数的参数
DWORD dwCreationFlags, //指定创建线程后是否立即启动
DWORD* lpThreadId //用于取得内核给新生成的线程分配的线程ID号
);
// 03ThreadDemo.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include "windows.h"
#include "stdio.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;
}
- WaitForSingleObject
用于等待指定的对象(hHandle)变成受信状态
WaitForSingleObject(
hThread, //hHandle 要等待的对象的句柄
INFINITE, //dwMilliseconds 要等待的时间(毫秒为单位)
)
函数返回:
1. 要等待的对象变成受信(signaled)状态
2. 参数dwMilliseconds指定的时间已过去
- 可执行对象的两种状态
一种是受信状态(signaled)和未受信(nonsignaled)状态。线程对象只有当线程运行结束时才达到受信状态。
+线程内核对象
线程内核对象是一个包含了线程状态信息的数据结构。该结构信息如下所示。
1. 线程上下文CONTEXT
每个线程都有它自己的一组CPU寄存器,称为线程的上下文,该组寄存器的值保存在一个CONTEXT结构里,反映了该线程上次运行时CPU寄存器的状态。
2. 使用计数Usage Count
线程内核对象的存在与Usage Count的值息息相关,当这个值为0时,系统认为已没有任何进程引用次内核对象了,于是线程就要从内存中撤销。
创建一个新的线程后,初始状态下Usage Count=2,打开此内核对象Usage Count会加1,比如使用OpenThread函数打开线程时Usage Count就会加1,
但使用后一定要使用CloseHandle函数进行关闭,关闭会导致Usage Count减1,如果不关闭将导致内存泄漏
HANDLE GetCurrentProcess(); //返回当前进程句柄
HANDLE GetCurrentThread(); //返回当前线程句柄
返回内核对象时,不会到导致Usage Count增减变化。
3. 暂停次数Suspend Count
线程内核对象中的Suspend Count用于指明线程的暂停计数。当调用CreateProcess或CreateThread函数时,线程内核对象被创建,其暂停计数被初始化为1(即处于暂停状态)。
当CreateProcess或CreateThread传递CREATE_SUSPEND标志时,函数就返回,新线程处于暂停状态,如果尚未传递,暂停次数将被递减为0,此时线程处于可调度状态。
DWORD ResumeThread(HANDLE hThread); //唤起一个挂起的线程
DWORD SuspendThread(HANDLE hThread); //挂起一线程
4. 退出代码(Exit Code)
在线程运行期间,线程函数还没有返回,Exit Code=STILL_ACTIVE,线程运行结束后,系统自动将Exit Code设为线程得到返回值
DWORD GetExitCodeThread(hThread,&dwExitCode)得到线程的退出代码
5. 是否受信(signaled)
线程运行期间,Signaled的值永远为FALSE,即未受信。当线程结束后,Signaled=TRUE ,此时该对象的等待函数就会返回,如WaitForSingleObject函数
- 线程的终止
线程终止时,会发生下列事件:
1. 在线程函数中创建的所有C++对象将通过它们各自的析构函数被正确地销毁
2. 该线程使用的堆栈被释放
3. 系统将线程内核对象中的Exit Code的值由STILL_ACTIVE设置为线程函数的返回值
4. 系统将递减线程内核对象中的Usage Count的值
- 终止线程的执行
1.线程函数自然退出(最佳推荐)
2.使用ExitThread函数来终止,但c/c++资源却不能得到正确的清除
void ExitThread(DWORD dwExitCode//线程的退出代码
);
3.使用TerminateThread函数,会导致线程使用的堆栈不会被释放,强烈建议不使用
BOOL TerminateThread(
HANDLE hThread, //目标线程句柄
DWORD dwExitCode //目标线程的退出代码
);
4.ExitProcess,相当于对进程中的每个线程使用TerminateThread函数
- 线程的优先级
0(最低)-31(最高),windows系统支持6个优先级类:idle、below normal、normal、above normal、high、real-time
BOOL SetThreadPriority(HANDLE hThread,int nPriority);
nPriority参数
THREAD_PRIORITY_TIME_CRITICAL //实时
THREAD_PRIORITY_HIGHEST //最高
THREAD_PRIORITY_ABOVE_NORMAL //高于正常
THREAD_PRIORITY_NORMAL //正常
THREAD_PRIORITY_BLEOW_NORMAL //低于正常
THREAD_PRIORITY_LOWEST //最低
THREAD_PRIORITY_IDLE //空闲
- 示例:不同优先级的线程
// 03PriorityDemo.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include "windows.h"
#include "stdio.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);
h[1] = CreateThread(NULL,0,ThreadNormal,NULL,CREATE_SUSPENDED,&dwThreadID);
SetThreadPriority(h[1],THREAD_PRIORITY_NORMAL);
ResumeThread(h[1]);
//等待两个线程内核对象都变成受信状态
WaitForMultipleObjects(
2, //DWORD nCount 要等待的内核对象的数量
h, //CONST HANDLE *lpHandles 句柄数组
TRUE, //BOOL bWaitAll 指定是否等待所有内核对象变成受信状态
INFINITE //DwORD dwMilliseconds 要等待的时间
);
CloseHandle(h[0]);
CloseHandle(h[1]);
return 0;
}
- c/c++版CreateThread函数——__beginthreadex
使用时需要强制转化(HANDLE)
unsigned long __beginthreadex(
void *security,
unsigned stack_size,
unsigned (__stdcall* start_address)(void *),
void * arglist,
unsigned initflag,
unsigned *thrdaddr
);
- 线程同步
同步可以保证在一个时间内只有一个线程对某个共享资源有控制权。
// 03CountError.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include "windows.h"
#include "stdio.h"
#include "process.h"
int g_nCount1 = 0;
int g_nCount2 = 0;
BOOL g_bContinue = TRUE;
UINT __stdcall ThreadFunc(LPVOID);
int main(int argc, char* argv[])
{
UINT uId;
HANDLE h[2];
h[0] = (HANDLE)_beginthreadex(NULL,0,ThreadFunc,NULL,0,&uId);
h[1] = (HANDLE)_beginthreadex(NULL,0,ThreadFunc,NULL,0,&uId);
//等待1秒后通知两个计数线程结束,关闭句柄
Sleep(1000);
g_bContinue = FALSE;
WaitForMultipleObjects(2,h,TRUE,INFINITE);
CloseHandle(h[0]);
CloseHandle(h[1]);
printf("g_nCount1=%d\n",g_nCount1);
printf("g_nCount2=%d\n",g_nCount2);
return 0;
}
UINT __stdcall ThreadFunc(LPVOID)
{
while(g_bContinue)
{
g_nCount1++;
g_nCount2++;
}
return 0;
}
- 临界区对象
临界区对象是定义在数据段中的一个CRITICAL_SECTION结构,windows内部使用这个记录一些信息,确保在同一个时间内只有一个线程访问该数据段中的数据。临界区对象能够很好地保护共享资源,但是它不能够用于进程之间资源的锁定,因为它不是内核对象。
把临界区对象定义在想保护的数据段中,在任何线程使用此临界区对象之前对它进行初始化。
void InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection //指向数据段中定义的CRITICAL_SECTION结构
);
线程访问临界区中的时候,必须首先调用EnterCriticalSection函数,申请进入临界区,同一时间内,windows只允许一个线程进入临界区
void EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
线程操作数据完成时,将临界区交还给windows,使用LeaveCriticalSection函数完成
void LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
当程序不再使用临界区对象时,必须删除它
void DeleteCriticalSection(
LPCRTITICAL_SECTION lpCriticalSection
);
- 示例:使用临界区对象
// 03CriticalSection.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <process.h>
#include "windows.h"
#include "stdio.h"
BOOL g_bContinue = TRUE;
int g_nCount1=0;
int g_nCount2=0;
CRITICAL_SECTION g_cs; //对存在同步问题的代码段使用临界区对象
UINT __stdcall ThreadFunc(LPVOID);
int main(int argc, char* argv[])
{
UINT uId;
HANDLE h[2];
//初始化临界区对象
InitializeCriticalSection(&g_cs);
h[0] = (HANDLE)_beginthreadex(NULL,0,ThreadFunc,NULL,0,&uId);
h[1] = (HANDLE)_beginthreadex(NULL,0,ThreadFunc,NULL,0,&uId);
Sleep(1000);
g_bContinue = FALSE;
WaitForMultipleObjects(2,h,TRUE,INFINITE);
CloseHandle(h[0]);
CloseHandle(h[1]);
DeleteCriticalSection(&g_cs);
printf("g_nCount1 = %d\n",g_nCount1);
printf("g_nCount2 = %d\n",g_nCount2);
return 0;
}
UINT __stdcall ThreadFunc(LPVOID)
{
while(g_bContinue)
{
EnterCriticalSection(&g_cs);
g_nCount1++;
g_nCount2++;
LeaveCriticalSection(&g_cs);
}
return 0;
}
-
互锁函数
互锁函数为了同步访问多线程共享变量提供了一个简单的机制。如果变量存在共享内存,不同进程的线程也可以使用此机制。 -
InterlockedIncrement和InterlockedDecrement
InterlockedIncrement函数将递增(加1)指定的32位变量。这个函数可以阻止其他线程同时使用此变量。
InterlockedDecrement函数将递减(减1)指定的32位变量。
LONG InterlockedIncrement(
LONG volatile* Addend //指向要递增的变量
);
LONG InterlockedDecrement(
LONG volatile* Addend //指向要递减的变量
);
- 示例:使用互锁函数实现上面的线程同步功能
#include "stdafx.h"
#include <process.h>
#include "windows.h"
#include "stdio.h"
BOOL g_bContinue = TRUE;
int g_nCount1=0;
int g_nCount2=0;
UINT __stdcall ThreadFunc(LPVOID);
int main(int argc, char* argv[])
{
UINT uId;
HANDLE h[2];
h[0] = (HANDLE)_beginthreadex(NULL,0,ThreadFunc,NULL,0,&uId);
h[1] = (HANDLE)_beginthreadex(NULL,0,ThreadFunc,NULL,0,&uId);
Sleep(1000);
g_bContinue = FALSE;
WaitForMultipleObjects(2,h,TRUE,INFINITE);
CloseHandle(h[0]);
CloseHandle(h[1]);
printf("g_nCount1 = %d\n",g_nCount1);
printf("g_nCount2 = %d\n",g_nCount2);
return 0;
}
UINT __stdcall ThreadFunc(LPVOID)
{
while(g_bContinue)
{
InterlockedIncrement((long*)&g_nCount1);
InterlockedIncrement((long*)&g_nCount2);
}
return 0;
}
- 事件内核对象
多线程程序设计大多会涉及线程间相互通信,一种比较好的通信方法是使用事件内核对象。
事件对象(event)是一种抽象对象,具备未受信和受信2种状态,它的状态设置和测试工作由windows来完成。
事件对象
1.nUsageCount
当计数为0时,windows销毁此内核对象占用的资源
2.bManualReset
指定在一个事件内核对象上等待的函数返回之后,windows是否重置这个对象为未受信状态
3.bSignaled
指定当前事件内核对象是否受信
- 事件对象用法
1.CreateEvent
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, //用来定事件对象的安全属性
BOOL bManualReset, //指定是否需要手动重置对象为未受信状态
BOOL bInitialState, //指定事件对象创建时的初始状态
LPCWSTR lpName //事件对象的名称
);
参数bManualReset:
分为自动重置和人工重置,当一个人工重置的事件对象受信以后,所有等待在这个事件上的线程都会变为可调度状态。
当一个自动重置的事件对象受信以后,windows只允许一个等待在该事件上的线程变成可调度状态,然后就自动重置此事件对象为未受信状态
HANDLE OpenEvent(
DWORD dwResiredAccess, //指定想要的访问权限
BOOL bInheritHandle, //指定返回句柄是否可被继承
LPCWSTR lpName //要打开的事件对象的名称
);
打开或创建事件对象之后,使用CloseHandle释放它占用的资源
BOOL SetEvent(HANDLE hEvent); //将事件状态设为“受信(signaled)”
BOOL ResetEvent(HANDLE hEvent); //将事件状态设为"未受信(nonsignaled)"
特别地,对一个自动重置类型对象,当在这样的事件对象上等待的函数(比如WaitForSingleObject函数)返回时,
windows会自动重置事件对象为未受信状态。通常情况下,为一个自动重置类型的事件对象调用ResetEvent函数是不必要的,因为windows会自动重置此对象
- 示例:事件对象使用
// 03EventDemo.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include "windows.h"
#include "stdio.h"
#include <process.h>
HANDLE g_hEvent;
UINT __stdcall ChildFunc(LPVOID);
int main(int argc, char* argv[])
{
HANDLE hChildThread;
UINT uId;
//创建一个自动重置的,未受信的事件内核对象
g_hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
hChildThread = (HANDLE)_beginthreadex(NULL,0,ChildFunc,NULL,0,&uId);
//通知子线程开始工作
printf("Please input a char to tell the Child Thread to work:\n");
getchar();
SetEvent(g_hEvent);
//等待子线程完成工作,释放资源
WaitForSingleObject(hChildThread,INFINITE);
printf("All the work has benn finished.\n");
CloseHandle(hChildThread);
CloseHandle(g_hEvent);
return 0;
}
UINT __stdcall ChildFunc(LPVOID)
{
WaitForSingleObject(g_hEvent,INFINITE);
printf("Child thread is working .....\n");
Sleep(5*1000);
return 0;
}
- 线程局部存储(TLS)
线程局部存储(Thread Local Storage,TLS)是一个使用很方便的存储线程局部数据得系统。利用TLS机制可以为进程中所有的线程关联若干个数据,
各个线程通过由TLS分配的全局索引来访问与自己关联的数据。这样,保证了每个线程都可以有线程局部的静态存储数据。
windows仅为系统中的每一个进程维护一个位数组,再为该进程中的每一个线程申请一个同样长度的数组空间,如下图所示
位数组的成员是一个标志,每个标志的值为FREE或INUSE,windows系统至少保证有TLS_MINIMUM_AVAILABLE(WinNT.h文件中定义)个标志位可用
- 动态使用TLS典型步骤如下:
1.主线程调用TlsAlloc为线程局部存储分配索引
DWORD TlsAlloc(void); //返回一个TLS索引
2.每个线程调用TlsSetValue和TlsGetValue设置或读取线程数组中的值
BOOL TlsSetValue(
DWORD dwTlsIndex, //TLS索引
LPVOID lpTlsValue //要设置的值
);
LPVOID TlsGetValue(
DWORD dwTlsIndex //TLS索引
);
3.主线程调用TlsFree释放局部存储索引。函数的唯一参数是TlsAlloc返回的索引
- 示例:使用TLS
// 03UseTLS.cpp : Defines the entry point for the application.
//
#include "stdafx.h"
#include <windows.h>
#include <process.h>
#include <stdio.h>
//利用TLS跟踪线程的运行时间
DWORD g_tlsUsedTime;
void InitStartTime();
DWORD GetUsedTime();
UINT __stdcall ThreadFunc(LPVOID)
{
int i;
//初始化开始时间
InitStartTime();
//模拟长时间工作
i = 1000*1000;
while(i--){}
//打印出本线程运行的时间
printf("This thread is comming to end.ThreadID:%d,Used Time:%d\n",GetCurrentThreadId(),GetUsedTime());
return 0;
}
int main(int argc,char* argv[])
{
UINT uId;
int i;
HANDLE h[10];
//通过在进程位数组中申请一个索引,初始化线程运行时间记录系统
g_tlsUsedTime = TlsAlloc();
//十个线程同时运行,并等待它们各自的输出结果
for(i=0;i<10;i++)
{
h[i] = (HANDLE)_beginthreadex(NULL,0,ThreadFunc,NULL,0,&uId);
}
for(i=0;i<10;i++)
{
WaitForSingleObject(h[i],INFINITE);
CloseHandle(h[i]);
}
TlsFree(g_tlsUsedTime);
return 0;
}
//初始化线程的开始时间
void InitStartTime()
{
//获得当前时间,将线程的创建时间与线程对象相关联
DWORD dwStart = GetTickCount();
TlsSetValue(g_tlsUsedTime,(LPVOID)dwStart);
}
//取得一个线程已运行的时间
DWORD GetUsedTime()
{
//获得当前时间,返回当前时间和线程创建时间的差值
DWORD dwElapsed = GetTickCount();
dwElapsed = dwElapsed-(DWORD)TlsGetValue(g_tlsUsedTime);
return dwElapsed;
}