1、使用MsgWaitForMultipleObjects或WaitForMultipleObjects时注意:
其第一个参数最大值64
即等待的线程最多不超过64个
错误:等待超过64个线程,返回WAIT_FAILED,参数无效
2、每个线程都有独有的寄存器,线程并发时:
变量variable = 0
线程A读取变量值variable到寄存器eax,eax进行加1,将eax中值写入变量。总共执行了3步
如果线程B在A执行的第一步后,第三步未结束前。那么B读取的variable还是0
此时A写入variable的值为1,B写入variable的值也为1.多线程的两次自加结果只加1。
3、错误笔记
CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);
EnterCriticalSection(&cs);
LeaveCriticalSection(&cs);
DeleteCriticalSection(&cs);
临界区崩溃
原因:在多线程尚未结束时,就调用DeleteCriticalSection删除了临界区
DWORD g_taskCnt = 0;
InterlockedIncrement(&g_taskCnt);
InterlockedDecrement(&g_taskCnt);
实现数值得原子性加减,保证一个线程访问变量时,其它线程不能访问
4、内核对象
内核对象都是进程相关的,除特定情况,不同进程间的句柄是不能相互使用的。
跨越进程边界共享内核对象:
方法:
子进程继承对象句柄
给对象命名(采用GUID来命名,以确保不会与其它公司的程序的对象名相同)
复制对象句柄,DuplicateHandle
文件共享内存,两个进程间共享数据块。
邮箱或管道,联网的运行进程间发送数据块。
互斥对象、信标、事件,不同进程间的线程能够同步运行。
进程内核对象与进程地址空间是两个不同的概念。
进程内核对象维护关于进程的统计信息。
(即使进程关闭,只要内核对象未撤销就可获取进程的统计信息,如GetExitCodeProcess)
进程地址空间加载代码,运行。
5、CreateProcess创建进程
创建的进程和主线程会被分别赋予一个独一无二的ID号。
但是,ID号可能被复用,即进程结束后,新的进程可能与之前进程ID相同。
父进程中在获取子进程句柄后,使用CloseHandle之前(保证内核对象未被撤销),ID都标识子进程
进程终止
1、主线程的进入点函数返回(最好使用这个方法)
保证所有线程资源得到清除的唯一方法
? 该线程创建的任何C + +对象将能使用它们的析构函数正确地撤消。
? 操作系统将能正确地释放该线程的堆栈使用的内存。
? 系统将进程的退出代码(在进程的内核对象中维护)设置为进入点函数的返回值。
? 系统将进程内核对象的返回值递减1
2、进程中的一个线程调用ExitProcess函数(应该避免使用这种方法)
3、另一个进程中的线程调用TerminateProcess函数(应该避免使用这种方法)
进程被终止,没有机会执行自己的清除操作
但是,一旦进程终止运行(无论采用何种方法),系统将确保该进程不会将它的任何部分
遗留下来。
4、进程中的线程自行终止(这种情况几乎从未发生)
进程终止时出现的情况:
(1)进程中所有的线程全部终止
(2)进程指定的所有用户对象和GDI对象均被释放,
所有内核对象被关闭(如果没有其它进程打开它们的句柄,那么这些内核对象将被撤销,
但是,如果其它进程打开了它们的句柄,内核对象的引用计数会减1,但不撤销,直到内核对象的引用计数为0)
(3)进程的退出代码将从STILL_ACTIVE改为传递给ExitProcess或TerminateProcess的代码
(4)进程内核对象的状态变成收到通知的状态,系统中其它线程可以挂起,直到进程终止运行
(5)进程内核对象的引用计数减1
句柄表依赖进程存在,句柄表中保存了内核对象的地址,而程序代码中获取的句柄都是句柄表中的索引值
进程是不活泼的,从来不执行任何东西,它只是线程的容器,线程在进程环境中创建,在进程地址空间中执行代码、数据操作
6、线程
每个进程至少需要一个线程
CreateTHread
_beginthreadex,C++运行期库函数
组成:
(1)线程内核对象(线程统计信息组成的一个小型数据结构),操作系统用它来实施线程管理,内核对象也是系统用来存放线程统计信息的地方。
(2)线程堆栈,用于维护线程在执行代码时需要的所有函数参数和局部变量
每当进程初始化时,其主线程与C/C++运行期库的启动代码一道开始启动,调用进入点函数mian,
继续运行直到进入点函数返回并且C/C++运行期库调用ExitProcess为止。
终止线程的运行:
(1)线程函数返回
(2)调用ExitThread函数,线程自行撤销
(3)同一个进程或另一个进程中的线程调用TerminatedThread函数
(4)包含线程的进程终止
线程返回,可保证:
(1)线程函数中创建的所有C++对象均将通过它们的撤销函数正确的撤销
(2)操作系统将正确的释放线程堆栈使用的内存
(3)系统将线程的退出代码(在线程程的内核对象中维护),从STILL_ACTIVE改为线程函数的返回值
(4)系统将递减线程内核对象的使用计数
ExitThread结束:
终止线程的运行,并导致操作系统清除该线程使用的所有操作系统资源,但是C++资源(如C++类对象)将不会被撤销。
ExitThread是VC++独有,其它平台不支持,因此最好使用C++运行期库函数替代_endthreadex
GetExitcodeThread获取线程退出代码,检查线程状态
_endthred与_endthreadex区别
_endthred会调用CloseHandle,因此如果调用_endthred退出线程后,
再调用GetExitCodeThread(hThread, &dwExitCode)都会失败
内核对象:
进程的句柄表中保存内核对象的地址,因此句柄是内核对象地址在当前进程中的一个索引值。
伪句柄:
GetCurrentProcess和GetCurrentThread获取当前进程或线程的内核对象句柄,但是获取的只是伪句柄
通过伪句柄操作,不会对内核对象的使用计数产生任何影响,例如:CloseHandle时,参数是伪句柄,则忽略并返回FALSE
DuplicateHandle可用于获取复制获取真实的句柄,但是获取到的句柄会递增内核对象的使用计数,因此在使用了该句柄后需要CloseHandle
暂停和恢复:
创建进程、线程时,可传递CREATE_SUSPENDED标志,那么进程或线程初始化成功之后们就会将暂停计数设置为1.
ResumeThread恢复线程
SuspendThread暂停线程(线程想要运行,那么暂停了几次就需要恢复几次)
Sleep(0)放弃当前CPU时间片
Windows线程同步:
用户方式的线程同步机制
1、互锁函数
范围:共享变量或共享内存内的变量。
原子性:保证多线程在同一时刻只能有一个线程获得对该同步变量的操作权限。
InterlockedExchangeAdd
InterlockedIncrement、InterlockedDecrement
InterlockedExchange
InterlockedExchangePointer
InterlockedCompareExchange
InterlockedCompareExchangePointer
2、关键代码段(临界区)
CRITICAL_SECTION cs;
InitializeCriticalSection(&cs);
EnterCriticalSection(&cs);
LeaveCriticalSection(&cs);
DeleteCriticalSection(&cs);
当线程试图进入另一个线程拥有的关键代码段时,调用线程就立即被至于等待状态,
意味着该线程从用户方式转换为内核方式(大约1000个CPU周期)。
因此为了提高代码段的运行性能,可在进入等待之前先用循环锁进行循环,以便设法多次取得该资源,
只有每一次都失败时,该线程才转入内核方式进行等待。
(单处理器时循环置为0)
InitializeCriticalSectionAndSpinCount(&cs, 4000)//第二个参数是循环的次数
SetCriticalSectionSpinCount设置循环次数
内核方式的同步机制:
内核机制的适应性远远优于用户方式,但是调用线程从用户方式转为内核方式速度缓慢,需要大约1000个CPU周期
等待函数,等待对象重置到通知状态
WaitForSingleObject(hProcess, INFINITE)
返回值:WAIT_OBJECT_0/WAIT_TIMEOUT/WAIT_FAILED
WaitForMultipleObject
事件内核对象:
CreateEvent
OpenEvent
SetEvent
ResetEvent
事件运行时处于未通知状态,结束时处于通知状态。
人工重置的事件:人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。需要自己调度ResetEvent
自动重置的事件:等待该事件的线程中只有一个线程变为可调度线程。
(自动重置事件副作用,当线程成功等待到该对象时,自动重置的事件就会自动重置到未通知状态)
等待定时器内核对象:(用户定时器,使用SetTimer进行设置)
在某个时间或按规定的间隔时间发出自己的信号通知的内核对象。
CreateWaitableTimer
OpenWaitableTimer
SetWaitableTimer(最后一个参数用于确定,当定时器时间到来时,是否唤醒暂停的计算机)
信标内核对象:
用于对资源进行计数。
CreateSemaphore
OpenSemaphore
ReleaseSemaphore增加当前资源数量
互斥对象内核对象:
确保线程拥有对单个资源的互斥访问权。
(互斥对象特性与关键代码段相同,但是互斥对象属于内核对象,关键代码段属于用户对象。
这也就意味着,互斥对象的运行速度比关键代码段慢,但是不同进程的多线程能够访问单个互斥对象,
并且线程在等待资源访问时可以设定一个超时值)
CreateMutex
OpenMutex
ReleaseMutex释放互斥对象
失败收获:
互斥对象不同于其它内核对象,拥有“线程所有权”,保持对互斥对象和线程对象的跟踪,本线程拥有的互斥对象只有本线程才能释放。
并且,若本线程结束时未释放互斥对象,那么该互斥对象视为被遗弃,返回WAIT_ABANDONED而非WAIT_OBJECT_0.
“线程所有权”,互斥对象对等待到它的随想保持跟踪
若占用互斥量的对象在释放互斥量之前终止,系统会认为互斥量被遗弃,因为占用它的线程被终止,因此无法释放它。
因为系统会记录所有的互斥量和线程内核对象,因此它确切的知道互斥量何时被遗弃。当互斥量被遗弃的时候,
系统会自动将互斥量的线程ID设为0,将它的递归计数设为0。然后系统检查有没有其他线程正在等待该互斥量。
如果有,那么系统会公平的选择一个正在等待的线程,把对象内部的线程Id设为所选择的那个线程的线程ID,
并将递归计数设为1,这样被选择的线程就变成可调度状态了。
一旦检测到某互斥量被检测到,则WaitForSingleObject返回的不是WAIT_OBJECT_0,而是一个特殊值WAIT_ABANDONED。
返回该值,说明等待的互斥量被某个线程遗弃,同时说明被保护的资源已经被破坏了。
这种情况下,写的程序自己必须决定该怎么做。
同步互斥不一样:
同步:一个线程需要等待另一个线程完成后再运行,是有序的。
互斥:多个线程竞争一个资源,是无序的。
相似的地方:
同一时间只能有一个线程占有该资源。
关键字段和互斥锁,因为“线程所有权”,所以只能在同一个线程中释放。只能用于互斥。
信号量、事件,可以在不同线程中释放,因此不仅可用于互斥,也可用于同步。