第九章:用内核对象进行线程同步

1:同用户模式下的线程同步的区别

可以跨进程进行同步,但速度慢,一个空的系统调用大约占200个CPU周期,但用内核对象进行同步比用户模式同步慢几个数量级的根本原因是伴随调度新线程而来的刷新高速缓存和错过高速缓存
2:等待函数

2.1:WaitForSingleObject(handle,INFINITY)

INFINITY被定义为0xFFFFFFFF,也就是-1

可以通过此函数返回值知道是什么原因等待成功的,WAIT_OBJECT_0等待内核对象被触发,WAIT_TIMEOUT超时,WAIT_FAILED失败(可能是无效句柄)

2.2:

DWORD WINAPI WaitForMultipleObjects(
	DWORD  nCount,              //1到MAXMUM_WAIT_OBJECTS(64)之间
	CONST  HANDLE *lpHandles,
	BOOL   bWaitAll,
	DWORD  dwMilliseconds);

返回值dw=WAIT_OBJECT_0+0表示句柄数组第一个句柄被触发

3:等待成功的副作用
3.1:所谓的副作用,是比如自动重置事件对象,当Wait函数等待到一个已触发的自动重置事件对象时,函数返回,并将这个内核对象设置为未触发,这种额外的工作就是副作用

有些等待有副作用,有些没有

3.2Wait函数是原子操作,如果两个线程在等待两个自动重置事件对象,由于Wait函数的原子性,不会发生等待成功一个,引发其副作用后再等待另外一个的情况,Wait函数的原子性保证要么都等待成功,并引发二者的副作用,要么什么都不改变

3.3:当多个线程等待一个内核对象时,Windows不保证优先级高的线程优先获得内核对象,相似的也不保证等待事件最长的

4:事件内核对象

4.1:人工重置事件对象被触发时,所有等待此内核对象线程都变为可调度状态, 人工重置事件没有副作用

自动重置事件对象被触发时,只有一个等待此内核对象的线程变为可调度状态,自动重置事件对象副作用是:Wait函数返回后将事件内核对象设为非触发状态

4.2:相关函数

HANDLE CreateEvent(
	LPSECURITY_ATTRIBUTES lpEventAttributes,
	BOOL bManualReset,	//是否是人工重置事件对象
	BOOL bInitialState, //初始是否是触发状态
	LPCTSTR lpName);

//与CreateEventEx的区别是CreateEventEx可以让我们以较低的权限打开此内核对象
//而CreateEvent总是要求全部权限

HANDLE OpenEvent(
				 DWORD dwDesiredAccess,
				 BOOL bInheritHandle,
				 LPCTSTR lpName
				 );


SetEvent(HANDLE hd);
ResetEvent(HANDLE hd);
PluseEvent(HANDLE hd);//等价于先SetEvent(),发送脉冲,ResetEvent()

5:题外话~时间设置函数

UINT_PTR SetTimer(HWND hWnd,
	UINT_PTR nIDEvent,			//ID,可以切获WM_TIMER,其wParam就是此ID
	UINT uElapse,			//时间,毫秒
	TIMERPROC lpTimerFunc		//回调函数
	);

//回调函数为
VOID CALLBACK TimerProc(HWND hwnd,
	UINT uMsg,			//值为WM_TIMER
	UINT_PTR idEvent,			//ID
	DWORD dwTime			//时间,毫秒
	);

6:可等待计时器内核对象
6.1:可等待计时器内核对象会在一个指定的时间触发,并以一定的频率触发

6.2:相关函数

HANDLE CreateWaitableTimer(
	LPSECURITY_ATTRIBUTES lpTimerAttributes,
	BOOL bManualReset,
	LPCTSTR lpTimerName
	);

HANDLE OpenWaitableTimer(
	DWORD dwDesiredAccess,
	BOOL bInheritHandle,
	LPCTSTR lpTimerName
	);

