1.为什么使用多线程
多个线程同时访问同一个全局变量,如果都是读取操作,则不会出现问题。如果一个线程负责改变此变量的值,而其他线程负责同时读取变量内容,则不能保证读取到的数据是经过写线程修改后的。
2.多线程缺陷
创建竞争条件是多线程或并发编程的主要缺陷之一,另一个缺陷就是死锁。
避免竞争条件:4种
a.不要使用可能同时位于临界区内的两个进程
b.不要依赖对CPU的速度做出的任何假设
c.不要让临界区外部停止的进程阻塞其他进程
d.不要让进城等待任意长的时间进入其临界区。
死锁必须的先行条件:4个
a.进程声明排他性控制他们需求的资源
b.进程在等待其他资源的释放时占有资源
c.资源无法强行从进程中删除
d.存在一个循环等待条件
3.多线程同步
同步原因:
多个线程同时访问同一个全局变量,如果都是读取操作,则不会出现问题。如果一个线程负责改变此变量的值,而其他线程负责同时读取变量内容,则不能保证读取到的数据是经过写线程修改后的。
用户模式与内核模式线程同步机制比较:
| 用户模式 | 内核模式 |
优点 | 线程同步机制速度快
| 支持多个进程之间的线程同步 防止死锁 |
缺点 | 容易陷入死锁状态 多个进程之间的线程同步会出现问题。(比如竞争资源、死锁) | 线程同步机制速度慢 线程必须从用户模式转为内核模式。这个转换需要很大的代价:往返一次需要占用x 8 6平台上的大约1 0 0 0个C P U周期 |
用户模式:
临界区:CRITICAL_SECTION,CCriticalSection类 一段独占对某些共享资源访问的代码,在任意时刻只允许一个线程对共享资源进行访问。临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
内核对象:
由于这种同步机制使用了内核对象,使用时必须将线程从用户模式切换到内核模式,而这种转换一般要耗费近千个CPU周期,因此同步速度较慢,但在适用性上却要远优于用户模式的线程同步方式。
事件:
HANDLE,CEvent类,线程通信时可以使用过事件内核对象来进行线程间的通信,除此之外,事件内核对象也可以通过通知操作的方式来保持线程的同步。
使用临界区只能同步同一进程中的线程,而使用事件内核对象则可以对进程外的线程进行同步,其前提是得到对此事件对象的访问权,可以通过OpenEvent()函数获取得到。如果事件对象已创建(在创建事件时需要指定事件名),函数将返回指定事件的句柄。对于那些 在创建事件时没有指定事件名的事件内核对象,可以通过使用内核对象的继承性或是调用DuplicateHandle()函数来调用 CreateEvent()以获得对指定事件对象的访问权。在获取到访问权后所进行的同步操作与在同一个进程中所进行的线程同步操作是一样的。
信号量:
HANDLE,CSemaphore类 信号量(Semaphore)它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量 时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数 就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目, 不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可 用资源计数加1。
互斥:HANDLE,CMutex类 Mutex是一种用途非常广泛的内核对象。能够保证多个线程对同一共享资源的互斥访问。在编写程序时,互斥对象多用在对那些为多个线程所访问的内存块的保护上,可以确保任何线程在处理此内存块时都对其拥有可靠的独占访问权。
MFC提供了多种同步对象,下面我们只介绍最常用的四种:
· 临界区(CCriticalSection)
· 事件(CEvent)
· 互斥量(CMutex)
· 信号量(CSemaphore)
///
// tt.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <Windows.h>
#include <stdio.h>
#include <conio.h>
#include <time.h>
int gTest=0;
#define THREADNUM 4
//临界区
CRITICAL_SECTION cs;
//事件句柄
HANDLE hEvent = NULL;
//信号量对象句柄
HANDLE hSemaphore;
// 互斥对象
HANDLE hMutex = NULL;
void fun(int tNum)
{
for (int i=0;i<1000*1000*1;i++)
{
//等待互斥对象通知
WaitForSingleObject(hMutex, INFINITE);
//试图进入信号量关口
//WaitForSingleObject(hSemaphore, INFINITE);
//等待事件置位
//WaitForSingleObject(hEvent, INFINITE);
//临界区
//EnterCriticalSection(&cs);//进入临界区,其它线程则无法进入
//gTest++;
printf("%d--1234567890123456789012345678901234567890123456789012345678901234567890\n",GetCurrentThreadId());
//gTest--;
//LeaveCriticalSection(&cs);//离开临界区,其它线程可以进入
//处理完成后即将事件对象置位
//SetEvent(hEvent);
//ResetEvent(hEvent);
//释放信号量计数
//ReleaseSemaphore(hSemaphore, 1, NULL);
//释放互斥对象
ReleaseMutex(hMutex);
}
}
int _tmain(int argc, _TCHAR* argv[])
{
//创建互斥对象
hMutex = CreateMutex(NULL,FALSE,NULL);
//创建信号量对象
hSemaphore = CreateSemaphore(NULL,2,2,NULL);
/*
创建的Event是自动复位还是人工复位.如果true,人工复位,一旦该Event被设置为有信号,则它一直会等到ResetEvent()API被调用时才会恢复为无信号.
如果为false,Event被设置为有信号,则当有一个wait到它的Thread时, 该Event就会自动复位,变成无信号.
如果想在每次调用WaitForSingleObject后让WINDOWS为您自动地把事件地状态恢复为”无信号”状态,必须把该参数设为FALSE,否则,您必须每次调用ResetEvent函数来清除事件的信号。
这里有两个API函数用来修改事件对象的信号状态:SetEvent和ResetEvent。前者把事件对象设为”有信号”状态,而后者正好相反。
在事件对象生成后,必须调用WaitForSingleObject来让线程进入等待状态.
*/
//创建事件对象
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
// 事件置位
SetEvent(hEvent);
//初始化临界区
//InitializeCriticalSection(&cs);
DWORD threadId[THREADNUM];
HANDLE hThread[THREADNUM];
long c1=clock();
for (int i=0;i<THREADNUM;i++)
{
hThread[i]=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)fun,(LPVOID)i,0,&(threadId[i]));
}
DWORD wm=WaitForMultipleObjects(THREADNUM,hThread,true,INFINITE);
long c2=clock();
printf("耗费时间%d\n",c2-c1);
//删除临界区
//DeleteCriticalSection(&cs);
//删除事件对象
//CloseHandle(hEvent);
//删除信号量
CloseHandle(hSemaphore);
return 0;
}
///
知识点:
可以使用WaitForSingleObject函数来等待一个内核对象变为已通知状态:DWORD WaitForSingleObject(
HANDLE hObject, //指明一个内核对象的句柄
DWORD dwMilliseconds); //等待时间
还可以使用WaitForMulitpleObjects函数来等待多个内核对象变为已通知状态:
DWORD WaitForMultipleObjects(
DWORD dwCount, //等待的内核对象个数
CONST HANDLE* phObjects, //一个存放被等待的内核对象句柄的数组
BOOL bWaitAll, //是否等到所有内核对象为已通知状态后才返回
DWORD dwMilliseconds); //等待时间
该函数的第一个参数指明等待的内核对象的个数,可以是0到MAXIMUM_WAIT_OBJECTS(64)中的一个值。
等你需要通知一个互斥内核对象并等待一个事件内核对象的时候,可以这么写:
ReleaseMutex(hMutex);
WaitForSingleObject(hEvent, INFINITE);
可是,这样的代码不是以原子的方式来操纵这两个内核对象。因此,可以更改如下:
SignalObjectAndWait(hMutex, hEvent, INFINITE, FALSE);
在用户线程/主线程中推荐MsgWaitForMultipleObjects代替WaitForSingleObject和WaitForMultipleObjects()函数
在多线程编程中,通常都需要线程间的同步,一个线程要等待另一个线程的事件才继续执行,一般的做法是采用WaitForSingleObject和WaitForMultipleObjects()函数来实现。但在实际的应用中,经常出现等待线程卡死的状况,也就是说等待的事件一直无效。为什么事件一直无效呢?很多的情况是等待线程阻塞了另外的线程,使另外的线程无法设置事件有效。为什么会阻塞呢?原因就比较多了,需要具体问题具体分析。 WaitForSingleObject和WaitForMultipleObjects()都是阻塞函数,事件无效就一直不返回,从而阻塞该线 程,使该线程无法处理其他的事务,如果其他的线程发送消息过来,将得不到处理而不返回,从而将其他的线程也阻塞,造成相互等待,这就是臭名昭著的“死 锁”!!!
微软提供了另外一个函数可以解决该问题,它就是MsgWaitForMultipleObjects()函数,该函数不但可以等待事件,还可以等待消息,从而处理消息,使线程不阻塞。
MsgWaitForMultipleObjectsEx
函数功能:阻塞时仍可以响应消息
函数原型
DWORD MsgWaitForMultipleObjectsEx(
DWORD nCount, // 句柄数组中句柄数目
LPHANDLE pHandles, // 指向句柄数组的指针
DWORD dwMilliseconds, // 以毫秒计的超时值
DWORD dwWakeMask, // 要等待的输入事件类型
DWORD dwFlags // 等待标志
);
参数
nCount,指定pHandles指向的数组中的对象句柄数目。最大对象数目是MAXIMUM_WAIT_OBJECTS-1
pHandles ,指向一个对象句柄数组。要得到可以使用的对象句柄类型清单,请查看备注部分。数组中可以包含多种对象类型。 Windows NT: 数组中句柄必须拥有SYNCHRONIZE访问权。要得到更多相关信息,请查阅MSDN中Standard Access Rights。
dwMilliseconds ,指定以毫秒计的超时值。即使参数dwWakeMask与dwFlags中指定的条件未满足,超时后函数仍然返回。如果dwMilliseconds值为0,函数测试指定的对象状态并立即返回。如果dwMilliseconds值为INFINITE,函数超时周期为无穷大。
dwWakeMask ,指定被加到对象句柄数组中的输入事件对象句柄的对象类型。这个参数可以是下面列出值的任意组合:
值含义
QS_ALLEVENTS
WM_TIMER, WM_PAINT, WM_HOTKEY输入消息或登记消息(posted message)在消息队列中
QS_ALLINPUT
任何消息在消息队列中
QS_ALLPOSTMESSAGE
登记消息(在此处列出的除外)在消息队列中
QS_HOTKEY
WM_HOTKEY消息在消息队列中
QS_INPUT
输入消息在消息队列中
QS_KEY
WM_KEYUP,WM_KEYDOWN,WM_SYSKEYUP或WM_SYSKEYDOWN消息在消息队列中
QS_MOUSE
WM_MOUSEMOVE消息或鼠标点击消息(WM_LBUTTONUP,WM_RBUTTONDOWN等)在消息队列中
QS_MOUSEBUTTON
鼠标点击消息(WM_LBUTTONUP,WM_RBUTTONDOWN等)在消息队列中
QS_MOUSEMOVE
WM_MOUSEMOVE消息在消息队列中
QS_PAINT
WM_PAINT消息在消息队列中
QS_POSTMESSAGE
登记消息(在此处列出的除外)在消息队列中
QS_SENDMESSAGE
由另一个线程或应用发送的消息在消息队列中
QS_TIMER
WM_TIMER消息在消息队列中
特别是在主线程和界面线程中推荐使用该函数,可以避免很多麻烦!!!
线程间通讯
1.使用全局变量通信
解决线程间通信最简单的一种方法是使用全局变量。对于标准类型的全局变量,我们建议使用volatile 修饰符,它告诉编译器无需对该变量作任何的优化,即无需将它放到一个寄存器中,并且该值可被外部改变。如果线程间所需传递的信息较复杂,我们可以定义一个结构,通过传递指向该结构的指针进行传递信息。
2.使用自定义消息通信
我们可以在一个线程的执行函数中向另一个线程发送自定义的消息来达到通信的目的。一个线程向另外一个线程发送消息 是通过操作系统实现的。利用Windows操作系统的消息驱动机制,当一个线程发出一条消息时,操作系统首先接收到该消息,然后把该消息转发给目标线程, 接收消息的线程必须已经建立了消息循环。