VC学习笔记之四多线程知识.docx
- 文档编号:11982684
- 上传时间:2023-04-16
- 格式:DOCX
- 页数:33
- 大小:31.12KB
VC学习笔记之四多线程知识.docx
《VC学习笔记之四多线程知识.docx》由会员分享,可在线阅读,更多相关《VC学习笔记之四多线程知识.docx(33页珍藏版)》请在冰豆网上搜索。
VC学习笔记之四多线程知识
4多线程知识
4.1线程的概念
Windows是一个多任务的系统,如果你使用的是windows2000及其以上版本,你可以通过任务管理器查看当前系统运行的程序和进程。
什么是进程呢?
当一个程序开始运行时,它就是一个进程,进程所指包括运行中的程序和程序所使用到的内存和系统资源。
而一个进程又是由多个线程所组成的,线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。
浏览器就是一个很好的多线程的例子,在浏览器中你可以在下载JAVA小应用程序或图象的同时滚动页面,在访问新页面时,播放动画和声音,打印文件等。
多线程的好处在于可以提高CPU的利用率——任何一个程序员都不希望自己的程序很多时候没事可干,在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。
然而我们也必须认识到线程本身可能影响系统性能的不利方面,以正确使用线程:
●线程也是程序,所以线程需要占用内存,线程越多占用内存也越多
●多线程需要协调和管理,所以需要CPU时间跟踪线程
●线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题
●线程太多会导致控制太复杂,最终可能造成很多Bug
一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。
操作系统给每个线程分配不同的CPU时间片,在某一个时刻,CPU只执行一个时间片内的线程,多个时间片中的相应线程在CPU内轮流执行,由于每个时间片时间很短,所以对用户来说,仿佛各个线程在计算机中是并行处理的。
操作系统是根据线程的优先级来安排CPU的时间,优先级高的线程优先运行,优先级低的线程则继续等待。
线程除了能够访问进程的资源外,每个线程还拥有自己的栈。
栈的大小是可以调整的,最小为1KB或4KB(也就是一个内存页。
内存页的大小取决于CPU),一般默认为64KB,但栈顶端永远保留2KB为防止溢出。
如果要改变栈初始时大小,在EVC"Project"-"Settings"-"Link"链接选项"/STACK"后的参数中指定大小。
其中参数1为默认大小,参数2为一个内存页大小,都用十六进制表示。
如果将栈的初始值设置太小,很容易导致系统访问非法并立即终止进程。
线程有五中状态,分别为运行、挂起、睡眠、阻塞、终止。
当所有线程全部处于阻塞状态时,内核处于空闲模式(Idlemode),这时对CPU的电力供应将减小。
4.2何时生成线程
可以考虑在任何程序处理异步活动时生成新线程。
比如,对多窗口编程时,为每个窗口生成一个线程很有好处。
多数多文档界面的应用程序可以为其每个子窗口创建各自的线程。
线程被分为两种:
用户界面线程和工作者线程(又称为后台线程)。
用户界面线程通常用来处理用户的输入并响应各种事件和消息,事实上,应用程序的主执行线程CWinApp对象就是一个用户界面线程,下面我们将着重讲解工作者线程。
4.3线程的创建
在VC中有很多创建线程的方法,下面我们将一一介绍
4.3.1调用API函数CreateThread
4.3.1.1CreateThread的原型如下:
HANDLECreateThread(
LPSECURITY_ATTRIBUTESlpThreadAttributes,
DWORDdwStackSize,
LPTHREAD_START_ROUTINElpStartAddress,
LPVOIDlpParameter,
DWORDdwCreationFlags,//creationflags
LPDWORDlpThreadId
);
其中:
lpThreadAttributes:
表示创建线程的安全属性,即线程的访问权限,它决定谁共享此对象且是否其他进程可以修改此线程,NT下有用。
除非用户线程是需要继承的,否则不需要设置它,在调用的时候直接用NULL代替就可以了。
dwStackSize:
指定线程栈的尺寸,如果为0则与进程主线程栈相同。
lpStartAddress:
指定线程开始运行的地址,即此线程所要调用的函数,是LPTHREAD_START_ROUTINE结构。
lpParameter:
表示传递给线程的32位的参数,即传递给函数的值,没有则为NULL。
dwCreateFlages:
如果它的值为STACK_SIZE_PARAM_IS_A_RESERVATION,那么参数2可以指定栈的大小,内核将按照参数2的数值来为此线程拥有的栈保留地址空间;如果它的值不为STACK_SIZE_PARAM_IS_A_RESERVATION,那么参数2必须设置为0。
此参数的值还可以为0、CREATE_SUSPENDED。
CREATE_SUSPENDED表示这个线程在创建后一直处于挂起状态,直到用ResumeThread函数来恢复。
最后一个参数保存函数返回的创建的线程ID。
lpThreadId用来存放返回的线程ID。
4.3.1.2线程的优先级别
进程的每个优先级类包含了五个线程的优先级水平。
在进程的优先级类确定之后,可以改变线程的优先级水平。
以下的CwinThread类的成员函数用于线程优先级的操作:
intGetThreadPriority(HANDLEhThread);
BOOLSetThradPriority()(intnPriority);
上述的二个函数分别用来获取和设置线程的优先级,这里的优先级,是相对于该线程所处的优先权层次而言的,处于同一优先权层次的线程,优先级高的线程先运行;处于不同优先权层次上的线程,谁的优先权层次高,谁先运行。
要注意的是要想设置线程的优先级,这个线程在创建时必须具有THREAD_SET_INFORMATION访问权限。
对于线程的优先权层次的设置,CwinThread类没有提供相应的函数,但是可以通过Win32SDK函数GetPriorityClass()和SetPriorityClass()来实现。
用SetPriorityClass设置进程优先级类,用SetThreadPriority设置线程优先级水平。
4.3.1.3返回值
CreateThread函数返回指向新线程的句柄,如果不能生成线程,句柄将会是NULL。
另外需要强调的一点是:
即使lpStartAddress和lpParameter参数值无效或者指向不能访问的数据,系统仍将创建线程。
在这些情况下,CreateThread函数返回有效句柄,但是新线程立即终止,并返回错误代码。
可以调用函数GetExitCodeThread函数测试线程生存能力,如果该函数返回STILL_ACTIVE,则线程尚未终止。
4.3.1.4销毁线程对象
在线程终止后,线程句柄仍然有效。
为了销毁线程对象,可以通过调用CloseHandle函数来关闭句柄,如果不知一个句柄存在,线程直到最后一个句柄关闭后才会被销毁;如果用户忘记关闭句柄,当进程终止时,系统会自动销毁线程。
正常情况下,线程运行到它所开始的函数的结尾就会结束,当线程运行到它所开始的函数的结尾,系统自动调用ExitThread函数:
VOIDExitThread(DWORDdwExitCode);
尽管系统自动调用ExitThread函数,如果某些条件迫使线程必须提前结束,用户还可以直接调用此函数结束线程。
4.3.1.5实例
先创建一个线程处理函数:
DWORDThreadProc(LPWORDlpdwParam)
{
inti=1;
CStringss;
while(i>0)
{
i++;
ss.Format("i的值为:
%d",i);
if(i>1000000)
i=1;
_sleep(1000);
}
returnDWORD(0);
}
接着再创建线程:
voidCThreadDlg:
:
OnButton2()
{
DWORDthreadID;
CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadProc2,
NULL,0,&threadID);
}
4.3.2创建MFC线程
4.3.2.1函数介绍
AfxBeginThread函数原型如下:
CWinThread*AfxBeginThread(//工作者线程
AFX_THREADPROCpfnThreadProc,//线程函数地址
LPVOIDpParam,//线程参数
intnPriority=THREAD_PRIORITY_NORMAL,//线程优先级
UINTnStackSize=0,//线程堆栈大小,默认为1M
DWORDdwCreateFlags=0,
LPSECURITY_ATTRIBUTESlpSecurityAttrs=NULL
);
CWinThread*AfxBeginThread(//界面线程
CRuntimeClass*pThreadClass,
intnPriority=THREAD_PRIORITY_NORMAL,
UINTnStackSize=0,
DWORDdwCreateFlags=0,
LPSECURITY_ATTRIBUTESlpSecurityAttrs=NULL
);
参数说明:
pfnThreadProc:
线程函数的地址,该参数不能设置为NULL,线程函数必须定义成全局函数或者类的静态成员函数。
例如:
UINTmyThreadFunc(LPVOIDlparam)
或者
classA
{
public:
staticUINTtaskmain(LPVOIDparam);
BOOLStartTask();
}
之所以要定义成类的静态成员函数,是因为类的静态成员函数不属于某个类对象,这样在调用函数的时候就不用传递一个额外的this指针。
pThreadClass:
指向从CWinThread派生的子类对象的RUNTIME_CLASS
pParam:
要传递给线程函数的参数
nPriority:
要启动的线程的优先级,默认优先级为THREAD_PRIORITY_NORMAL(普通优先级)。
nStackSize:
新线程的堆栈大小,如果设置为0,则使用默认大小,在应用程序中一般情况下线程的默认堆栈大小为1M
dwCreateFlags:
线程创建标志,该参数可以指定为下列标志:
CREATE_SUSPENDED:
以挂起方式启动线程,如果你在线程启动之前想初始化一些CWinThread类中的一些成员变量。
比如:
m_bAutoDelete或者你的派生类中的成员变量,当初始化完成之后,你可以使用CWinThread类的ResumeThread成员函数来恢复线程的运行,如果把该标志设置为0,则表示立即启动线程。
lpSecurityAttrs:
指向安全描述符的指针,如果使用默认的安全级别只要讲该参数设置为NULL就可以了!
4.3.2.2终止线程
终止线程采用函数AfxEndThread(UINTnExitCode),其中,nExitCode是退出码。
4.3.2.3源代码
UINTmyThreadFunc(LPVOIDlparam)
{
inti=1;
while(i<5)
{
AfxMessageBox("nihao");
_sleep(1000);
i++;
}
AfxEndThread(0);
return0;
}
voidCThreadDlg:
:
OnButton2()
{
CWinThread*pThread=AfxBeginThread(myThreadFunc,GetSafeHwnd());
}
DWORDExitCode;
:
:
GetExitCodeThread(pThread->m_hThread,&ExitCode);//检查线程是否结束
if(ExitCode==STILL_ACTIVE)
{
//运行中
}
else
{
//线程已经结束
}
4.3.3使用相同功能的C运行库函数
几个运行库函数具有Win32线程函数完全相同的功能:
unsignedlong_beginthread(
void(*start_address)(void*),//startfunction
unsignedstack_size,
void*arglist);
void_endthread(void);
void_sleep(unsignedlongulMilliseconds);
_beginthread函数为一些诸如signal等C运行库函数所依赖的线程执行初始化。
规则是一致的:
如果用户程序用C运行库函数操纵线程,那么无论在什么情况下用户只有一个选择,那就是只能用C运行库函数。
如果用户程序使用Win32函数操纵线程,那么就必须使用CreateThread函数和ExitThread函数。
4.4挂起和恢复线程的执行
挂起的线程停止运行,不会由调度程序分配处理器时间。
在其他线程使其恢复运行之前,该线程会一直保持这种状态。
线程通过调用下列函数使另一个线程暂停或者恢复继续执行。
DWORDSuspendThread(HANDLEhThread);
DWORDResumeThread(HANDLEhThread);
一个线程可以在没有接收到恢复继续执行的指令的情况下连续多次挂起,但是每个SuspendThread命令必须最后和一个ResmeThread命令相匹配。
SuspendThread的原形是:
DWORDSuspendThread(HANDLEhThread);它返回的是线程的前一个暂停记数.线程暂停的次数,可以是MAXIMUM_SUSPEND_COUNT次(在WINNT.H中是127);SuspendThread与内核方式的执行是异步的,但是在线程恢复运行之前,不会发生用户方式的执行。
调用SuspendThread必须小心,,如果线程试图从堆栈中分配内存,那么该线程将在该线程上设置一个锁,当其他线程试图访问该堆栈时,这些线程的访问就被停止,直到第一个线程恢复运行,只有知道目标线程在干什么时,并且采取强有力的措施避免因暂停线程带耒的问题或死锁状态,SuspendThread才是安全的。
CwinThread类中包含了应用程序悬挂和恢复它所创建的线程的函数,其中SuspendThread()用来悬挂线程,暂停线程的执行;ResumeThread()用来恢复线程的执行。
如果你对一个线程连续若干次执行SuspendThread(),则需要连续执行相应次的ResumeThread()来恢复线程的运行。
UINTmyThreadFunc(LPVOIDlparam)
{
inti=1;
while(i<100)
{
AfxMessageBox("nihao");
_sleep(1000);
if(i==3)
{
pThread->SuspendThread();
}
i++;
}
AfxEndThread(NULL);
return0;
}
CWinThread*pThread;
pThread=AfxBeginThread(myThreadFunc,GetSafeHwnd());
虽然线程只可以自己挂起自己而不能自己恢复运行,但线程可以使自己睡眠一段时间然后唤醒。
睡眠指令从调度队列中暂时删除该线程,线程执行被迫延迟,一段时间间隔后再将其放回调度队列,这样就恢复运行了。
此时,睡眠显然要比空循环好,因为它不占用任何CPU时间。
线程调用下列函数暂停执行一段预定的时间:
VOIDSleep(DWORDdwMilliseconds);
DWORDSleepEx(
DWORDdwMilliseconds,
BOOLbAlertable);
扩展的SleepEx函数特别适用于和后台输入输出函数协调工作,并且可以不用待操作完成就用于初始化读写操作。
操作在后台进行。
当该任务结束后,系统通过程序启动一会调函数通知用户。
后台输入/输出(也叫重叠输入/输出)对和诸如磁带机、网络驱动器等慢速设备协同工作而又需要和用户交互的应用程序特别有用。
SleepEx中的布尔参数会使其在指定的睡眠时间尚未结束之前、而重叠的输入/输出操作却已经完成之时,由系统唤醒未到期的线程。
如果SleepEx函数被中断,该函数返回WAIT_IO_COMPLETION,否则如果睡眠时间结束而未中断,该函数返回0。
4.5线程的同步
在程序中使用多线程时,一般很少有多个线程能在其生命期内进行完全独立的操作。
更多的情况是一些线程进行某些处理操作,而其他的线程必须对其处理结果进行了解。
正常情况下对这种处理结果的了解应当在其处理任务完成后进行。
如果不采取适当的措施,其他线程往往会在线程处理任务结束前就去访问处理结果,这就很有可能得到有关处理结果的错误了解。
例如,多个线程同时访问同一个全局变量,如果都是读取操作,则不会出现问题。
如果一个线程负责改变此变量的值,而其他线程负责同时读取变量内容,则不能保证读取到的数据是经过写线程修改后的。
为了确保读线程读取到的是经过修改的变量,就必须在向变量写入数据时禁止其他线程对其的任何访问,直至赋值过程结束后再解除对其他线程的访问限制。
象这种保证线程能了解其他线程任务处理结束后的处理结果而采取的保护措施即为线程同步。
线程同步是一个非常大的话题,包括方方面面的内容。
从大的方面讲,线程的同步可分用户模式的线程同步和内核对象的线程同步两大类。
用户模式中线程的同步方法主要有原子访问和临界区等方法。
其特点是同步速度特别快,适合于对线程运行速度有严格要求的场合。
内核对象的线程同步则主要由事件、等待定时器、信号量以及信号灯等内核对象构成。
由于这种同步机制使用了内核对象,使用时必须将线程从用户模式切换到内核模式,而这种转换一般要耗费近千个CPU周期,因此同步速度较慢,但在适用性上却要远优于用户模式的线程同步方式。
下面我们将对各种线程同步方式进行讲解。
4.5.1临界区对象
临界区(CriticalSection)是一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。
如果有多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。
临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。
临界区在使用时以CRITICAL_SECTION结构对象保护共享资源,并分别用EnterCriticalSection()和LeaveCriticalSection()函数去标识和释放一个临界区。
所用到的CRITICAL_SECTION结构对象必须经过InitializeCriticalSection()的初始化后才能使用,而且必须确保所有线程中的任何试图访问此共享资源的代码都处在此临界区的保护之下。
否则临界区将不会起到应有的作用,共享资源依然有被破坏的可能。
需要特别说明的是:
临界区对象仅能存在于单个进程内部。
下面通过一段代码展示了临界区在保护多线程访问的共享资源中的作用。
通过两个线程来分别对全局变量g_cArray[10]进行写入操作,用临界区结构对象m_csWIlist来保持线程的同步,并在开启线程前对其进行初始化。
为了使实验效果更加明显,体现出临界区的作用,在线程函数对共享资源g_cArray[10]的写入时,以Sleep()函数延迟1毫秒,使其他线程同其抢占CPU的可能性增大。
如果不使用临界区对其进行保护,则共享资源数据将被破坏,而使用临界区对线程保持同步后则可以得到正确的结果。
代码实现清单附下:
classCCritSecLock
{
public:
CCritSecLock(CRITICAL_SECTION*pcrit)
:
m_pcrit(pcrit)
{
:
:
EnterCriticalSection(pcrit);
}
~CCritSecLock()
{
:
:
LeaveCriticalSection(m_pcrit);
}
private:
CRITICAL_SECTION*m_pcrit;
};
/*************************对临界区的操作************************/
//共享资源
charg_cArray[10];
CRITICAL_SECTIONm_csWIlist;
UINTThreadProc10(LPVOIDpParam)
{
//进入临界区
CCritSecLocklock(&m_csWIlist);
//对共享资源进行写入操作
for(inti=0;i<10;i++)
{
g_cArray[i]='a';
Sleep
(1);
}
return0;
}
UINTThreadProc11(LPVOIDpParam)
{
//进入临界区
CCritSecLocklock(&m_list);
for(inti=0;i<10;i++)
{
g_cArray[10-i-1]='b';
Sleep
(1);
}
return0;
}
voidCThreadDlg:
:
OnButton3()
{
CRITICAL_SECTIONg_clsCriticalSection;
//初始化环境
InitializeCriticalSection(&m_csWIlist);
//启动线程
AfxBeginThread(ThreadProc10,NULL);
AfxBeginThread(ThreadProc11,NULL);
//等待计算完毕
Sleep(300);
//输出计算结果
CStringsResult=CString(g_cArray);
AfxMessageBox(sResult);
}
4.5.2事件内核对象
在前面讲述线程通信时曾使用过事件内核对象来进行线程间的通信,除此之外,事件内核对象也可以通过通知操作的方式来保持线程的同步。
内核对象的线程同步则主要由事件、等待定时器、信号量以及信号灯等内核对象构成。
由于这种同步机制使用了内核对象,使用时必须将线程从用户模式切换到内核模式,而这种转换一般要耗费近千个CPU周期,因此同步速度较慢,但在适用性上却要远优于用户模式的线程同步方式。
WaitForSingleObject函数允许线程挂起自己直到特定对象发送其信号。
利用此函数,线程也可以表示该线程希望等待此对象多长时间。
如果等待时间不定,设置间隔为INFINITE。
如果对象已经可用,或者在指定时间内已经到达其信号状态,WaitForSingle
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- VC 学习 笔记 多线程 知识