一. Windowsx消息响应机制
系统消息对列 -> 当前应用程序的消息对列 -> GetMessage()从对列中取出消息 -> TranslateMessage()翻译函数 -> DispatchMessage()分发函数 --->Afxunclproc回调函数(所有的函数都有一个回调函数,即消息入口点函数)---消息发送到窗口----> pWnd -> windoproc ----消息映射表--------> 处理函数
tips:SendMessage()不进入程序的消息对列,但进入系统的消息对列
Sleep时,效果,可以执行其他消息吗 ? 进度条变慢:sleep(0);让出本次时间片,sleep(100):睡100s,醒来后再轮转到它才行 。 sleep时可以执行其他消息因为没有从消息对列中拿消息所以不能执行
下面的做法虽然可以在跑进度条的时候,接收其他消息,但二者不能同时执行,创建线程则可以。
???
while(1)
{
MSG msg;
proctrl.StepIt();
//--------------可以并发处理别的消息------------
if(GetMessage(&msg,0,0,0))//从队列中取得消息
{
TranslateMessage(&msg);//翻译消息
DispatchMessage(&msg);//分发消息
}
//---------并发:轮换时间片,不能同时执行----------
}
二. 创建线程
建立线程有三种方式:CreateThread() ,WindowsAPI 此方法的退出方式为ExitThread
AfxBeginThread() ,MFC
_beginthreadex() ,C++类中封装的函数 :由申请空间和CreateThread()两部分组成 , 释放则由ExitThread()和释放空间两部分组成
线程:进程中的执行单元 , 分配CPU的基本单位。
线程栈:存储在线程中创建的变量(线程退出,线程栈被销毁掉)
内核对象:计数器 (初始值为2,当为0时,释放内核对象)1.线程退出(-1)2.关闭句柄(-1)(CloseHandle()) ; openThread()计数器+1
挂起计数器 : SuspendThread()挂起计数器+1;ResumeThread()挂起计数器-1
信号:线程退出则有信号,否则无信号(WAIT_TIMEOUT)。
函数:
m_hThread = CreateThread(NULL, //安全属性
0 ,//线程栈 默认1MB
&ThreadProc, //函数地址
this, //线程参数
0 ,//创建标志 0 立即运行 CREATE_SUSPENDED 挂起
NULL //线程ID
);
挂起线程:
SuspendThread (m_hThread);//挂起计数器+1
恢复线程:
ResumeThread(m_hThread);//线程存在则恢复线程(挂起计数器-1),挂起计数器不能为负
线程退出:
//1.正常退出
m_bFlagQuit = false;
//2.强制退出
//ExitThread()退出调用它的线程
//判断线程是否已经退出,如果没有( 无信号),则强制杀死
if(WAIT_TIMEOUT == WaitForSingleObject(m_hThread,100))
TerminateThread(m_hThread,-1);
if(m_hThread)
{
CloseHandle(m_hThread);//关闭句柄
m_hThread = NULL;
}
三 . 进程间通信:事件
另一个进程使当前进程退出:事件
hSignal = CreateEvent(NULL,//安全属性
TRUE,//复位属性(自动/手动)TRUE为人工,FALSE为自动
FALSE,//初始化(无信号)
_T("Event"));//名字
在另一个进程中打开:置为有信号
HANDLE hEvent = OpenEvent(EVENT_ALL_ACCESS , FALSE , _T("Event"));//第一个为权限,第三个为事件名称(唯一)
if(hEvent)
SetEvent(hEvent);
线程执行函数:
DWORD WINAPI ThreadProc(LPVOID lpParameter)//线程要执行的函数
{
CThreadDlg* pthis = (CThreadDlg*)lpParameter;
while(1)
{
if(WAIT_OBJECT_0 == WaitForSingleObject(pthis->hSignal , 100))
//等待100ms,事件有信号则返回
break;
pthis->proctrl.StepIt();
Sleep(100);
}
return 0;
}
重置信号:
ResetEvent(hSignal);//重置信号(自动则不需要)
四. 用户界面线程:UI线程
创建用户界面线程:AfxBeginThread()//窗口没了,线程就没了,
相当于创建了一个线程,在线程执行的函数中,创建了一个窗口,比如:MyDlg dlg; dlg.DoModal();
Dialog继承自Cwnd,MyThread,继承自CWinThread
CWinApp类就是继承自CWinThread,pMainWnd指针(线程主窗口)指向对话框,将对话框和主线程连接起来
仿照主窗口和主线程,写一个我们自己的UIThread
当点击窗口右上角的关闭标识的时候,1>WM_CLOSE 2>WM_DESTROY 3>WM_QIUT
???用户界面线程(UI线程)与工作者线程的区别:工作者线程没有消息对列。
五. PostMessage 和SendMessage
六. //调用约定
//WINAPI (C++) stdcall 参数入栈顺序从右--左 函数本身清理
//WINAPIV (C) cdecl 参数入栈顺序从右--左 由调用者清理空间 支持可变的参数个数
七.线程同步
可以跨进程使用的:互斥量,事件,信号量
内核对象:都有一个变量:使用计数
一.原子访问:指的是一个线程在访问某个资源的同时,能够保证没有其他线程会在同一时刻访问同一资源。Interlocked系列。
volatile,:防止编译优化(从寄存器中取值,相对于从外存取值更节省时间,但造成了不能取到的值不能及时更新),对特殊地址的稳定访问。
缺点:只能实现对一个32位或者64位值的单独访问,不能实现复杂的功能。
InterlockedExchangeAdd(&g_x,1);
InterlockedIncrement(&g_x);
InterlockedDecrement(&g_x);
二 . 关键段(临界区):同一时刻只一个线程访问代码段
IniticalizeCriticalsection(&g_cs); //初始化CRITICAL_SECTION 中的成员变量
EnterCriticalSection(&g_cs); //线程用它来检查占用标志的函数
LeaveCriticalSection(&g_cs); //离开“卫生间”,门上重新置为无人
DeleteCriticalsection(&g_cs); //删除事件的内核对象,以及为CRITICAL_SECTION初始化的
当有线程正在执行临界区代码时,系统会为想要访问该代码段的的线程创建一个事件的内核对象,将其由用户模式切换到内核模式,处于等待(挂起)状态,当线程执行完代码段时,系统会从一些想要访问该代码段的线程中,将其中一个线程置为可调度状态。
由于用户模式和内核模式切换大概1000个CPU周期,太浪费时间:
(1)异步方式:TryEnterCriticalSection线程在访问时,如果不能访问资源,那么它继续做其他事情,而不用等待。
if(!TryEnterCriticalSection(&pthis->m_cs))//异步
continue;
(2)用旋转锁。因此,当调用EnterCriticalSection的时候,它会用一个旋转锁不断的循环,尝试在一段时间内获得访问权。只有当尝试失败的时候,线程才会切换到内核模式并进入等待状态。为了在使用关键段的时候同时使用旋转锁,我们必须调用下面的函数来初始化关键段。
bool InitializeCriticalSectionAndSpinCount( &pthis->m_cs, 1);
SetCriticalSectionSpinCount( &pthis->m_cs,1)//改变旋转锁的次数
CCriticalSection类:
- 定义一个类变量 :CCriticalSection critical_section;
- 加锁:critical_section.Lock();
- critical_section.Unlock();
三.互斥量:
互斥对象包含一个使用计数、线程ID以及一个递归计数。
线程ID:用来标识当前占用这个互斥量的是系统中那个线程。如果为false 则此互斥量是触发状态,否则,当前线程有对互斥量的拥有权。直到他释放,其他线程才可以拥有。
递归计数:表示这个线程占用该互斥量的次数。
- 创建互斥量
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全属性
BOOL bInitialOwner, //false 则此互斥量是触发状态(可以拿到互斥量),否则,当前线程有对互斥量的拥有权。
LPCTSTR lpName //互斥量的名字);
2.在线程函数中抢互斥量的拥有权,一旦拥有,则其他线程不可以再去抢夺
WaitForSingleObject(pthis->m_mutex,INFINITE);
直到此线程调用
ReleaseMutex(pthis->m_mutex);
其他线程可以继承,抢夺对互斥量的拥有权。
关键段和互斥量的差别:
1.互斥量可以跨进程,关键段只能在同一进程的各个线程之间。当同一个进程中时,使用关键段(节省资源更有效率)
2.关键段是用户模式下的同步对象,互斥量是内核对象,
这种切换是非常耗时的:在X86平台上,一个空的系统调用大概会占用200个CPU 周期—当然,这还不包括执行被调用函数在内核模式下的实现代码。但是造成内核对象比用户模式下的同步机制慢几个数量级的原因,是伴随调度新线程而来的刷新高速缓存以及错过高速缓存(即未命中)。???
互斥量不同于其他内核对象的地方:
系统会检查想要获得互斥量的线程的线程ID与互斥量对象的内部记录的线程ID是否相同。如果线程ID 一致,那么系统会让线程保持可调度状态—即使该互斥量尚未触发。每次线程成功地等待了一个互斥量,互斥量对象的递归计数会递增,使递归计数大于1的唯一途径是利用这个例外,让线程多次等待同一个互斥量。 ReleaseMutex( HANDLE hMutex);函数会将对象的递归计数减1,。如果线程成功地等待了互斥量对象不止一次,那么线程必须调用ReleaseMutex相同的次数才能使对象的递归计数变成0.当递归计数为0的时候,函数还会将线程ID设为0,这样就触发了对象。
简单的来说,就是当多次等待信号量,就要释放多次。
互斥量的等待和释放要在同一个线程中,因为互斥量会记录线程ID,必须由等待到的线程释放
四.事件:人工时间,自动事件
CEvent 类
CEvent(BOOL bInitiallyOwn=FALSE,//初始状态为无信号
BOOL bManualReset=FALSE,//创建的事件为自动事件
LPCTSTR lpszName=NULL,
LPSECURITY_ATTRIBUTES lpsaAttribute=NULL);
ResetEvent()将 事件设为无信号状态。人工事件要自己重置,否则一直保持有信号状态
SetEvent()将事件置为有信号,自动事件执行完该语句,系统自动将事件置为无信号,不需要该函数。
五.信号量:CSemaphore 类
CSemaphore (LONG lInitialCount=1,//当前可用资源数,(初始值为最大值)
LONG lMaxCount=1,//最大资源计数
LPCTSTR pstrName=NULL,
LPSECURITY_ATTRIBUTES lpsaAttributes=NULL);
ReleaseSemaphore()当前可用资源数+1
八.内核对象:系统提供的用户模式下代码与内核模式下代码进行交互的基本接口
一个内核对象是一块内核分配的内存,它只能被运行在内核模式下的代码访问。内核对象记录的数据在整个系统中只有一份,所以它们也称为系统资源。
引入内核对象以后,系统可以方便地完成下面 4 个任务:
(1) 为系统资源提供可识别的名字。
(2) 在进程之间共享资源和数据。
(3) 保护资源不会被未经认可的代码访问。
(4) 跟踪对象的引用情况。这使得系统知道什么时候一个对象不再被使用了,以便释放它占用的空间。
内核对象是进程内的资源,使用计数属性指明进程对特定内核对象的引用次数,当系统发 现引用次数是 0 时,它就会自动关闭资源。事实上这种机制是很简单的,一个进程在第一次创 建内核对象的时候,系统为进程分配内核对象资源,并将该内核对象的使用计数属性初始化为1;以后每次打开这个内核对象,系统就会将使用计数加 1,如果关闭它,系统将使用计数减 1,减到 0 就说明进程对这个内核对象的所有引用都已关闭,系统应该释放此内核对象资源。
内核对象的数据结构仅能够从内核模式访问。应用程序必须使用API 函数访问内核对象。调用函数创建内核对象时, 函数会返回标识此内核对象的句柄。可以想象此句柄是一个能够被进程中任何线程使用的一个 不透明的值,许多 API 函数需要以它作为参数,以便系统知道要操作哪一个内核对象。为了使系统稳定,这些句柄是进程相关的,也就是仅对创建该内核对象的进程有效。如果 将一个句柄值通过某种机制传给其他进程中的线程,那么,该线程以此句柄为参数调用相关函数时就会失败。