BOOL SetWaitableTimer(
	HANDLE hTimer,
	const LARGE_INTEGER* pDueTime,			//第一次触发时间
	LONG lPeriod,					//每隔好多毫秒触发一次,如果为0则只触发一次
							//如果为负值,如-5,则表示5*100纳秒后触发第一次
	PTIMERAPCROUTINE pfnCompletionRoutine,  
	LPVOID lpArgToCompletionRoutine,		
	BOOL fResume					//如果计算机处于挂起模式,此值为TRUE将唤醒计算机
	);
//多次调用hTimer相同的SetWaitableTimer会重新设置此时间,而不必先Cancle它

CancelWaitableTimer(HANDLE hd);

6.3:SetWaitableTimer示例:

HANDLE hdTimer=CreateWaitableTimer(NULL,false,NULL);

SYSTEMTIME st;
st.wYear=2008;
st.wMonth=1;
st.wDay.........;
FILETIME ftTemp,ft;
SystemTimeToFileTime(&st,&ftTemp);
LocalFileTimeToFileTime(&ftTemp,&ft);

LARGE_INTEGER li;
li.LowPart=ft.dwLowDateTime;
li.HighPart=ft.dwHighDateTime;

SetWaitableTimer(hdTimer,&li,1,NULL,NULL,FALSE);

FILETIME和 LARGE_INTEGER二进制结构是一样的,但是必须如上进行赋值,因为LARGE_INTEGER必须对齐到64位边界

6.4:pfnCompletionRoutine和lpArgToCompletionRoutine定义一个APC

6.4.1:pfnCompletionRoutine是回调函数,lpArgToCompletionRoutine是传递给回调函数的参数

回调函数如下

VOID CALLBACK TimerAPCProc(
  LPVOID lpArgToCompletionRoutine, //此参数就是调用SetWaitableTimer的lpArgToCompletionRoutine
  DWORD dwTimerLowValue,
  DWORD dwTimerHighValue
);

当一个可等待计时器触发时,其内核对象先被触发,然后检查是否有APC,如果有,并且调用SetWaitableTimer的线程处于可提醒状态,则将APC函数添加到队列中,如果线程不处于可提醒状态,则不会将APC函数添加到队列,这样可以节约资源

//将计时器被触发的时间转换为SYSTEMTIME
VOID CALLBACK TimerAPCProc(
	LPVOID lpArgToCompletionRoutine, 
	DWORD dwTimerLowValue,
	DWORD dwTimerHighValue
	);
{
	FILETIME   ft1,ft2;
	SYSTEMTIME st;

	ft1.dwLowDateTime=dwTimerLowValue;
	ft1.dwHighDateTime=dwTimerHighValue;
	FileTimeToLocalFileTime(&ft1,&ft2);
	FileTimeToSystemTime(&ft2,st);

}

6.4.2:只有当所有APC队列的函数处理完,回调函数才会返回,因此,我们必须确保在回调函数处理完之后计时器才再次被触发,不然APC加入队列的熟读就快过了他的处理速度,这样会一直占用线程

6.4.3:APC示例

HANDLE hd=CreateWaitableTimer(NULL,TRUE,NULL);

LARGE_INTEGER li={0};
SetWaitableTimer(hd,&li,5000,TimeAPCRoutine,NULL,FALSE);

SleepEx(INFINITY,TRUE);

6.4.4:不应该这样写代码

SetWaitableTimer(...,TimeAPCRoutine,...);
WaitForSingleObjectEx(hTimer,INFINITY,TRUE);
//当计时器触发时,Wait函数返回,线程被唤醒,这使线程退出可等待状态,APC函数则不会被调用

6.4.5:所谓的线程进入可等待状态,是指线程调用了下列函数之一

SleepEx();
WaitForSingleObjectEx();
WaitForMultipleObjectsEx();
MsgWaitForMultipleObjectsEx();
SignalObjectAndWait();

6.5:计时器和SetTimer的区别是SetTimer要占用更多的资源,并且其优先级是最低的,当线程中没有其他消息可供处理时,才会去处理WM_TIMER消息

6.6:信号量
6.6.1:信号量有三个计数:使用计数(这是所有内核对象都有的),最大资源计数,当前可用资源计数

