多线程,就是在一个进程里除主线程外创建其他的线程为进程工作,为什么要使用多线程?因为使用多线程在多数情况下都能提升运行速度。多线程主要的应用场景有
避免阻塞(异步调用)
比如你用一般模型的socket通讯,没收到信息前都会堵塞等待,这是你创建多线程等待自己去做其他的。
避免CPU空转
以http server为例,如果只用单线程响应HTTP请求,即处理完一条请求,再处理下一条请求的话,CPU会存在大量的闲置时间 因为处理一条请求,经常涉及到RPC、数据库访问、磁盘IO等操作,这些操作的速度比CPU慢很多,而在等待这些响应的时候,CPU却不能去处理新的请求,因此http server的性能就很差
所以很多web容器,都采用对每个请求创建新线程来响应的方式实现,这样在等待请求A的IO操作的等待时间里,就可以去继续处理请求B,对并发的响应性就好了很多
当然,这种设计方式并不是绝对的,现在像node.js、Nginx等新一代http server,采用了事件驱动的实现方式,用单线程来响应多个请求也是没问题的。甚至实现了更高的性能,因为多线程是一把双刃剑,在提升了响应性的同时,创建销毁线程都是需要开销的,另外CPU在线程之间切换,也会带来额外的开销。避免了这些额外开销,可能是node.js等http server性能优秀的原因之一吧
创建多线程,
1.使用api创建 ,CreateThread()创建多线程。2.使用c函数创建beginthread()。3.使用c++11提供的std::thread创建。一般使用CreateThread()创建的多线程都会与WaitForSingleObject(),ReleaseSemaphore()等连用。
创建多线程后,就会有同步问题(在一个时间点内只能一个人对某个变量进行操作),如果不同步,就会有通时对一个变量的不正确操作,比如:1.2个线程对一个全局变量进行++操作,在同一时间他们都进行5+1=6的操作,A线程操作完之后并不会提示线程B5已经变成6了,线程B依旧进行5+1=6的操作,这样A对这一操作被B覆盖了,会影响到最后的值。2.一个人去银行窗口,去银行取的5000存款的3000,同时,他老婆在自动取款机取3000,因为窗口和存款机没有同步,所以他们认为还有5000块钱,都成功的取出了3000块钱。这就是线程竞争。
线程同步的基本思路都是在一个时间内只允许某个线程对某个资源的操作,实现的方法有很多,1.事件,2.互斥量,3.临界区,4.信号量。
事件的关键api:
CreateEvent() 创建事件
SetEvent() 对事件发出”有信号“,就绪
ResetEvent()对事件发出”无信号“,等待
WaitForSingleObject()接收信号,没信号时堵塞,有信号是就绪。
事件的思路很简单:
创建事件(有参数决定初始信号)
创建线程(线程执行的函数开头使用WaitForSingleObject()确定它是否运行)
SetEvent()/ResetEvent()决定是否有信号运行,
创建事件时会要求你设置自动重置还是手动重置,自动重置每次只能唤醒一个线程结束后再唤醒其他线程,且不确定线程顺序(除非)。
实例:
#include <process.h>
#include <Windows.h>
#include <iostream>
char szBuffer[255] = {0};
HANDLE hEvent = NULL;
unsigned __stdcall ThreadFunc1(void* param)
{
WaitForSingleObject(hEvent,INFINITE);
printf("单词计数线程收到信息:%s",szBuffer);
return 0;
}
unsigned __stdcall ThreadFunc2(void* param)
{
WaitForSingleObject(hEvent,INFINITE);
printf("词法分析线程收到信息:%s",szBuffer);
return 0;
}
unsigned __stdcall ThreadFunc3(void* param)
{
WaitForSingleObject(hEvent,INFINITE);
printf("语法分析线程收到信息:%s",szBuffer);
return 0;
}
int main()
{
//读取本地文件到内存
FILE *pFile = fopen("E:\\test.txt","r");
if (pFile == NULL)
{
printf("读取本地文件失败\n");
return 0;
}
if (fgets(szBuffer,sizeof(szBuffer),pFile) == NULL)
{
sprintf_s(szBuffer,sizeof(szBuffer),"文件为空,不存在数据");
}
fclose(pFile);
//创建手动重置事件,并设置为无信号状态
hEvent = CreateEvent(NULL,//如果lpEventAttributes是NULL,此句柄不能被继承。
TRUE,//如果是TRUE,则是手动重置,否则是自动重置
FALSE,//指定事件对象的初始状态。如果为TRUE,初始状态为有信号状态;否则为无信号状态
"TEST"//指定事件的对象的名称,如果lpName为NULL,将创建一个无名的事件对象。
);
//启动三个操作线程并挂起(事件阻塞)
HANDLE hThread1 = (HANDLE)_beginthreadex(NULL,0,ThreadFunc1,NULL,0,NULL);
HANDLE hThread2 = (HANDLE)_beginthreadex(NULL,0,ThreadFunc2,NULL,0,NULL);
HANDLE hThread3 = (HANDLE)_beginthreadex(NULL,0,ThreadFunc3,NULL,0,NULL);
//设置事件为有信号状态,启动所有线程
SetEvent(hEvent);
//线程执行完毕关闭线程在进程内核对象句柄表中的内核对象
Sleep(100);
CloseHandle(hThread1);
CloseHandle(hThread2);
CloseHandle(hThread3);
CloseHandle(hEvent);
return 0;
}
互斥量关键api:
createmutex()创建互斥量
OpenMutex()打开互斥量(其他线程需要该锁时用到)
WaitForSingleObject() 等待 信号 (这个内核对象通用的)
ReleaseMutex() 关闭信号
互斥量的思路,创建互斥量后,第一个WaitForSingleObject()的线程能后拥有mutex的所有权,即能够就绪(线程有3个状态,就绪,运行,堵塞),其他的WaitForSingleObject()收不到mutex的信号,想要对被锁住的变量操作时会被堵塞,直到拥有mutex的线程使用ReleaseMutex()释放mutex,再重复前面的步骤。
死锁:1. 对一个变量多重加锁 2. 2个线程同时锁住了一个自己的变量又要去申请另一个线程的被锁住的变量,会无限等待先去。
实例:
#include <process.h>
#include <Windows.h>
DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
int Count=5;
HANDLE hMutex;
void main()
{
HANDLE hThread1,hThread2;
hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
hMutex=CreateMutex(NULL,FALSE,NULL);
//TRUE代表主线程拥有互斥对象 但是主线程没有释放该对象 互斥对象谁拥有 谁释放
//FLASE代表当前没有线程拥有这个互斥对象
Sleep(4000);
}
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while (true)
{
WaitForSingleObject(hMutex,INFINITE);
if (Count>0)
{
printf("t1:%d\n",Count--);
}
else
{
break;
}
ReleaseMutex(hMutex);
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while (true)
{
WaitForSingleObject(hMutex,INFINITE);
if (Count>0)
{
printf("t2:%d\n",Count--);
}
else
{
break;
}
ReleaseMutex(hMutex);
}
return 0;
临界区关键api:
0. Critial Sections cs; 创建临界区 &cs
1. VOID InitializeCritialSection(LPCRITICAL_SECTION lpCs); --初如化变量
2. VOID DeleteCritialSection(LPCRITICAL_SECTION lpCs) ---清除变量
3. VOID EnterCritialSection(LPCRITICAL_SECTION lpCs) --进入临界区即加锁
4. VOID LeaveCritialSection(LPCRITICAL_SECTION lpCs) --离开临界区即解锁.
临界区和互斥量很像,不过他不是内核对象,且互斥量的加锁,开锁都会花时间,所以一般临界区都会比互斥量快,也因为不是内核对象,当在临界区内出现异常,外面的线程不能知道里面发生了什么,会无限等待下去,而内核对象有WaitForSingleObject()函数能够判断。临界区锁的对象是代码段,互斥量的对象是变量(我也不知道有什么区别)。
实例:
#include <windows.h>
/*
1.Critial Sections 临界区域,所谓临界区,就是指一块处理共享资源的代码。这段代码可以用Critial Sections保护起来,确保一次只有一个线程进入该代码
即进入临界区。
Critial Sections并不是核心对像,没有Handle,所以不能等待,大概率死锁。
提供4个函数来作为基本用法
1. VOID InitializeCritialSection(LPCRITICAL_SECTION lpCs); --初如化变量
2. VOID DeleteCritialSection(LPCRITICAL_SECTION lpCs) ---清除变量
3. VOID EnterCritialSection(LPCRITICAL_SECTION lpCs) --进入临界区即加锁
4. VOID LeaveCritialSection(LPCRITICAL_SECTION lpCs) --离开临界区即解锁.
*/
LONG g_value = 49;
//创建临界区
CRITICAL_SECTION cs;
DWORD WINAPI ThreadFun(LPVOID lpParam) {
for (int i = 0; i<50; i++)
{
EnterCriticalSection(&cs);
if (g_value > 0)
{
Sleep(100);
printf("售出第%d张\n", 49 - g_value + 1);
g_value--;
}
LeaveCriticalSection(&cs);
}
return 0;
}
int _tmain(int argc, _TCHAR* argv[])
{
InitializeCriticalSection(&cs);
HANDLE handles[2];
for (int i = 0; i<2; i++)
{
handles[i] = CreateThread(NULL, 0, ThreadFun, NULL, 0, NULL);
}
Sleep(100000);//等待以致两个子线程运行完成。
for (int i = 0; i<2; i++)
{
CloseHandle(handles[i]);
}
DeleteCriticalSection(&cs);
return 0;
}
信号量通常用来同步2个进程的方法,比如消费者-生产者模式。
信号量关键函数:
CreateSemaphore() 创建信号量
OpenSemaphore()、 打开信号量 (其他进程需要使用)
ReleaseSemaphore()、 按指定数量增加指定信号量对象的计数。
WaitForSingleObject() 等待解锁/信号量不为0等就绪,信号计数-1
WaitForMultipleObjects(), 等待多个信号
CloseHandle() 关闭信号量
信号量思路:
创建一个信号量,设定他的最大操作进程(线程)数
一个进程/线程中的WaitForSingleObject()等待信号,当有线程ReleaseSemaphore()时,最大操作数+1,最大操作数大于0时,WaitForSingleObject()就能接受信号使进程从堵塞转为就绪,最大操作数-1.
实例:
#include <Windows.h>
//#include <thread>
#include <process.h>
#include <iostream>
// 信号量对象句柄
HANDLE hSemaphore;
void ThreadProc1(LPVOID pParam)
{
Sleep(1);
int i = 3;
{
WaitForSingleObject(hSemaphore, INFINITE);// 试图进入信号量关口
while ((i--) > 0)
{
// 线程任务处理
printf("线程一正在执行!\n\n");
}
ReleaseSemaphore(hSemaphore, 1, NULL); // 释放信号量计数
}
}
void ThreadProc2(LPVOID pParam)
{
Sleep(5);
int i = 3;
{
WaitForSingleObject(hSemaphore, INFINITE);// 试图进入信号量关口
while ((i--) > 0)
{
// 线程任务处理
printf("线程二正在执行!\n\n");
}
ReleaseSemaphore(hSemaphore, 1, NULL); // 释放信号量计数
}
}
void ThreadProc3(LPVOID pParam)
{
Sleep(10);
int i = 3;
{
WaitForSingleObject(hSemaphore, INFINITE);// 试图进入信号量关口
while ((i--) > 0)
{
// 线程任务处理
printf("线程三正在执行!\n\n");
}
ReleaseSemaphore(hSemaphore, 1, NULL); // 释放信号量计数
}
}
int main()
{
// 创建信号量对象
hSemaphore = CreateSemaphore(NULL, 1, 1, NULL);
// 开启线程
_beginthread(ThreadProc1, 0, NULL);
_beginthread(ThreadProc2, 0, NULL);
_beginthread(ThreadProc3, 0, NULL);
system("pause");
return 0;
}
结果:
结语:上面介绍的都是同步的基本用法,也就是简单的调用api,动手几遍就能简单学会了,没有过多的探究他们的实现原理什么的,对于像我这样的入门学生住够用了。( + + )。且现在c++11有std::thread类库,个人认为基本也不会自己手动写这类api,做个了解就行了。
附录:
1、基本概念
线程是进程的一个执行流,是CPU调度的基本单位,是CPU中能独立运行的最小单位。也可以称为轻量级进程。
进程是分配资源的最小单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
2、选择多线程而不是多进程的理由?
(1)线程启动时间远小于进程启动时间。启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式,而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据。
(2)线程间切换时间远小于进程间切换的时间。
(3)线程开销远小于进程开销。总的说来,一个进程的开销大约是一个线程开销的30倍左右。
(4)线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
此外,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
3、进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。