第一节:【Window】创建线程的3种方式
第二节:【Window】线程同步概述
第三节:【Window】线程同步方式1——临界区(关键代码段)
第四节:【Window】线程同步方式2——互斥量
第五节:【Window】线程同步方式3——事件
第六节:【Window】线程同步方式4——信号量
文章目录
一、 线程的状态
线程有挂起状态、执行状态、阻塞状态和等待状态。
下面分别介绍:
- 挂起状态:线程创建后并没有直接执行或是调用函数挂起了线程。被挂起了的线程没有执行的能力,只有调用启动函数了之后才能执行。
- 执行状态:在线程的时间片内,拥有CPU资源的时候,这是,线程便开始执行。
- 阻塞状态:由于进行大量输入输出操作或发生执行错误时,线程失去执行状态,只有等待问题解除之后,线程才能进入等待状态。
- 等待状态:线程启动或时间片抢占失败是等待其他线程执行,在此期间,线程随时可能被执行。
二、创建线程
2.1 创建线程方式1——CreateThread
2.1.1 说明
线程内核对象就是一个包含了线程状态信息的数据结构。每次对CreateThread 的成功调用,系统都会在内部为其分配一个内核对象。
线程上下文反应了线程上次执行的寄存器状态,来保证线程之间切换(即还原现场)。
计数器,调用一次OpenThread(CreateThread ),计数器加1,CloseThread(CloseHandle)计数器减一。当计数器值为0时,没有线程使用该内核对象,系统收回内存。计数器的初始值是2(主线程是1,创建的线程是2)。
2.1.2 函数说明
-
头文件
因为C++不像Java一样需要进行跨平台优化,所以我们使用最简单的方法来实现多线程技术——windows.h中的CreateThread以及相关函数和类。首先,以如下的方式引用头文件:#include<windows.h>
-
函数原型:
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程安全性描述(一个结构体,一般是NULL) SIZE_T dwStackSize, //一种数值(栈深度,一般是0) LPTHREAD_START_ROUTINE lpStartAddress, //启动函数 _In_opt_ __drv_aliasesMem LPVOID lpParameter, // 附加参数(一般为NULL) _In_ DWORD dwCreationFlags, //运行参数(是否在创建完成后就启动线程 _Out_opt_ LPDWORD lpThreadId // 返回句柄(一般是0,或者是一个DWORD型变量的地址,别忘了&) );
-
参数说明
lpThreadAttributes 指向SECURITY_ATTRIBUTES结构的指针,决定返回的句柄是否可被子进程继承,如果为NULL则表示返回的句柄不能被子进程继承。 dwStackSize 设置初始栈的大小,以字节为单位,如果为0,那么默认将使用与调用该函数的线程相同的栈空间大小。 pStartAddress 指向线程函数的指针,函数名称没有限制,但是必须以下列形式声明: DWORD WINAPI 函数名 (LPVOID lpParam)
,格式不正确将无法调用成功。Parameter 向线程函数传递的参数,是一个指向结构的指针,不需传递参数时,为NULL。 CreationFlags 控制线程创建的标志,可取值如下:1)CREATE_SUSPENDED(0x00000004):创建一个挂起的线程(就绪状态),直到线程被唤醒时才调用。2)0:表示创建后立即激活。3)STACK_SIZE_PARAM_IS_A_RESERVATION:dwStackSize参数指定初始的保留堆栈的大小,如果STACK_SIZE_PARAM_IS_A_RESERVATION标志未指定,dwStackSize将会设为系统预留的值 lpThreadId 保存新线程的id,是指向线程id的指针,如果为空,线程id不被返回 第三个参数 ——启动函数:
c LPTHREAD_START_ROUTINE lpStartAddress
我们一般这样写:
(LPTHREAD_START_ROUTINE) ThreadStart
意思就是在线程启动的时候调用ThreadStart,之后他就不管了,也就是说这个函数? 就是线程主函数相当于main的意思。也就是说在这个函数中调用的类资源或函数资源 都是属于这个线程的。除了static的存储类。倒数第二个参数——运行参数。
这是实际上是一个bool类型的值,用于标示是否在创建线程后立刻执行,如果为true,也就是0,那么就会立刻执行,否则将会挂起,等待启动
-
返回值
还有我要说一下HANDLE这个类型,它其实是一个指针,也是CreateThread的返回值。也就是一个线程句柄,用于标示一个线程。当然,其他对于线程的操作都需要使用这个指针。函数成功,返回线程句柄,否则返回NULL。如果线程创建失败,可通过GetLastError函数获得错误信息。
-
注:
5.1 如果线程函数return,返回值会隐式调用ExitThread函数,可以使用GetExitCodeThread函数获得该线程函数的返回值。
5.2 使用CreateThread创建的线程具有THREAD_PRIORITY_NORMAL线程优先级。可以使用GetThreadPriority和SetThreadPriority函数获取和设置线程优先级值。
5.3 当一个线程结束时,这个线程的对象将获得有信号状态,使得任何等待这个对象的线程都能够成功并继续执行下去。
5.4 系统中的线程对象一直存活到线程结束,并且所有指向它的句柄都需要通过调用CloseHandle关闭。
5.5 如果一个线程调用了CRT,应该使用_beginthreadex 和_endthreadex(需要使用多线程版的CRT)。
2.1.3 线程状态切换
1. 启动线程
如果调用这个函数,将会启动HANDLE参数所代表的线程.
DWORD ResumeThread(HANDLE hThread); //启动线程
//说明:DWORD是一个数值,代表句柄,无需关注;
//参数表示要启动的线程的句柄,也就是刚才介绍的由CreateThread返回的HANDLE
2. 挂起线程
下面我们看看如何挂起线程,使线程进入挂起状态:
DWORD SuspendThread(HANDLE hThread); //挂起线程
//说明:DWORD是一个整数值,代表一个句柄,无需过分关注
//参数:一个HANDLE线程指针,由CreateThread创建
3. 停止线程
挂起线程后可以进行释放以便停止线程:
delete HANDLE //释放指针资源
//说明:HANDLE是一个HANDLE型指针,代表释放一个线程的资源,使线程死亡
实际上,停止一个线程还有一种方法——强行停止,但是已经不建议使用,现在都是使用挂起+delete的方法
,因为使用强行停止会有很多的安全问题,但也是一个功能,所以在这里为大家介绍一下:
BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode); //强行停止线程
//说明:返回值代表是否成功
//参数:HANDLE指针代表需要结束的线程,DWORD数值代表该线程的退出值
//功能:在任何位置结束任何线程
4. 等待状态
挂起线程可能为了等待重要操作然后再执行线程,以下函数将解除线程挂起状态,使线程进入等待状态:
DWORD ResumeThread(HANDLE hThread); //使线程脱离挂起状态
//说明:返回值也是句柄
//参数:HANDLE类型指针,表示要继续的线程,或刚创建而没有启动的线程
//注意:如果对等待状态下的线程使用本函数,可能会抛出异常或无效果,具体请见MSDN
2.1.4 简单的例程
//多线程抢占输出
#include <iostream>
#include <windows.h>
using namespace std;
void ThreadUser() { //线程入口
cout << "子线程开始" << endl;
for (int i = 0; i < 10; i++) { //抢占循环
cout << "子线程第" << i << "次循环抢占;" << endl; //输出信息
Sleep(100); //抢占延时
}
cout << "子线程结束" << endl;
}
int main() {
cout << "主线程开始" << endl;
HANDLE h; //线程句柄
h=CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadUser, NULL, 1, 0); //创建子线程
ResumeThread(h); //启动子线程
for (int i = 0; i < 10; i++) { //抢占循环
cout << "主线程第" << i << "次循环抢占;" << endl; //输出信息
Sleep(100); //抢占延时
}
Sleep(1000); //等待子线程
CloseHandle(h);
cout << "主线程结束" << endl;
system("pause");
return 0;
}
学习:
原博主写的代码有错误,做点小更改。
https://www.cnblogs.com/zhangyutong926/p/3652632.html
#include <Windows.h>
#include <iostream>
using namespace std;
typedef struct _STRUCT_DATA_
{
int id; //用于标识出票id
int tickets;
}_DATA, *_pDATA;
DWORD WINAPI Fun1(LPVOID lpParam);
DWORD WINAPI Fun2(LPVOID lpParam);
void main()
{
HANDLE hThread1;
HANDLE hThread2;
_DATA stru_data;
stru_data.id = 0;
stru_data.tickets = 20;
hThread1 = CreateThread(NULL, 0, Fun1, &stru_data, 0, NULL);
hThread2 = CreateThread(NULL, 0, Fun2, &stru_data, 0, NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
Sleep(4000);
}
DWORD WINAPI Fun1(LPVOID lpParam)
{
_pDATA data = (_pDATA)lpParam;
while (TRUE)
{
if (data->tickets > 0)
{
Sleep(1);
cout << "fun1: " << data->id++;
cout << " ···thread 1: sell ticket: " << data->tickets-- << endl;
}
else
break;
}
return 0;
}
DWORD WINAPI Fun2(LPVOID lpParam)
{
_pDATA data = (_pDATA)lpParam;
while (TRUE)
{
if (data->tickets > 0)
{
Sleep(1);
cout << "fun2: " << data->id++ ;
cout << " ***thread 2 :sell ticket: " << data->tickets-- << endl;
}
else
break;
}
return 0;
}
2.2 创建线程方式2——_beginthread
并不是Windows标准API,创建线程函数,该函底层调用CreateThread。
2.2.1 函数说明
-
头文件
#include <process.h>
-
函数原型
unsigned long _beginthread( void(_cdecl *start_address)(void *), //声明为void (*start_address)(void *)形式 unsigned stack_size, //是线程堆栈大小,一般默认为0 void *arglist //向线程传递的参数,一般为结构体 );
typedef unsigned int uintptr_t; typedef unsigned (__stdcall* _beginthreadex_proc_type)(void*); uintptr_t _beginthreadex( void* _Security, //线程安全属性 unsigned _StackSize, //线程堆栈大小 _beginthreadex_proc_type _StartAddress, //线程函数地址 void* _ArgList, //传递给线程函数的地址 unsigned _InitFlag, //指定线程是否立即启动 unsigned* _ThrdAddr //存储线程id号 );
2.2.2 安全属性
SECURITY_ATTRIBUTES
结构包含一个对象的安全描述符,并指定检索到指定这个结构的句柄是否是可继承的。这个结构为很多函数创建对象时提供安全性设置。如:CreateFile,CreatePipe,CreateProcess,RegCreateKeyEx,RegSaveKeyEx
。
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength; //结构体的大小,可用SIZEOF取得
LPVOID lpSecurityDescriptor; //安全描述符
BOOL bInheritHandle; //安全描述的对象能否被新创建的进程继承
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
2.2.3 线程终止
_endthreadex()和_endthrea()显示式结束一个线程。然而,当线程函数返回时,_endthread和_endthreadex 被自动调用。endthread和_endthreadex的调用有助于确保分配给线程的资源的合理回收。
线程函数退出。
线程使用的堆栈被释放。
dwExitCode
设置为线程函数的返回值。
递减内核中的code
值,让内核的引用计数减一。
-
结束线程调用,终止自己
VOID WINAPI ExitThread( __in DWORD dwExitCode // 线程结束时的退出码 );
-
由当前线程终止其他线程
BOOL WINAPI TerminateThread( __in_out HANDLE hThread, // 终止的线程句柄 __in DWORD dwExitCode // 退出码 );
-
释放线程空间、释放线程TLS空间、调用ExiteThread结束线程
void _endthread(void); // retval:设定的线程结束码,与ExiteThread函数的参数功能一样, //其实这个函数释放线程TLS空间,再调用ExiteThread函数,但没有释放线程空间。 void _endthreadex(unsigned retval);
可以显示的调用这两个函数来结束线程。系统从线程启动函数返回时,也会自动调用相应的结束 线程函数,收回分配给线程的资源。
2.2.4 _beginthread()和_beginthreadex()的区别
两组函数都是用来创建和结束线程的。这两对函数的不同点如下:
- 从形式上开,_beginthreadex()更像CreateThread()。_beginthreadex()比_beginthread()多3个参数:intiflag,security和threadaddr。
- 两种创建方式的线程函数不同。_beginthreadex()的线程函数必须调用_stdcall调用方式,而且必须返回一个unsigned int型的退出码。
- _beginthreadex()在创建线程失败时返回0,而_beginthread()在创建线程失败时返回-1。这一点是在检查返回结果是必须注意的。
- 如果是调用_beginthread()创建线程,并相应地调用_endthread()结束线程时,系统自动关闭线程句柄;而调用_beginthreadx()创建线程,并相应地调用_endthreadx()结束线程时,系统不能自动关闭线程句柄。因此调用_beginthreadx()创建线程还需程序员自己关闭线程句柄,以清除线程的地址空间。
原文:https://blog.csdn.net/xuanyin235/article/details/77693275
2.2.5 实例
实例一:
#include <iostream>
#include <windows.h>
#include <process.h>
using namespace std;
unsigned int WINAPI ThreadProFunc(void *pParam);
int main(int argc, char **argv)
{
HANDLE hThread;
unsigned int threadId;
hThread = (HANDLE)_beginthreadex(NULL, NULL, ThreadProFunc, NULL, 0, &threadId);
for (int i = 0; i < 100; i++) {
cout << "nihao" << endl;
}
CloseHandle(hThread); //关闭线程句柄
system("pause");
return 0;
}
unsigned int WINAPI ThreadProFunc(void *pParam)
{
for (int i = 0; i < 100; i++) {
cout<<"hello\n";
}
return 0;
}
枪战模式
实例二:
#include <Windows.h>
#include <process.h>
#include <iostream>
using namespace std;
typedef struct _STRUCT_DATA_
{
int id; //用于标识出票id
int tickets;
}_DATA, *_pDATA;
//CRITICAL_SECTION g_cs;
unsigned __stdcall Fun1(LPVOID lpParam);
unsigned __stdcall Fun2(LPVOID lpParam);
void main()
{
HANDLE hThread[2] = { NULL,NULL };
unsigned threadid[2] = { 0 };
_DATA stru_data;
stru_data.id = 0;
stru_data.tickets = 200;
hThread[0] = (HANDLE)_beginthreadex(NULL, 0, Fun1, &stru_data, 0, &threadid[0]);
hThread[1] = (HANDLE)_beginthreadex(NULL, 0, Fun2, &stru_data, 0, &threadid[1]);
//InitializeCriticalSection(&g_cs);
Sleep(4000);
//LeaveCriticalSection(&g_cs);
}
unsigned __stdcall Fun1(LPVOID lpParam)
{
_pDATA data = (_pDATA)lpParam;
while (TRUE)
{
//EnterCriticalSection(&g_cs);
if (data->tickets > 0)
{
Sleep(1);
cout << "fun1: " << data->id++ ;
cout << " *** thread 1 :sell ticket: " << data->tickets-- << endl;
//LeaveCriticalSection(&g_cs);
}
else
{
//LeaveCriticalSection(&g_cs);
break;
}
}
return 0;
}
unsigned __stdcall Fun2(LPVOID lpParam)
{
_pDATA data = (_pDATA)lpParam;
while (TRUE)
{
//EnterCriticalSection(&g_cs);
if (data->tickets > 0)
{
Sleep(1);
cout << "fun2: " << data->id++ ;
cout << " === thread 2:sell ticket: " << data->tickets-- << endl;
// LeaveCriticalSection(&g_cs);
}
else
{
//LeaveCriticalSection(&g_cs);
break;
}
}
return 0;
}
总结:
https://blog.csdn.net/youshijian99/article/details/79679783
https://blog.csdn.net/xuanyin235/article/details/77693275
2.3 创建线程方式3——AfxBeginThread
MFC是微软的VC开发集成环境中提供给程序员的基础函数库,它用类库的方式将Win32 API进行封装,以类的方式提供给开发者。在VC++附带的MFC类库中,提供了对多线程编程的支持,基本原理与基于Win32 API的设计一致,但由于MFC对同步对象做了封装,因此实现起来更加方便,避免了对象句柄管理上的烦琐工作。
MFC提供了两个重载版的AfxBeginThread()函数,一个用于用户界面线程,另一个用于工作者线程。
2.3.1 用户界面线程原型
对于用户界面线程,其原型为:
CWinThread* AFXAPI AfxBeginThread(
CRuntimeClass* pThreadClass, //从CWinThread派生的RUNTIME_CLASS类
int nPriority, //线程优先级,如果为0,则与创建该线程的线程相同
UINT nStackSize, //线程的堆栈大小,如果为0,则与创建该线程的线程相同
DWORD dwCreateFlags, //创建标识,如果是CREATE_SUSPENDED,表示悬挂状态,否则线程在创建后开始线程的执行
LPSECURITY_ATTRIBUTES lpSecurityAttrs); //线程的安全属性
2.3.2 工作者线程原型
对于工作线程,其原型为:
CWinThread* AfxBeginThread(
AFX_THREADPROC pfnThreadProc, //线程的入口函数,声明:UNIT MyThreadFunction(LPVOID pParam)
LPVOID lParam, //传入线程的参数,注意它的类型为LPVOID,所以我们可以传递一个结构体入线程
int nPriority = THREAD_PRIORITY_NORMAL, //线程优先级,一般设置为0,让它和主线程具有共同的优先级
UINT nStackSize = 0, //指定新创建的线程的栈的大小.如果为 0,新创建的线程具有和主线程一样的大小的栈
DWORD dwCreateFlags = 0, //创建标识,如果是CREATE_SUSPENDED,表示悬挂状态,否则线程在创建后开始线程的执行
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL //线程的安全属性
);
//用于创建工作者线程
//返回值:成功时返回一个指向新线程的线程对象的指针,否则为NULL
在MFC
应用程序中,可以利用如下代码实现:
CWinThread *thread1 = AfxBeginThread((AFX_THREADPROC)Fun1, &stru_data);
此外还是用到:
SuspendThread()函数用于暂停线程;
ResumeThread()函数用于恢复线程。
2.3.3 结束线程
结束线程的两种方式:
- 这是最简单的方式,也就是让线程函数执行完成,此时线程正常结束.它会返回一个值,一般0是成功结束,当然你可以定义自己的认为合适的值来代表线程成功执行.在线程内调用
AfxEndThread将会直接结束线程
,此时线程的一切资源都会被回收.注意在线程中使用了CString类,则不能用AfxEndThread来进行结束线程,会有内存泄漏,只有当程序结束时,会在输出窗口有提示多少byte泄漏了。因为Cstring的回收有其自己的机制。建议在AfxEndThread之前先进行return。 - 如果你想让另一个线程B来结束线程A,那么,你就需要在这两个线程中传递信息.不管是工作者线程还是界面线程,如果你想在线程结束后得到它的结果,那么你可以调用:
::GetExitCodeThread
函数。
2.3.4 举例
CWinThread* mythread;
//死循环函数
UINT EndlessLoop (LPVOID lpParam)
{
int i = 1;
for (;;)
i += i;
return 0;
}
void CSingleThreadDlg::OnBnClickedStartthread()
{
//启动线程函数
mythread = AfxBeginThread(
EndlessLoop,//即上面定义的函数
NULL
);
}
void CSingleThreadDlg::OnBnClickedPausethread()
{
// TODO: 在此添加控件通知处理程序代码
mythread->SuspendThread();//挂起线程
}
void CSingleThreadDlg::OnBnClickedresumethread()
{
// TODO: 在此添加控件通知处理程序代码
mythread->ResumeThread();//恢复线程
}
2.4 三者区别
CreateThread | CreateThread 是Windows API 函数,提供操作系统级别操作,不用于MFC及RTL函数中。一般不建议调用此函数。CreateThread是Windows的API函数,提供操作系统级别的创建线程的操作。_beginthread(及_beginthreadex)与AfxBeginThread的底层实现都调用了CreateThread函数。 |
---|---|
_beginthread/beginthreadex | _beginthread/beginthreadex 函数在实现过程中都调用了CreateThread 函数,但都在调用前做了很多初始化工作,在调用后又做了很多检查工作,这使得线程的创建更完整。结束线程的_endthread()函数和_endthreadex()函数 在实现的过程中调用了Exithread() 函数,但他们都做了更多的善后工作,其中的_endthread() 函数甚至还包揽了句柄的删除工作。 |
AfxBeginThread | AfxBeginThread 是MFC创建线程函数,首先创建了相应的CWinThread 对象,然后调用CWinThread::CreateThread,CWinThread::CreateThread 中,完成了对线程对象的初始化工作。 |
CreateThread是由操作系统提供的接口,而AfxBeginThread和_BeginThread则是编译器对它的封装。
实际应用建议:
-
不要在一个MFC程序中使用_beginthreadex()或CreateThread()。这句话的意思是由于
AfxBeginThread()是MFC封装的启动线程的函数
,里面包含了很多和MFC相关的启动信息,而且封装了一些常用的操作,使用起来也比较简便。而用另外两个函数就需要程序员对类型,安全性检查进行更多的思考! -
MFC中用_beginthreadex()函数应该是最佳选择
,因为_beginthreadex()函数是CRun-timeLibrary中的函数,函数的参数和数据类型都是CRun-timeLibrary中的类型,这样在启动线程时就不需要进行Windows数据类型和CRun-timeLibrary中的数据类型之间的转化。减低了线程启动时的资源消耗和时间的消耗!
请牢记:MFC中,决不应该调用
CreateThread
。相反,应该使用Visual C++运行库函数_beginthreadex
。
学习总结:
xuanyin235
蒋鹿丸