6.6.2:信号量的规则是:

如果当前可用资源计数>0,则信号量被触发

如果当前可用资源计数=0,则信号量未被触发

当前可用资源计数不可能大于最大资源计数,也不可能小于0

6.6.3相关函数

HANDLE CreateSemaphore(
	LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
	LONG lInitialCount,
	LONG lMaximumCount,	//32位值,最大21亿多
	LPCTSTR lpName
	);

//CreateSemaphoreEx();可设置访问权限

HANDLE OpenSemaphore(
	DWORD dwDesiredAccess,
	BOOL bInheritHandle,
	LPCTSTR lpName
	);

//当调用一个Wait函数等待一个信号量时,如果等待成功,则其副作用是将信号量当前可使用资源计数-1
//如果等待失败,则挂起一个线程,当其他线程释放一个信号量,系统会使这些等待的线程其中一个变为可调度状态

BOOL ReleaseSemaphore(
	HANDLE hSemaphore,
	LONG lReleaseCount,		//将一个值加到当前可使用资源计数上,如果传递0或者传递一个lReleaseCount+当前可
							//用资源计数>最大资源计数的值,lpPreviousCount都会被设置为0
							//所以,没有提供一个查询当前可使用资源计数而不改变它的值的函数
	LPLONG lpPreviousCount  //前一个当前可使用资源计数,可以专递NULL
	);

6.7:互斥量

6.7.1:互斥量和关键段很像,互斥量包含一个使用计数,一个线程ID,一个递归计数

6.7.2:互斥量规则:

当线程ID为0时,互斥量处于触发状态

当线程ID不为0时,互斥量处于未触发状态

当互斥量处于未触发状态,调用一个Wait函数,当调用线程ID和互斥量保存的线程ID是统一个是,递增器递归计数

DWORD WaitForInputIdle(
  HANDLE hProcess,
  DWORD dwMilliseconds
);


6.7.3:相关函数

HANDLE CreateMutex(
	LPSECURITY_ATTRIBUTES lpMutexAttributes,
	BOOL bInitialOwner,	//初始状态线程ID是否设置为本线程ID,这样互斥量会处于未触发状态
	LPCTSTR lpName
	);

//CreateMutexEx()可以设置访问权限

HANDLE OpenMutex(
	DWORD dwDesiredAccess,
	BOOL bInheritHandle,
	LPCTSTR lpName
	);

BOOL ReleaseMutex(
	HANDLE hMutex
	);
//当一个线程获得互斥量时,其递归计数为1,每Wait一次其递归计数+1,调用ReleaseMutex()会递减其
//递归计数,当递归计数为0时,会将其线程ID设为0,使互斥量被触发,系统会公平的去使其他在等待此互斥量的线程
//变为可调度状态

6.7.4:互斥量和其他内核对象最大的不同,是当线程ID相同时,即使互斥量未触发,调用Wait函数也不会挂起调用线程,而只会增加其递归计数 

6.7.5:除了互斥量,其他内核对象不会记住自己是那个线程等待成功的,当其他线程调用ReleaseMutex()释放一个不是自己占有的互斥量时,会返回False,调用GetLastError()会返回ERROR_NOT_WONER

6.7.6:如果占有互斥量的线程被意外终止而没有ReleaseMutex(),系统将设置互斥量为被遗弃,其线程ID被设置为0,挂起计数也被设置为0,当互斥量被遗弃时,系统会检查当前是否有线程在等待此互斥量,如果有,系统将此线程设为可调度状态,把互斥量线程ID设为此线程ID,把挂起计数设为1,这些步骤都和以前一样,唯一的不同是:Wait函数返回值不是WAIT_OBJECT_0而是WAIT_ABANDONED,这里是有风险的,因为一般程序都不会去检查此值,如果Wait返回,也许受保护的代码和数据已经被破坏了,我们应该根据实际情况绝对是否有必要去检查此值和做相应的处理
7:WaitForInputIdle

DWORD WaitForInputIdle(
  HANDLE hProcess,
  DWORD dwMilliseconds
);

