每次使用多线程时,总有些细节问题不清楚,这里从基础部分开始整理一下,以便后续进行学习和使用。
机器不同,系统给每个线程分配的时间片和运行机制也不同。我这里是基于win10系统的VS2010的win32控制台应用程序做的。运行结果,会与孙鑫视频课中的有些不同,所以有些代码稍微调整,比如Sleep(1)的位置。
线程创建函数CreateThread
修改说明:
CreateThread(); 来创建线程其实是一种不太好的方法,在实际使用中尽量使用_beginthread()来创建线程,因为更加的安全。
函数的原型:
HANDLE WINAPI CreateThread(
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ SIZE_T dwStackSize,
_In_ LPTHREAD_START_ROUTINE lpStartAddress,
_In_opt_ LPVOID lpParameter,
_In_ DWORD dwCreationFlags,
_Out_opt_ LPDWORD lpThreadId
);
参数说明:
第一个参数 lpThreadAttributes
表示线程内核对象的安全属性,一般传入NULL表示使用默认设置。
第二个参数 dwStackSize
表示线程栈空间大小。传入0表示使用默认大小(1MB)。
第三个参数 lpStartAddress
表示新线程所执行的线程函数地址,多个线程可以使用同一个函数地址。
第四个参数 lpParameter
是传给线程函数的参数。
第五个参数 dwCreationFlags
指定额外的标志来控制线程的创建,为0表示线程创建之后立即就可以进行调度,如果为CREATE_SUSPENDED则表示线程创建后暂停运行,这样它就无法调度,直到调用ResumeThread()。
第六个参数 lpThreadId
将返回线程的ID号,传入NULL表示不需要返回该线程ID号。
返回值
CreateThread的返回值是线程的句柄,失败的话就返回NULL。
用完线程需要关闭线程句柄。CloseHandle(hThread);
一、最简单的多线程原型
#include <Windows.h>// 使用系统API函数,所以需要包含此头文件
#include <iostream>
using namespace std;
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
cout << "thread1 is running" << endl;
return 0;
}
int main()
{
HANDLE hThread1;
hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
// 创建之后,关闭线程句柄。
// 表明主线程不需要引用刚创建的线程,但是线程依然在运行。
// 让线程的引用计数减一,当线程结束,计数就能为0,自动清空释放。
// 否则只能等整个进程计数后,才释放内核对象
CloseHandle(hThread1);
// 这个时间,是为了让线程输出,如果设置的小,不等线程输出完,主线程就会先输出了,然后线程继续输出剩下的部分。
Sleep(10);
cout << "main thread!" << endl;
system("pause");
return 0;
}
当sleep 10ms时,线程能完整输出。当sleep 1ms 时,线程的回车不能输出。 如果线程输出内容比较多,还会被截断。所以需要控制好主线程的Sleep()的时间。
二、测试系统为多线程分配的时间片
系统不同,那么每个线程得到的时间片长短也不同。
#include <Windows.h>// 使用系统API函数,所以需要包含此头文件
#include <iostream>
using namespace std;
int index = 1;
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while(index++ < 1000)
{
cout << "thread1 is running" << endl;
}
return 0;
}
int main()
{
HANDLE hThread1;
hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
// 创建之后,关闭线程句柄。
// 表明主线程不需要引用刚创建的线程,但是线程依然在运行。
// 让线程的引用计数减一,当线程结束,计数就能为0,自动清空释放。
// 否则只能等整个进程计数后,才释放内核对象
CloseHandle(hThread1);
// 这个时间,是为了让线程输出,如果设置的小,不等线程输出完,主线程就会先输出了,然后线程继续输出剩下的部分。
// Sleep(10);
while(index++ < 1000)
cout << "main thread!" << endl;
system("pause");
return 0;
}
从结果,可以看出,两个时间片由系统分配,然后主线程和线程,按照各自得到的时间片交替输出。
三、开辟两个线程,模拟售卖火车票系统
#include <Windows.h>// 使用系统API函数,所以需要包含此头文件
#include <iostream>
using namespace std;
int tickets = 100;
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while(true)
{
if (tickets > 0)
{
Sleep(1);
cout << "thread1 sell ticket : " << tickets--<< endl;
}
else
{
break;
}
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while(true)
{
if (tickets > 0)
{
Sleep(1);
cout << "thread2 sell ticket : " << tickets--<< endl;
}
else
{
break;
}
}
return 0;
}
int main()
{
HANDLE hThread1, hThread2;
hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
Sleep(1000);// 让上面两个线程运行完之后,主线程再结束
system("pause");
return 0;
}
运行结果如下。当tickets=1时,进入1线程,1线程Sleep(1), 2线程开始卖票,然后2线程Sleep(1), 又将执行权交给1线程, 1线程卖完,剩0张票, 所以2线程,就卖0票了。但是在运行过程中,并没有出现这个结果,可能是系统自己处理了最后的判断。定位跟踪了一下,发现确实,到tickets=1时,Sleep(1);就被跳过了,不再睡眠,直接在这一个线程内执行完毕。
虽然不会出现买0张票,当设置tickets=3,单步跟踪时,出现了两个线程同时买2号票,所以还是有问题的。为了安全起见,还是需要互斥或者线程锁的,下面依次进行介绍三种互斥或线程锁的方法。
四、线程互斥CreateMutex
HANDLE hMutex; 相当于一把钥匙,谁拿到了,就先用,等用完了,把钥匙还给系统,下一个线程再取钥匙,进行使用。
hMutex = CreateMutex(NULL, FALSE, NULL);//初始的时候,创建的钥匙,要设置为FALSE,就是谁也没有它的拥有权。如果不小心,设置成了TRUE,那么就需要紧跟其后释放一下线程:ReleaseMutex(hMutex);即谁拥有谁释放。
WaitForSingleObject(hMutex, INFINITE);// 某线程,拿到钥匙的使用权。
...// 执行需要进行的内容
ReleaseMutex(hMutex);// 使用完之后,要释放线程的使用权,否则别的线程请求不到,就堵塞了。
#include <Windows.h>// 使用系统API函数,所以需要包含此头文件
#include <iostream>
using namespace std;
int tickets = 100;
HANDLE hMutex; // 两个线程都会用到,所以需要声明为全局变量
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while(true)
{
WaitForSingleObject(hMutex, INFINITE);// 直到有信号,否则一直等待,不设定超时
if (tickets > 0)
{
Sleep(1);
cout << "thread1 sell ticket : " << tickets--<< endl;
}
else
{
break;
}
ReleaseMutex(hMutex);// 释放指定互斥对象的所有权,如果不释放,线程2无法请求
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while(true)
{
WaitForSingleObject(hMutex, INFINITE);
if (tickets > 0)
{
Sleep(1);
cout << "thread2 sell ticket : " << tickets--<< endl;
}
else
{
break;
}
ReleaseMutex(hMutex);
}
return 0;
}
int main()
{
HANDLE hThread1, hThread2;
hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
//一开始为FALSE,表明没有线程拥有这个互斥对象
hMutex = CreateMutex(NULL, FALSE, NULL);
Sleep(1000);
system("pause");
return 0;
}
运行结果:完美的交替运行。
五、线程同步——创建事件对象CreateEvent
#include <Windows.h>// 使用系统API函数,所以需要包含此头文件
#include <iostream>
using namespace std;
int tickets = 100;
HANDLE hEvent; // 事件对象
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while(true)
{
WaitForSingleObject(hEvent, INFINITE);// 直到有信号,否则一直等待,不设定超时
if (tickets > 0)
{
Sleep(1);
cout << "thread1 sell ticket : " << tickets--<< endl;
}
else
{
break;
}
SetEvent(hEvent);
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while(true)
{
WaitForSingleObject(hEvent, INFINITE);// 请求事件对象,将hEvent设置为非信号状态
if (tickets > 0)
{
Sleep(1);
cout << "thread2 sell ticket : " << tickets--<< endl;
}
else
{
break;
}
SetEvent(hEvent);// 将hEvent设置为有信号状态,其他线程可拿去使用
}
return 0;
}
int main()
{
HANDLE hThread1, hThread2;
hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
// 第二个参数:TRUE,表明人工重置对象, FALSE,非人工,即自动重置对象
// 第三个参数:FALSE,表明一开始没有线程拥有这个hEvent;无信号状态
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
SetEvent(hEvent);// 使得hEvent为有信号状态,其他线程可以取到
Sleep(1000);
CloseHandle(hEvent);// 最后需要关闭事件句柄
system("pause");
return 0;
}
运行结果:交替进行,直到将票卖完。
六、线程同步——关键代码段
用关键代码段,来控制输出,使得每个线程买一部分票。但是结果:全部都是1线程在卖票,不管是否设置sleep。后来调整Sleep的位置,得到想到的交替进行的结果。
不知道系统内部是如何调用的这个关键代码段标记。跟教科书上不一样。所以最终是不建议用这个。
#include <Windows.h>// 使用系统API函数,所以需要包含此头文件
#include <iostream>
using namespace std;
int tickets = 1000;// 两个线程共同卖票
CRITICAL_SECTION section;// 临界区对象
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while(true)
{
EnterCriticalSection(§ion);// 进入临界区,获取所有权
if (tickets > 0)
{
//Sleep(2);// 不管用
cout << "thread1 sell ticket : " << tickets-- << endl;
}
else
{
break;
}
LeaveCriticalSection(§ion);// 释放所有权
Sleep(1);// 会得到正解
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while(true)
{
EnterCriticalSection(§ion);
if (tickets > 0)
{
//Sleep(2);// 不管用
cout << "thread2 sell ticket : " << tickets-- << endl;
}
else
{
break;
}
LeaveCriticalSection(§ion);
Sleep(1);// 会得到正解
}
return 0;
}
int main()
{
InitializeCriticalSection(§ion);// 用之前,必须初始化
HANDLE hThread1, hThread2;
hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
Sleep(4000);// 设置的时间一定要等线程运行完,否则,下面的代码会出现问题
DeleteCriticalSection(§ion);// 程序都运行完成之后,释放
system("pause");
return 0;
}
如果没有运行完线程,就调用:DeleteCriticalSection(§ion); 则会出现如下问题。
正确运行结果:将Sleep(1); 放在关键代码段结束之后。
至此,多线程使用的几种方法整理完毕。至于在多线程中,使用哪个线程,需要根据情况而定。通过这一次的对比,个人还是更倾向于第一种互斥。