多线程编程详解.docx
- 文档编号:23971576
- 上传时间:2023-05-23
- 格式:DOCX
- 页数:34
- 大小:108.94KB
多线程编程详解.docx
《多线程编程详解.docx》由会员分享,可在线阅读,更多相关《多线程编程详解.docx(34页珍藏版)》请在冰豆网上搜索。
多线程编程详解
多线程(Multi-Thread)
一、程序、进程、线程的概念
1、程序(Program)
⏹程序是一个具体的文件,是计算机指令的集合,存储在磁盘上,如EXE文件。
2、进程(Process)
⏹进程:
是一个正在运行程序的实例,是程序在其自身的地址空间中的一次执行活动。
⏹进程是资源申请、调度和独立运行的单位,因此,它使用系统中的运行资源;而程序不能申请系统资源,不能被系统调度,也不能作为独立运行的单位,因此,程序不占用系统的运行资源。
⏹进程由两个部分组成:
内核对象:
操作系统用它来管理进程。
是系统用来存放进程的统计信息的地方。
地址空间:
包含所有可执行模块或DLL模块的代码和数据,以及动态内存所分配的空间,如堆空间和栈空间。
⏹进程是不活泼的。
进程从来不执行任何东西,它只是线程的容器。
若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,此线程负责执行包含在进程地址空间中的代码。
⏹单个进程可能包含若干个线程,这些线程都“同时”执行进程地址空间中的代码。
⏹每个进程至少拥有一个线程,来执行进程的地址空间中的代码。
当操作系统创建一个进程时,会自动创建这个进程的第一个线程,称为主线程。
此后,主线程可以创建其他的线程。
如main()、WinMain()所在的线程一般就是主线程。
⏹系统赋予每个进程独立的虚拟地址空间。
对于32位进程来说,这个地址空间是4GB。
⏹每个进程有它自己的私有地址空间。
3、线程(Thread)
⏹线程也由两个部分组成:
内核对象:
操作系统用它来管理线程。
是系统用来存放线程的统计信息的地方。
线程堆栈:
它用于维护线程在执行代码时需要的所有参数和局部变量。
⏹当创建线程时,系统创建一个线程的内核对象。
该内核对象不是线程本身,而是操作系统用来管理线程的较小的数据结构。
该数据结构保存了线程的相关统计信息。
⏹线程总是在某个进程中创建。
系统从进程的地址空间中分配内存,供线程的堆栈使用。
新线程运行的进程环境与创建线程的环境相同。
因此,新线程可以访问进程的内核对象的所有句柄、进程中的所有内存,以及同一进程中的所有其他线程的堆栈。
这使得单个进程中的多个线程能够非常容易地互相通信。
⏹线程只有一个内核对象和一个堆栈,保留的记录很少,因此所需要的内存也很少。
⏹因为线程需要的开销比进程少,因此在编程中经常采用多线程来解决编程问题,而尽量避免创建新的进程。
⏹操作系统为每一个运行的线程分配一定的CPU时间----时间片。
系统通过一种循环的方式为线程提供时间片,线程在自己的时间内运行,因时间片相当短,因此,给用户的感觉,就好像线程是同时运行的一样。
⏹Sleep()函数会主动暂停当前线程的时间片,暂时交出控制权,自己去“睡觉”。
⏹如果计算机拥有多个CPU,多个线程就能真正意义上同时运行了。
⏹使同一进程中的各线程协调一致地工作称为线程的同步。
系统提供了多种同步方法,如:
临界区(CriticalSection),事件(Event),互斥量(Mutex),信号量(Semaphore)等。
⏹可以使用PostThreadMessage()函数进行线程间的通讯。
4、使用多线程的场合
⏹帮助理解:
一个理发师要为ABC三位贵宾理发,为了不使三个贵宾感到自己有先后之分,理发师可以为A服务n秒,之后为B服务n秒,再为C服务n秒,然后再为A服务n秒,如此循环;只要n足够小,ABC就感觉到自己没有被怠慢。
如果有3个理发师,当然是最理想的了,可以每个理发师真正的为一个贵宾服务。
这里的理发师就相当于CPU,一个理发师就是单CPU,三个理发师就是多CPU了;而为三个贵宾理发,就是3个工作任务。
⏹QQ多人同时聊天。
⏹火车站多窗口售票。
⏹大批量文件复制:
复制文件本身使用一个线程,显示进度使用一个线程。
如果用一个线程的话,则主界面会失去反应,给用户感觉是死机了。
二、Win32线程函数
Win32提供了一系列的API函数来完成线程的创建、挂起、恢复、终结以及通信等工作。
下面选取一些重要函数进行说明。
2.1、创建线程
HANDLECreateThread(
LPSECURITY_ATTRIBUTESlpThreadAttributes,//安全属性,一般NULL
DWORDdwStackSize,//栈的大小,一般设为0
LPTHREAD_START_ROUTINElpStartAddress,//线程入口函数指针
LPVOIDlpParameter,//传递给线程函数的参数
DWORDdwCreationFlags,//创建后挂起或立即执行
LPDWORDlpThreadId//线程的ID,一般NULL
);
该函数在其调用进程的进程空间里创建一个新的线程,并返回已建线程的句柄。
各参数说明如下:
⏹lpThreadAttributes:
指向一个SECURITY_ATTRIBUTES结构的指针,该结构决定了线程的安全属性,一般置为NULL;
⏹dwStackSize:
指定了线程的堆栈深度,一般都设置为0;
⏹lpStartAddress:
表示新线程开始执行时代码所在函数的地址,即线程的起始地址。
一般情况为(LPTHREAD_START_ROUTINE)ThreadFunc,ThreadFunc是线程函数名,其原型如下:
DWORDWINAPIThreadProc(
LPVOIDlpParameter//线程创建者传递给线程的参数
);
⏹lpParameter:
指定了线程执行时传送给线程的32位参数,即线程函数的参数;
⏹dwCreationFlags:
控制线程创建后的状态,可以取两种值。
如果该参数为0,线程在被创建后就会立即开始执行;如果为CREATE_SUSPENDED,则创建线程后,该线程处于挂起状态,并不马上执行,直至函数ResumeThread()被调用;
⏹lpThreadId:
该参数返回所创建线程的ID,一般设为NULL;
2.2、挂起线程
DWORDSuspendThread(HANDLEhThread);
该函数用于挂起参数hThread指定的线程,如果函数执行成功,则线程的执行被挂起。
2.3、唤醒线程
DWORDResumeThread(HANDLEhThread);
该函数用于唤醒参数hThread指定的线程,结束该线程的挂起状态,并开始执行该线程。
2.4、结束线程
VOIDExitThread(DWORDdwExitCode);
该函数用于线程终结自身的执行,主要在线程的执行函数中被调用。
参数dwExitCode用来设置线程的退出码。
只能在线程内部调用该函数,谁调用就会结束谁。
2.5、终止线程
BOOLTerminateThread(HANDLEhThread,DWORDdwExitCode);
一般情况下,线程运行结束之后,线程函数会正常返回,但是应用程序可以调用该函数来强行终止某一线程的执行。
各参数含义如下:
hThread:
将被终结的线程的句柄;
dwExitCode:
用于指定线程的退出码。
2.6、关闭线程句柄
BOOLCloseHandle(HANDLEhObject);
关闭一个已经打开对象的句柄,用在这里,指关闭一个线程的句柄。
注意,只是关闭句柄,并不是关闭线程,线程依然在运行中。
只是调用本函数的线程不再需要对该线程进行操作,放弃对它的控制,使线程的内部计数减一,当线程的内部计数为0时,线程会自动关闭。
Here示例0:
讲解0MultiThread工程,理解线程的各个步骤。
2.7、设置线程的优先级
BOOLSetThreadPriority(HANDLEhThread,intnPriority);
各参数含义如下:
hThread:
线程的句柄;
nPriority:
指定线程的优先级,如THREAD_PRIORITY_NORMAL等。
2.8、C运行时的线程创建
C运行时是在Windows操作系统尚未面世时就已经存在的一套C语言的函数库,因为当时并未考虑到多线程的情况,所以在Windows操作系统下用CreateThread创建的线程中调用了某些C运行时函数,如asctime等,则有可能出现问题,为此后来特地增加了CreateThread的C运行时版本_beginthreadex,原型为:
unsignedint_beginthreadex(
void*security,
unsignedstack_size,
unsigned(__stdcall*start_address)(void*),
void*arglist,
unsignedinitflag,
unsigned*thrdaddr
);
2.9、示例1:
火车站售票系统(“1Mutex”工程)
创建一个MFC对话框工程MultiThread1,界面布局如下图所示:
映射2个编辑框为变量m_str1和m_str2,代码放在MultiThread1Dlg.cpp的最下方,如下:
intnTickets;
HANDLEhThread1;
HANDLEhThread2;
DWORDWINAPIThreadProc1(LPVOIDlpParameter)
{
CMultiThread1Dlg*pDlg=(CMultiThread1Dlg*)lpParameter;
while(TRUE)
{
if(nTickets>0)
{
//:
:
Sleep
(1);
CStringstr;
str.Format("%d",nTickets);
pDlg->m_str1+=str+"\r\n";//如果刚好在此时本线程的时间片用完了
nTickets--;//票号就不会-1,而是去执行线程2,于是就重票了。
}
else
{
break;
}
}
return0;
}
DWORDWINAPIThreadProc2(LPVOIDlpParameter)
{
CMultiThread1Dlg*pDlg=(CMultiThread1Dlg*)lpParameter;
while(TRUE)
{
if(nTickets>0)
{
//:
:
Sleep
(1);
CStringstr;
str.Format("%d",nTickets);
pDlg->m_str2+=str+"\r\n";//如果刚好在此时本线程的时间片用完了
nTickets--;//票号就不会-1,而是去执行线程2,于是就重票了。
}
else
{
break;
}
}
return0;
}
voidCMultiThread1Dlg:
:
OnBtnStart()
{
nTickets=30;
m_str1="";
m_str2="";
hThread1=:
:
CreateThread(NULL,0,ThreadProc1,this,0,NULL);
hThread2=:
:
CreateThread(NULL,0,ThreadProc2,this,0,NULL);
//hThread2=:
:
CreateThread(NULL,0,ThreadProc2,this,CREATE_SUSPENDED,NULL);
:
:
CloseHandle(hThread1);//这并不会关闭线程,只是这里不再需要对线程进行
:
:
CloseHandle(hThread2);//操作,放弃对它们的控制,使线程的内部计数减一。
Sleep(1000);
this->UpdateData(FALSE);
}
点击“开始售票”,发现2个窗口都进行了售票,说明多线程代码都进行工作了。
将上述创建线程2的代码倒数第二个参数改成CREATE_SUSPENDED后运行,2号窗口不再售票。
三、线程同步
上述程序有个明显的问题:
一票多售。
例如:
如果在线程1售票到20号后,已经打印出了票号,但还没有执行到nTickets--语句时,线程1的时间片刚好用完了,线程2得到运行权,但此时nTickets还是20号,于是售出20号票。
这样就出现了一票多售的情况。
因此,多线程编程有时需要处理同步问题。
要同步多个线程,可以使用临界区(CriticalSection),互斥量(Mutex),事件(Event),信号量(Semaphore)、互锁函数等。
临界区非常适合于在同一个进程内部以序列化的方式访问共享的数据。
然而,有时用户希望一个线程与其他线程执行的某些操作取得同步,这就需要使用内核对象来同步线程。
常用的内核对象有互斥量、事件和信号量,其他的还包括文件、控制台输入、文件变化通知、可等待的计时器。
每一个内核对象在任何时候都处于两种状态之一:
信号态(signaled)和无信号态(nonsignaled)。
线程在等待其中的一个或多个内核对象时,如果内核对象处于无信号态,线程自身将被系统挂起,直到等待的内核对象变为有信号状态时,线程才恢复运行。
常用的等待函数有2个:
WaitForSingleObject和WaitForMultipleObjects
DWORDWaitForSingleObject(//等待单个内核对象
HANDLEhHandle,//指向内核对象的句柄
DWORDdwMilliseconds//等待的毫秒数,如果为INFINITE,则无限期等待。
);
WaitForSingleObject函数返回值
返回值
含义
WAIT_OBJECT_0
对象处于有信号状态
WAIT_TIMEOUT
对象在指定时间内没有变为有信号状态
WAIT_ABANDONED
对象是一个互斥量,由于被放弃了而变为有信号状态
WAIT_FAILED
发生了错误。
调用GetLastError可以得到详细的错误信息
DWORDWaitForMultipleObjects(//等待多个对象
DWORDnCount,//对象的个数
CONSTHANDLE*lpHandles,//对象句柄数组
BOOLbWaitAll,//是否要等到所有的对象都变为信号态
DWORDdwMilliseconds//等待的毫秒数,如果为INFINITE,则无限期等待。
)
3.1、使用互斥量(Mutex)同步多线程
相关函数有CreateMutex,ReleaseMutex,WaitForSingleObject等。
3.1.1、创建互斥量
HANDLECreateMutex(
LPSECURITY_ATTRIBUTESlpMutexAttributes,
BOOLbInitialOwner,
LPCTSTRlpName
);
互斥量能够同步多个进程间的数据访问。
各参数说明如下:
⏹lpMutexAttributes:
指向一个SECURITY_ATTRIBUTES结构的指针,该结构决定了线程的安全属性,一般置为NULL;
⏹bInitialOwner:
BOOL类型,如果为真,则创建该互斥量的线程获得该对象的所有权,否则,该线程不获得其所有权。
⏹lpName:
互斥量对象的名称。
如果为NULL,则创建一个匿名的互斥对象。
如果不为空,则在函数调用成功后,调用GetLastError函数将会返回ERROR_ALREADY_EXISTS,可以利用该特性阻止一个进程多次启动。
3.1.2、释放互斥量
BOOLReleaseMutex(HANDLEhMutex);
访问完共享资源后,利用该函数释放对互斥量的控制权。
3.1.3、请求互斥量的使用权,进而锁定对共享资源的访问。
DWORDWaitForSingleObject(HANDLEhHandle,DWORDdwMilliseconds);
参数含义:
hHandle:
所请求的互斥量对象的句柄;
dwMilliseconds:
指定等待时间,单位为毫秒。
一般设为INFINITE,表示无限等待。
该函数调用后,会一直等到参数dwMilliseconds指定的时间已过,或者在该时间内,互斥对象变成了有信号状态,函数才会返回。
返回值:
WAIT_OBJECT_0:
所请求的对象是有信号状态
WAIT_TIMEROUT:
指定的时间以过,并且所请求的对象是无信号状态。
WAIT_ABANDONED:
所请求的对象是一个互斥对象,并且掀起拥有该对象的线程在终止前没有释放该对象,这时,该对象的所有权将授予当前调用线程,并且该互斥对象被置为无信号状态。
3.1.4、使用互斥量同步方法修改上述代码,结果完全正常了。
代码如下,红色部分为新添加的代码:
intnTickets;
HANDLEhThread1;
HANDLEhThread2;
HANDLEhMutex;
DWORDWINAPIThreadProc1(LPVOIDlpParameter)
{
CMultiThread1Dlg*pDlg=(CMultiThread1Dlg*)lpParameter;
while(TRUE)
{
:
:
WaitForSingleObject(hMutex,INFINITE);
if(nTickets>0)
{
//:
:
Sleep
(1);
CStringstr;
str.Format("%d",nTickets);
pDlg->m_str1+=str+"\r\n";//如果刚好在此时本线程的时间片用完了
nTickets--;//票号就不会-1,而是去执行线程2,于是就重票了。
:
:
ReleaseMutex(hMutex);
}
else
{
:
:
ReleaseMutex(hMutex);
break;
}
}
return0;
}
DWORDWINAPIThreadProc2(LPVOIDlpParameter)
{
CMultiThread1Dlg*pDlg=(CMultiThread1Dlg*)lpParameter;
while(TRUE)
{
:
:
WaitForSingleObject(hMutex,INFINITE);
if(nTickets>0)
{
//:
:
Sleep
(1);
CStringstr;
str.Format("%d",nTickets);
pDlg->m_str2+=str+"\r\n";//如果刚好在此时本线程的时间片用完了
nTickets--;//票号就不会-1,而是去执行线程2,于是就重票了。
:
:
ReleaseMutex(hMutex);
}
else
{
:
:
ReleaseMutex(hMutex);
break;
}
}
return0;
}
voidCMultiThread1Dlg:
:
OnBtnStart()
{
nTickets=30;
m_str1="";
m_str2="";
hThread1=:
:
CreateThread(NULL,0,ThreadProc1,this,0,NULL);
hThread2=:
:
CreateThread(NULL,0,ThreadProc2,this,0,NULL);
//hThread2=:
:
CreateThread(NULL,0,ThreadProc2,this,CREATE_SUSPENDED,NULL);
hMutex=:
:
CreateMutex(NULL,FALSE,NULL);
:
:
ReleaseMutex(hMutex);//主线程并不需要hMutex的控制权,释放掉
:
:
CloseHandle(hThread1);//这并不会关闭线程,只是这里不再需要对线程进行
:
:
CloseHandle(hThread2);//操作,放弃对它们的控制,使线程的内部计数减一。
Sleep(1000);
this->UpdateData(FALSE);
:
:
CloseHandle(hMutex);
}
3.1.5、使用命名的互斥量阻止进程多次运行
BOOLCMultiThread1App:
:
InitInstance()
{
HANDLEhMutex=:
:
CreateMutex(NULL,FALSE,"tickets");
if(hMutex&&ERROR_ALREADY_EXISTS==:
:
GetLastError())
returnFALSE;
:
:
ReleaseMutex(hMutex);
//:
:
CloseHandle(hMutex);//必须注释掉,否则不能阻止多次运行
//……
}
3.2、使用事件(Event)同步多线程
与互斥量和信号量不同,互斥量和信号量用于控制对共享数据的访问,而事件发送信号表示某一操作已经完成。
有两种事件对象:
手动重置事件和自动重置事件。
手动重置事件用于同时向多个线程发送信号;自动重置事件用于向一个线程发送信号。
如果有多个线程调用WaitForSingleObject或者WaitForMultipleObjects等待一个自动重置事件,那么当该自动重置事件变为信号态时,其中的一个线程会被唤醒,被唤醒的线程开始继续运行,同时自动重置事件又被置为无信号态,其他线程依旧处于挂起状态。
从这一点看,自动重置事件有点类似于互斥量。
手动重置事件不会被WaitForSingleObject和WaitForMultipleObjects自动重置为无信号态,需要调用相应的函数才能将手动重置事件重置为无信号态。
因此,当手工重置事件有信号时,所有等待该事件的线程都将被激活。
事件对象使用CreateEvent函数创建:
HANDLECreateEvent(
LPSECURITY_ATTRIBUTESlpEventAttributes,//安全属性,一般为NULL
BOOLbManualReset,//是手动重置吗?
BOOLbInitialState,//初始化成有信号态吗?
LPCTSTRlpName//名称,为NULL则匿名
);
参数bManualReset为TRUE时,指定创建的是手动重置事件,否则为自动重置事件;
参数bInitialState表示事件对象被初始化时是信号态还是无信号态;
参数lpName指定事件对象的名称,其他进程中的线程可以通过该名称调用CreateEvent或者OpenEv
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 多线程 编程 详解