多线程编程 ----- 四种同步方法
多线程,就是在一个进程里除主线程外创建其他的线程为进程工作,为什么要使用多线程?因为使用多线程在多数情况下都能提升运行速度。多线程主要的应用场景有
避免阻塞(异步调用)
比如你用一般模型的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、进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
多线程编程 ----- 四种同步方法相关推荐
- iOS多线程全套:线程生命周期,多线程的四种解决方案,线程安全问题,GCD的使用,NSOperation的使用(上)
2017-07-08 remember17 Cocoa开发者社区 目的 本文主要是分享iOS多线程的相关内容,为了更系统的讲解,将分为以下7个方面来展开描述. 多线程的基本概念 线程的状态与生命周期 ...
- iOS 多线程的四种技术方案
iOS 多线程的四种技术方案 image pthread 实现多线程操作 代码实现: void * run(void *param) {for (NSInteger i = 0; i < 100 ...
- Java 实现多线程的四种方式 超详细
Java 实现多线程的四种方式 文章目录 Java 实现多线程的四种方式 一.继承 Thread 类 二.实现 Runnable 接口 三.实现 Callable 接口 四.线程池 1,Executo ...
- java创建多线程的四种方式
java多线程的创建方式是面试经常会被问到的一个问题,因此在这里我对java创建多线程的四种方式做一个简单的归纳与总结,便于复习. 一.继承Thread类创建多线程 ① 创建一个继承于Thread类的 ...
- 创建多线程的四种方式
创建多线程的四种方式 方式一:继承于Thread类 创建一个继承于Thread类的子类 重写Thread类的run()->将此线程执行的操作声明在run()中 创建Thread类的子类的对象 通 ...
- Java:简述Java多线程的四种实现方式
关联文章:<Java:简述Java中的多线程编程> Java多线程实现方式主要有四种:继承Thread类.实现Runnable接口.实现Callable接口通过FutureTask包装器来 ...
- android java 多线程,Android多线程的四种方式
当我们启动一个App的时候,Android系统会启动一个Linux Process,该Process包含一个Thread,称为UI Thread或Main Thread.通常一个应用的所有组件都运行在 ...
- Android多线程的四种方式:Handler、AsyncTask、ThreadPoolExector、IntentService
1.Handler(适用于多个异步任务的更新UI) 采用生产者-消费者模型,Handler就是生产者,通过他可以生产需要执行的任务,Looper就是消费者,不断从MessageQueu ...
- Java 实现多线程的四种方式
在 Java 中实现多线程一共有四种方式: 继承 Thread 类 实现 Runnable 接口 实现 Callable 接口 线程池 下面我将对这四种方式进行入门级的解析和演示. 一.继承 Thre ...
最新文章
- VMware HA实战攻略之五VMwareHA测试验收
- LInux--进程间通信
- SAP Data Intelligence Graph json源代码的结构分析
- c语言程序运行结果怎么看,帮忙看看我这个C语言程序有什么错误,运行结果不正确...
- angular 模块构建_我如何在Angular 4和Magento上构建人力资源门户
- Python学习之路——装饰器
- 自己都不觉得自己值钱,别人怎么觉得你值钱?
- hadoop单机环境搭建
- 五分钟快速入门 Spring Data JPA
- 线上发布?华为P50将于今日开启预热:可能没有超大杯版
- 平面设计配色方案有哪些
- 怎么实现excel2007/2010不同文档窗口分离显示
- 个人博客系统整体介绍
- python判断信用卡号是否合法_怎么使用 Python 校验银行卡号码是否有效-阿里云开发者社区...
- 软件工程网络工程第二次训练(AC代码和详细解释)(C语言描述)
- tkinter点击按钮实现图片的切换
- C++语言的基本符号与词汇
- 异形布局 canvas画龙
- 小说作者推荐:不问三九合集
- STM32触摸屏按下检测多次解决方案
热门文章
- cocos creator 3D | 拇指投篮 | 3D项目入门实战
- 解析淘宝淘口令获取商品ID方法说明
- 2017.06.15-2016.06.18回顾 loc/iloc/ix dataframe相关 oracle无自增去重 correl
- 使用ajax发送数组请求,Ajax请求传递数组参数
- 自用-----------------
- 4. 串的【朴素模式匹配算法】、【KPM算法:求next数组、nextval数组】
- 卡尔曼滤波的使用举例
- Java项目:springboot大学生实习管理系统
- 因果推断杂记——因果推断与线性回归、SHAP值理论的关系(十九)
- 【06】Spring源码-分析篇-ApplicationContext