01.文章目录
文章目录
02.浅谈多线程
多线程在编程中有相当重要的地位,我们在实际开发时或者找工作面试时总能遇到多线程的问题,对多线程的理解程度从一个侧面反映了程序员的编程水平。
其实C++语言本身并没有提供多线程机制,但Windows系统为我们提供了相关API,我们可以使用它们来进行多线程编程。本文就以实例的形式聊聊多线程编程的知识。
但是C++11之后,C++也有了自己的多线程库,后面再聊,先聊Windows提供的API
03.创建线程的API(单独聊)
C++代码:
//该函数在其调用进程的进程空间里创建一个新的线程,并返回已建线程的句柄。
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
参数说明:
-
IpThreadAttributes线程安全属性设置,一般设置为NULL;
-
dwStackSize 指定了线程的堆栈深度,一般设置为0,表示线程堆栈与创建它的线程相同
-
IpStartAddress 线程执行时的地址,即线程执行所在函数的地址 函数创建规定:DWORD WINAPI 函数名 (LPVOID lpParam) ,格式不正确将无法调用成功。
-
IpParameter 指定了线程执行时传送给线程的32位参数,即线程函数的参数
-
dwCreationFlags 控制线程创建的附加标志,可以取两种值:如果该参数为0,线程在被创建后就会立即开始执行;
如果该参数为CREATE_SUSPENDED,则系统产生线程后,该线程处于挂起状态,并不马上执行,直至函数ResumeThread被调用; -
lpThreadId:该参数返回所创建线程的ID;
ps:LPVOID是一个没有类型的指针,也就是说你可以将LPVOID类型的变量赋值给任意类型的指针,比如在参数传递时就可以把任意类型传递给一个LPVOID类型为参数的方法,
然后在方法内再将这个“任意类型”从传递时的“LPVOID类型”转换回来。
3.1线程ID
线程ID类型 DWORD
例如:DWORD n = GetCurrentThreadId(); //获取当前进程的ID
将n的地址传入线程创建函数即可。
04.线程相关API
这里主要聊三个常用的API:
1.线程悬挂和恢复
2.设置线程优先级
3.线程退出
4.1 线程悬挂和恢复
创建新的线程后,该线程就开始启动执行。但如果在dwCreationFlags中使用了CREATE_SUSPENDED特性,线程并不马上执行,而是先挂起,等到调用ResumeThread后才开始启动线程。
//该函数用于挂起指定的线程,如果函数执行成功,则线程的执行被终止。
DWORD SuspendThread(HANDLE hThread);
结束挂起状态:
//该函数用于结束线程的挂起状态,执行线程。
DWORD ResumeThread(HANDLE hThread);
4.2 线程优先级
设置线程的优先级和得到线程的优先级,即CPU把时间片段给谁
//调用该函数得到线程优先权。
int GetThreadPriority(HANDLE hThread);
//调用该函数来设置线程的优先权。
BOOL SetThreadPriority(HANDLE hThread,int nPriority);
4.3线程退出
当调用线程的函数返回后,线程自动终止。
//如果需要在线程的执行过程中终止则可调用函数:
void ExitThread(DWORD dwExitCode);
//如果在线程的外面终止线程,则可调用下面的函数:
BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode);
05.线程注意与拓展
5.1 线程注意事项
- TerminateThread函数可能会引起系统不稳定,而且线程所占用的资源也不释放。因此,一般情况下,建议不要使用该函数。
- 如果要终止的线程是进程内的最后一个线程,则线程被终止后相应的进程也应终止。
- 释放资源后,将线程HANDLE置成NULL。
- 使用TerminateThread后,需调用CloseHandle( )函数释放线程所占用的堆栈。
5.2 拓展——进程间通信
5.2.1 全局变量方式
- 进程和线程共享全局变量,可利用该全局变量达到通信的目的。
- 将进程的HADNLE作为参数传递给线程函数,然后线程可根据此HANDLE对进程的变量进行操作。
5.2.2 消息通信方式
消息通信方式肯定是借助Windows提供的API了:
1.BOOL PostMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
2.BOOL PostThreadMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
显然它们发送到的对象不同:PostMessage发消息给窗口,PostThreadMessage发消息给线程。且当PostThreadMessage的hWnd = NULL时,表明该消息传递给当前线程本身。
BOOL PostMessage( UINT message, WPARAM wParam = 0, LPARAM lParam =0 );
返回值如果公布了消息,则返回非零值;否则返回0。不管对方有没有收到,直接返回。
参数说明:
message:指定了要公布的消息。
wParam:指定了附加的消息信息。这个参数的内容依赖于要公布的消息。
lParam:指定了附加的消息信息。这个参数的内容依赖于要公布的消息。
说明:这个函数将一个消息放入窗口的消息队列,然后直接返回,并不等待对应的窗口处理消息。消息队列中的消息是通过调用Windows的GetMessage或PeekMessage函数来获得的。可以通过Windows的PostMessage函数来访问其它应用程序。
BOOL PostThreadMessage( UINT message , WPARAM wParam, LPARAMlParam );
返回值如果成功,则返回非零值;否则返回0。
参数说明:
message:用户自定义消息的ID。
wParam:第一个消息参数。
lParam:第二个消息参数。
说明:调用这个函数以向其它CWinThread对象发送一个用户自定义消息。发送的消息通过消息映射宏ON_THREAD_MESSAGE被映射到适当的消息处理函数。
注:CloseHandle是关闭线程句柄,用来释放线程资源的,不是终止线程的。关闭线程句柄只是释放句柄资源,新开启线程后,如果不再利用其句柄,应该关闭句柄,
释放系统资源。关闭线程句柄和线程的结束与否没有关系。如果主线程只想创建线程,而并不想之后再查询或操纵它,那么及时关闭句柄是个好习惯,免得当时没关,
以后又忘了,于是泄漏了系统的句柄资源(系统句柄总数是有限的)。
06.互斥量
互斥量是将资源互斥,即进程在某一相同的程序时,可以将某一程序片段设定为互斥量,线程不能同时访问此代码段,只有当前线程退出此互斥锁,另一线程才可以进入。
6.1 创建互斥量
HANDLE WINAPI CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes, //线程安全相关的属性,常置为NULL
BOOL bInitialOwner, //创建Mutex时的当前线程是否拥有Mutex的所有权
LPCTSTR lpName //Mutex的名称
);
参数说明:
- MutexAttributes:也是表示安全的结构,与CreateThread中的lpThreadAttributes功能相同,表示决定返回的句柄是否可被子进程继承,如果为NULL则表示返回的句柄不能被子进程继承。
- bInitialOwner:表示创建Mutex时的当前线程是否拥有Mutex的所有权,若为TRUE则指定为当前的创建线程为Mutex对象的所有者,其它线程访问需要先ReleaseMutex
- lpName:Mutex的名称
6.1 互斥量等待指定对象
WaitForSingleObject:等待一个指定的对象(如Mutex对象),直到该对象处于非占用的状态(如Mutex对象被释放)或超出设定的时间间隔。除此之外,还有一个与它类似的函数WaitForMultipleObjects,它的作用是等待一个或所有指定的对象,直到所有的对象处于非占用的状态,或超出设定的时间间隔。
DWORD WINAPI WaitForSingleObject(
HANDLE hHandle, //要获取的锁的句柄
DWORD dwMilliseconds //超时间隔
);
参数说明:
- hHandle:要等待的指定对象的句柄。
- dwMilliseconds:超时的间隔,以毫秒为单位;如果dwMilliseconds为非0,则等待dwMilliseconds时间间隔用完或对象变为非占用的状态,如果dwMilliseconds 为INFINITE则表示无限等待,直到等待的对象处于非占用的状态。
6.3 释放互斥量
//说明:释放所拥有的互斥量锁对象,hMutex为释放的互斥量句柄
BOOL WINAPI ReleaseMutex(HANDLE hMutex);
参数说明:
hMutex:指定要释放的互斥量句柄名字。
07.多线程实例(多版本对照)
声明:此博客所有的实例都是C++版
7.1 多线程(不做处理)
这里我说明一下:我们手动创建的线程均是子线程,父线程既是创建他的进程,线程依赖于进程而存活。
#include <iostream>
#include <Windows.h> //头文件,用到的函数都出自这里面
using namespace std;
//子线程
DWORD WINAPI FUN(LPVOID lparameter)
{//这里的i跟下面的i是区分的,两个函数中的局部变量不冲突
for(int i = 0; i < 10; i++)
{
cout << "This is child Thread" << endl;
}
return NULL;
}
int main()
{
//创建线程
HANDLE hThread = CreateThread(NULL,0,FUN,NULL,0,NULL); //如果想要线程ID,最后一个参数找个值来接
CloseHandle(hThread);
for(int i = 0; i < 10; i++)
{
cout << "This is main Thread" <<endl;
}
system("pause");
return 0;
}
Result:
上面的执行结果是主线程打印1次,然后子线程打印10次,最后主线程打印剩下的。而且执行很快,看不清效果,我们用Sleep函数给个延迟。
7.2 多线程(Sleep版)
#include <iostream>
#include <Windows.h> //头文件,用到的函数都出自这里面
using namespace std;
//子线程
DWORD WINAPI FUN(LPVOID lparameter)
{//这里的i跟下面的i是区分的,两个函数中的局部变量不冲突
for(int i = 0; i < 10; i++)
{
cout << "This is child Thread" << endl;
Sleep(1000);
}
return NULL;
}
int main()
{
//创建线程
HANDLE hThread = CreateThread(NULL,0,FUN,NULL,0,NULL); //如果想要线程ID,最后一个参数找个值来接
CloseHandle(hThread);
for(int i = 0; i < 10; i++)
{
cout << "This is main Thread" <<endl;
Sleep(1000);
}
system("pause");
return 0;
}
Result:
这个版本虽然比上个版本好,但是,并没有改变根本,还是子线程跟主线程抢CPU资源,偶尔交替执行,而且,有时候打印完一句话之后本来应该换行,但是资源被抢走了,所以,没有换行,打印另外一个,但是偶尔又换两行。所以,问题并没有解决,但是我们先解决换行。
7.3 多线程(解决换行问题)
#include <iostream>
#include <Windows.h> //头文件,用到的函数都出自这里面
using namespace std;
//子线程
DWORD WINAPI FUN(LPVOID lparameter)
{//这里的i跟下面的i是区分的,两个函数中的局部变量不冲突
for(int i = 0; i < 10; i++)
{
cout << "This is child Thread\n";
Sleep(1000);
}
return NULL;
}
int main()
{
//创建线程
HANDLE hThread = CreateThread(NULL,0,FUN,NULL,0,NULL); //如果想要线程ID,最后一个参数找个值来接
CloseHandle(hThread);
for(int i = 0; i < 10; i++)
{
cout << "This is main Thread\n";
Sleep(1000);
}
system("pause");
return 0;
}
Result:
这个版本,我们用C语言常用的换行符 \n 来代替了endl,问题解决了,确实是按照我们想要的换行方式换行的。但是我们还是得解决根本问题——资源分配掌握在我们手里。
这个时候就要用到我们的互斥量了
7.4 多线程(互斥量版)
#include <iostream>
#include <Windows.h> //头文件,用到的函数都出自这里面
using namespace std;
//创建互斥量句柄
HANDLE hMutex;
//子线程
DWORD WINAPI FUN(LPVOID lparameter)
{//这里的i跟下面的i是区分的,两个函数中的局部变量不冲突
for(int i = 0; i < 10; i++)
{
WaitForSingleObject(hMutex,INFINITE);
cout << "This is child Thread\n";
Sleep(1000);
ReleaseMutex(hMutex);
}
return NULL;
}
int main()
{
//创建线程
HANDLE hThread = CreateThread(NULL,0,FUN,NULL,0,NULL); //如果想要线程ID,最后一个参数找个值来接
hMutex = CreateMutex(NULL,FALSE,(LPCTSTR)"Mutex1");
CloseHandle(hThread);
for(int i = 0; i < 10; i++)
{
WaitForSingleObject(hMutex,INFINITE);
cout << "This is main Thread\n";
Sleep(1000);
ReleaseMutex(hMutex);
}
system("pause");
return 0;
}
Result:
这个版本就非常完美了,打印、换行都是按照我们想要的方式来的。但是这里提示一点,容易被忘记的一点。
创建互斥量的第二个参数,如果设置为TRUE,主线程就是所有者,就只会打印主线程,执行完主线程就结束了。所以如果想两个线程都能执行,仅仅只是互斥的效果,那么这里一定要设置为FALSE,上面的所有代码都是亲测,可以直接使用,下面再贴一下所有者的意思代码!
7.5 多线程(所有权阐释)
#include <iostream>
#include <Windows.h> //头文件,用到的函数都出自这里面
using namespace std;
//创建互斥量句柄
HANDLE hMutex;
//子线程
DWORD WINAPI FUN(LPVOID lparameter)
{//这里的i跟下面的i是区分的,两个函数中的局部变量不冲突
for(int i = 0; i < 10; i++)
{
//WaitForSingleObject(hMutex,INFINITE); //取消子线程的加锁就行了
ReleaseMutex(hMutex); //这个的位置下面上面都一样,但是逻辑上是上面
cout << "This is child Thread\n";
Sleep(1000);
}
return NULL;
}
int main()
{
//创建线程
HANDLE hThread = CreateThread(NULL,0,FUN,NULL,0,NULL); //如果想要线程ID,最后一个参数找个值来接
hMutex = CreateMutex(NULL,FALSE,(LPCTSTR)"Mutex1");
CloseHandle(hThread);
for(int i = 0; i < 10; i++)
{
WaitForSingleObject(hMutex,INFINITE);
cout << "This is main Thread\n";
Sleep(1000);
//ReleaseMutex(hMutex);
}
system("pause");
return 0;
}
08.小结
关于多线程,说实话,我也不是很明白,但是上面这些是我目前消化了的知识,是没有问题的。
今天就浅谈一下多线程的创建、互斥量在多线程中的作用。
互斥量的使用场景一定要是多个线程使用同一资源时才有意义,当然,解决多线程同步的方法还有很多,比如我知道的还有原子类也可以,关于原子类就后面再详谈吧。
如果上面的内容对你有用,可以点个收藏留条回来的路,哈哈哈。
版权声明:转载请注明出处,谢谢!