7.1:此函数会将调用者挂起,等待hProcess指定的进程,知道此进程第一个窗口的线程没有等待处理的输入为止

一般应用为CreateProecss()创建了一个子进程后,父进程等待子进程把窗口创建好,然后返回,这样父进程就能查找到子进程的窗口句柄,这是父进程知道子进程初始化完毕的唯一方法

7.2:此函数另一个用途是:创建一个窗口,我们可能想发送给窗口一些消息,但我们不知道窗口什么时候创建完,我们可以调用此函数等待直到窗口初始化完毕,然后再发送消息给它
8:MsgWaitForMultipleObjects(Ex)
此函数和WaitForMultipleObjects相似,但此函数不仅会等待内核对象,如果有消息到达调用线程所创建的窗口,此函数也会返回

9:WaitForDebugEvent()

此函数为调试器使用,当调试器调试一个进程时,调用此函数的线程会将自己挂起,直到一个调试事件到来,然后此函数返回,调试器就知道发生了调试事件

10:SignalObjectAndWait()

DWORD SignalObjectAndWait(
  HANDLE hObjectToSignal,       //触发的内核对象只能是事件,信号量,互斥量
  HANDLE hObjectToWaitOn,       //等待的对象可以是任何内核对象
  DWORD dwMilliseconds,
  BOOL bAlertable               //表示当线程处于等待状态时,是否能使用APC
);

此函数原子触发一个内核对象并等待另一个内核对象

出于两个原因应该使用此内核对象:

1:这比如下代码性能高很多

ReleaseMutex(...);
WaitForSingleObject(...);

2:以下代码是错误的

//线程A通知线程B开始工作并等待它完成,错误的做法
//线程A
SetEvent(hEvent);
WaitForSingleObject(hOtherEvent,INFINITE);
//线程B
WaitForSingleObject(hEvent,INFINITE);
PulseEvent(hOtherEvent);

//正确的做法
//线程A
SignalObjectAndWait(hEvent,hOtherEvent,INFINITE,FALSE);
//线程B
WaitForSingleObject(hEvent,INFINITE);
PulseEvent(hOtherEvent);

11:使用等待链遍历API来检测死锁

可以采用此方法检测关键段,互斥量,等待进程内核对象和线程内核对象的死锁

这是在vista中提供的方法,书上讲的很详细

你可以使用 Vue Router 的导航守卫来实现外链跳转时携带 token 但不显示在地址栏中。下面是一种可能的解决方案: 1. 首先,确保你已经安装了 Vue Router,并在项目中引入它。 2. 在路由文件中,定义一个全局前置守卫(global before guard),用来拦截所有的外链跳转。 ```javascript import router from './router' router.beforeEach((to, from, next) => { // 判断是否是外链跳转 if (to.meta.externalLink) { // 获取 token const token = 'your_token_here' // 创建一个隐藏的表单,用于提交 token const form = document.createElement('form') form.action = to.path form.method = 'post' // 创建一个隐藏的输入框,存放 token const tokenInput = document.createElement('input') tokenInput.type = 'hidden' tokenInput.name = 'token' tokenInput.value = token // 将输入框添加到表单中 form.appendChild(tokenInput) // 将表单添加到页面中 document.body.appendChild(form) // 提交表单 form.submit() } else { next() } }) ``` 3. 在定义路由时,为外链跳转的路由添加一个 meta 属性,用来标识它是外链跳转。 ```javascript const routes = [ { path: '/external', name: 'ExternalLink', component: ExternalLink, meta: { externalLink: true } }, // 其他路由... ] const router = createRouter({ history: createWebHistory(), routes }) export default router ``` 这样,当用户点击一个带有 `externalLink` meta 属性的链接时,会触发全局前置守卫,创建一个隐藏的表单,将 token 作为参数提交,然后页面会进行跳转,但地址栏中并不会显示 token。 请注意,这只是一种解决方案,具体实现可能因你的项目结构和需求而有所不同。你需要根据实际情况进行调整和优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值