前言

  • SetEvent 和 ResetEvent 是 Windows 操作系统提供的函数,用于操作事件对象。它们属于同步原语函数,用于实现线程同步和互斥。这些函数的类型如下:

    • SetEvent:

      • 声明:BOOL WINAPI SetEvent(HANDLE hEvent);
      • 功能:将指定的事件对象的状态设置为有信号状态,唤醒正在等待该事件的线程,使其可以继续执行。
      • 用途:通常用于通知等待事件的线程可以继续执行,例如在异步操作完成时通知等待线程。
    • ResetEvent:

      • 声明:BOOL WINAPI ResetEvent(HANDLE hEvent);
      • 功能:将指定的事件对象的状态设置为无信号状态,使它不能唤醒等待事件的线程,等待线程将被阻塞。
      • 用途:通常用于重置事件的状态,以便稍后可以通过 SetEvent 再次将其设置为有信号状态。
  • 这些函数通常用于多线程编程,用于协调和同步线程的操作。SetEvent 用于通知线程可以执行,而 ResetEvent 用于阻止线程执行,直到事件被设置为有信号状态。这种机制可以用于实现线程之间的通信和同步,以确保线程按照所需的顺序执行。

1、ResetEvent

代码定义:

WINBASEAPI
BOOL
WINAPI
ResetEvent(
    __in HANDLE hEvent
    );

代码解释:

  • ResetEventWindows API中的一个函数,用于将事件对象(Event)的状态重置为非触发状态。事件是一种用于线程同步的内核对象,它可以处于"有信号"(signaled)或"无信号"(nonsignaled)状态。通常,事件对象被用于线程之间的通信和同步。

函数原型如下:

BOOL WINAPI ResetEvent(
    __in HANDLE hEvent
);

  • 参数解释:

    • hEvent: 要重置的事件对象的句柄。

    • ResetEvent 函数的作用是将指定事件对象的状态从"有信号"重置为"无信号",这意味着线程等待该事件的状态将变为未满足,因此如果有线程在等待该事件,则它们将继续等待,直到该事件再次被触发。

    • 通常情况下,ResetEvent 用于重用事件对象,例如,多个线程等待某个事件的触发,一旦事件被触发,它将执行相关操作,然后通过 ResetEvent 重新将事件状态重置为无信号,以便下一次等待。这有助于实现线程间的协同工作和同步。

请注意,ResetEvent 函数通常与 SetEvent 函数结合使用,SetEvent 用于将事件状态设置为有信号,而 ResetEvent 用于将其重置为无信号。这种组合允许多个线程在事件上等待,当事件被触发后,一个线程执行相关操作,然后通过 ResetEvent 重置事件状态,以便下一个线程等待。

结合实际代码:

int CAuthenManager::GetAccountState(const tstring& tgt)
{
	TRACET();

	if(tgt.empty())
	{
		TRACEE("tgt is empty!");
		return SDOL_ERRORCODE_FAILED;
	}

	TRACED(_T("tgt is [%s]."), tgt.c_str());

	if (m_pSdoBaseHandle == NULL)
	{
		TRACEE("m_pSdoBaseHandle is NULL!");
		return SDOL_ERRORCODE_FAILED;
	}

	ResetEvent(m_hEventLs);

	SetTimeout(GET_TICKET_TIME_OUT, SECOND_CHECK_ACCOUNT_TIME_OUT);

	CTimeRecorder::GetInstance()->RecordStartTime(CallInterface_LoginBySessionIDViaSSO, ::GetTickCount());
	m_nLastActionId = CallInterface_LoginBySessionIDViaSSO;

	if(0 != SdoBase_SetSessionId(m_pSdoBaseHandle,StringHelper::UnicodeToUtf8(tgt).c_str()))
	{
		TRACEW("AM -- a.ss.l set session failed.");
		return FALSE;
	}


	int nError = 0;
	while (true)
	{
		nError = SdoBase_ExtendLoginState2(m_pSdoBaseHandle,StringHelper::UnicodeToUtf8(tgt).c_str());

		if (nError == ERROR_PROCESSING)
		{
			TRACEW("AM -- g.tkt -- Sb is busy, processing another request. Error[%d]", nError);
			Sleep(300);
			continue;
		}
		break;
	}


	SetTimeout(DEFAULT_TIME_OUT, SECOND_DEFAULT_TIME_OUT);

	if (nError != 0)
	{
		TRACEE("AM -- g.tkt -- Calling interface failed. Error[%d]", nError);
		return SDOL_ERRORCODE_FAILED;
	}

	if (WAIT_TIMEOUT == WaitForSingleObject(m_hEventLs, GET_TICKET_TIME_OUT + SECOND_GET_TICKET_TIME_OUT))
	{
		TRACEE("AM -- g.tkt -- timeout!");
		return SDOL_ERRORCODE_GETTICKET_TIMEOUT;
	}

	return get_account_state_error_code;
}

代码解释:

  • 这段C++代码是一个身份认证管理器的实现,用于验证获取用户帐户的状态。以下是代码的主要步骤和逻辑:

    • 如果传入的 tgt(目标)参数为空,会返回 SDOL_ERRORCODE_FAILED,并记录错误消息。

    • 如果 m_pSdoBaseHandle 为 NULL,同样会返回 SDOL_ERRORCODE_FAILED,表示未初始化句柄。

    • 调用 ResetEvent(m_hEventLs) 来重置一个事件对象,用于等待异步操作的完成。接下来,设置了一个超时时间。

    • 调用 SdoBase_SetSessionId 来设置会话 ID,该函数传入了 tgt 的 UTF-8 表示。如果设置会话失败,会返回 FALSE。

    • 进入一个循环,调用 SdoBase_ExtendLoginState2 来检查登录状态,传入 tgt 的 UTF-8 表示。如果返回值为 ERROR_PROCESSING,表示服务忙碌,会等待一段时间后继续尝试,直到成功为止。

    • 设置默认超时时间,然后检查上一步的返回值,如果不为0,表示调用接口失败,返回 SDOL_ERRORCODE_FAILED。

    • 调用 WaitForSingleObject 来等待事件 m_hEventLs 的信号,等待的超时时间是 GET_TICKET_TIME_OUT + SECOND_GET_TICKET_TIME_OUT 毫秒。 如果超时,返回 SDOL_ERRORCODE_GETTICKET_TIMEOUT,否则返回 get_account_state_error_code。

  • 这段代码的目的似乎是执行一系列身份验证和状态检查操作,可能是为了获取用户帐户的认证状态。在代码中,使用了异步等待事件,以及处理不同的错误和超时情况。最后的返回值可能是 get_account_state_error_code 的值,但该值的定义未在提供的代码中找到。

如果没有ResetEvent(m_hEventLs) 会发生什么事情。

  • 如果没有 ResetEvent(m_hEventLs);,事件 m_hEventLs 的状态将保持为有信号状态。这意味着一旦事件被设置为有信号状态(通常是通过 SetEvent 函数),等待事件的线程将立即被唤醒,并可以继续执行。等待事件的线程不会被阻塞,即使事件一直处于有信号状态。

  • 在上下文中,ResetEvent 的目的是确保事件 m_hEventLs 的初始状态为无信号状态,以便等待事件的线程在异步操作开始之前被阻塞。然后,当异步操作完成时,通过 SetEvent 将事件设置为有信号状态,以唤醒等待事件的线程。

  • 如果没有 ResetEvent(m_hEventLs);,那么等待事件的线程可能会在异步操作开始之前被唤醒,这可能导致竞争条件或不确定的行为。因此,正确地初始化事件状态是确保线程同步和协作的重要部分。

    • 竞争条件和不确定的行为可能发生在多线程环境中,其中多个线程尝试访问和修改共享资源,但由于执行顺序的不确定性,可能导致问题。以下是一个简单的示例,说明没有适当同步的情况下可能会发生竞争条件或不确定的行为:

    • 考虑一个具有两个线程的情况,它们都试图访问共享变量 counter:

#include <iostream>
#include <thread>

int counter = 0;

void IncrementCounter() {
    for (int i = 0; i < 1000000; ++i) {
        counter++; // 递增共享变量
    }
}

int main() {
    std::thread t1(IncrementCounter);
    std::thread t2(IncrementCounter);

    t1.join();
    t2.join();

    std::cout << "Final counter value: " << counter << std::endl;
    return 0;
}

  • 在上述示例中,两个线程并行地递增 counter 变量的值。 由于没有使用适当的同步机制,例如互斥锁或信号量,线程之间可能会发生竞争条件,导致不确定的行为。 具体来说,可能会出现以下情况:

    • 一个线程递增 counter 后,另一个线程在读取 counter 之前也递增了它。这可能导致 counter 的值不正确。

    • 由于线程之间的交错执行,最终的 counter 值是不确定的,每次运行的结果都可能不同。

    • 在某些情况下,可能会发生数据竞争,导致程序崩溃或出现不可预测的行为。

  • 为了避免这种情况,应该使用适当的同步机制来确保多个线程之间正确地协同工作,而不会导致竞争条件或不确定的行为。例如,可以使用互斥锁来保护共享资源,以便每次只有一个线程可以访问它,从而避免竞争条件。

    • 在上面的示例中,由于没有适当的同步机制,两个线程并发地递增 counter 变量,可能会导致以下结果之一:

      • counter 的最终值小于 2000000:由于两个线程交错执行,它们同时递增 counter,但其中一个线程的递增操作可能会覆盖另一个线程的结果,导致最终的计数值小于 2000000。

      • counter 的最终值大于 2000000:同样,由于竞争条件,某个线程可能在另一个线程递增之前进行递增操作,导致 counter 的最终值大于 2000000。

      • counter 的最终值等于 2000000:虽然存在竞争条件,但两个线程的操作之间的竞争可能会平均分配,导致 counter 的最终值接近 2000000。

      • 程序可能会崩溃:由于没有适当的同步,数据竞争可能导致程序崩溃或出现不可预测的行为。这种情况可能会因操作系统、编译器和硬件平台的不同而有所不同。

    • 总之,没有适当的同步机制,多线程环境中的循环递增操作可能导致计数值的不确定性,甚至可能引发竞争条件或导致程序的不稳定行为。要确保正确的行为,必须使用适当的同步机制,例如互斥锁,以协调线程的访问。

2、SetEvent

代码定义:

WINBASEAPI
BOOL
WINAPI
SetEvent(
    __in HANDLE hEvent
    );

代码解释:

  • SetEventWindows API中的一个函数,用于将事件对象(Event)的状态设置为有信号状态(signaled)。事件是一种用于线程同步的内核对象,它可以处于"有信号"(signaled)或"无信号"(nonsignaled)状态。通常,事件对象被用于线程之间的通信和同步。

    • 函数原型如下:
BOOL WINAPI SetEvent(
    __in HANDLE hEvent
);

  • 参数解释:

    • hEvent: 要设置为有信号状态的事件对象的句柄。

    • SetEvent 函数的作用是将指定事件对象的状态从"无信号"设置为"有信号",这意味着与该事件相关联的等待线程将立即被唤醒,继续执行。

    • 通常情况下,SetEvent 用于通知等待线程某个事件已经发生,使它们可以继续执行。例如,一个线程可以使用 SetEvent 来通知其他线程某个任务已经完成。一旦事件被设置为有信号,所有正在等待该事件的线程都将被唤醒,然后它们可以执行相应的操作。

    • SetEvent 通常与 ResetEvent 函数结合使用。ResetEvent 用于将事件状态重置为无信号,以便在下一个事件之前等待。这种组合允许多个线程在事件上等待,当事件被触发后,一个线程执行相关操作,然后通过 ResetEvent 重置事件状态,以便下一个线程等待。

  • 请注意,SetEvent 函数不会阻塞当前线程,它只是设置事件的状态。

结合实际代码:

void SDOAPI CAuthenManager::onExtendLoginStateCallback(int nResultCode, const char* szFailReason, SdoBaseHandle* handle)
{
	TRACET();

	//获取检查登录态的结果
	sm_pAuthenManager->get_account_state_error_code=nResultCode;
	::SetEvent(sm_pAuthenManager->m_hEventLs);

	if (nResultCode != 0)
	{
		if (nResultCode >= -10130200 && nResultCode <= -10130100)
		{
			// 来自认证组件中的错误码 [-10130200,-10130100]
			// 会被映射到区间 [-10524200,-10524100]
			nResultCode = nResultCode - 394000;
		}

		if (szFailReason != NULL)
		{
			TRACEW("AM -- o.e.lscb ---%d---%s", nResultCode, szFailReason);
		}

		sm_pAuthenManager->RecordLastTimeSpanAndReport(nResultCode);

		return ;
	}

	sm_pAuthenManager->RecordLastTimeSpanAndReport(nResultCode);

	string strSessionId = sm_pAuthenManager->GetCurrentSessionId();
	sm_pAuthenManager->SetSSOCookieData(StringHelper::ANSIToUnicode(strSessionId));
	
}

代码解释:

  • 这段代码是 CAuthenManager 类的一个成员函数,用于处理检查登录态的回调。以下是代码的主要步骤和逻辑:

    • 获取检查登录态的结果码 nResultCode,这个结果码将在后续的逻辑中使用。这个结果码表示登录态检查的结果。

    • 使用 ::SetEvent 函数设置 sm_pAuthenManager 对象的成员变量 m_hEventLs,这是一个事件对象,用于通知等待事件的线程。通过设置事件,通知主调函数(在前一个代码段中)等待的线程可以继续执行。

    • 如果 nResultCode 不等于0,表示登录态检查失败,进入错误处理分支。

    • 如果 nResultCode 在某个范围内,将其映射到另一个范围。这是一种错误码的映射处理。

    • 如果 szFailReason 不为NULL,记录错误消息。

    • 调用 sm_pAuthenManager 的 RecordLastTimeSpanAndReport 函数,可能用于记录和报告错误。

    • 如果 nResultCode 等于0,表示登录态检查成功,进入成功处理分支。

    • 调用 sm_pAuthenManager 的 RecordLastTimeSpanAndReport 函数,可能用于记录和报告成功。

    • 获取当前会话的会话ID(strSessionId),并将其设置为 SSOCookie 数据。

  • 这段代码处理了登录态检查的回调结果,根据结果码的不同执行不同的逻辑。成功的情况下,会设置事件以通知等待的线程,而失败的情况下会记录错误信息和处理错误码。

    • 在前一个代码段中设置异步是为了执行后台的异步操作,以防止阻塞主线程。主要的原因是某些操作可能需要等待网络请求或其他耗时操作的结果,而如果在主线程中同步执行这些操作,会导致应用程序的界面无响应,给用户带来不好的体验。

      • ResetEventSetEvent 函数本身不会阻塞线程。它们是用于线程同步的事件对象的操作函数。

        • ResetEvent:ResetEvent 用于将事件对象的状态设置为非信号状态(无信号),这不会阻塞线程。当事件对象处于非信号状态时,线程在调用等待函数如 WaitForSingleObject 时,如果事件仍然是非信号状态,线程将会被阻塞,等待事件变为有信号状态。

        • SetEvent:SetEvent 用于将事件对象的状态设置为信号状态(有信号),同样它本身不会阻塞线程。当事件对象变为有信号状态时,任何线程在等待这个事件的时候(使用等待函数),会被唤醒,继续执行。

      • 所以,ResetEventSetEvent 主要用于线程同步,它们的阻塞效果是在等待函数(如 WaitForSingleObject)中体现的,而不是在它们自身的调用中。这两个函数是协调线程之间的操作,以实现正确的同步行为。

        • 当一个线程调用等待函数(如 WaitForSingleObject)等待一个事件对象时,如果事件对象的状态是非信号状态,那么线程将被阻塞,即暂时停止执行,等待事件变为有信号状态。

          • 事件对象的状态可以是两种:

            • 信号状态(Signaled):表示事件已经发生或就绪,可以通知等待的线程继续执行。当事件对象处于信号状态时,等待它的线程会被唤醒,继续执行相关的逻辑。

            • 非信号状态(Non-Signaled):表示事件尚未发生或尚未就绪,等待的线程需要等待事件变为信号状态才能继续执行。当事件对象处于非信号状态时,等待它的线程会被阻塞,暂停执行,直到事件被设置为信号状态。

        • 所以,当一个线程等待一个事件对象,并且事件对象处于非信号状态时,线程将被阻塞,直到其他线程调用 SetEvent 来将事件设置为信号状态,此时等待的线程会被唤醒,继续执行。这是一种线程同步的机制,用于协调多个线程的操作。

      • 如果没有 ResetEventSetEvent,但有 WaitForSingleObject 调用,那么 WaitForSingleObject 会一直等待,因为事件对象的状态不会发生改变。具体情况取决于事件对象的初始状态:

        • 如果事件对象的初始状态是非信号状态,那么调用 WaitForSingleObject 会立即阻塞,线程将一直等待,直到其他某个线程调用 SetEvent 将事件对象的状态设置为信号状态。

        • 如果事件对象的初始状态是信号状态,那么调用 WaitForSingleObject 不会阻塞,线程将立即继续执行,不需要等待。

        • 在没有 ResetEventSetEvent 的情况下,WaitForSingleObject 只是对事件对象的当前状态进行检查,并根据该状态来决定是否阻塞线程。如果事件对象的状态一直保持不变,那么 WaitForSingleObject 将一直等待下去,可能导致线程挂起。

      • 通常情况下,ResetEvent 用于初始化事件对象的状态,将其设置为非信号状态,然后等待的线程可以通过 WaitForSingleObject 阻塞等待事件变为有信号状态。SetEvent 用于在某个条件得到满足时将事件设置为信号状态,唤醒等待的线程,使它们可以继续执行。这是一种用于线程同步的常见机制。

    • 在这个特定的情况中,CAuthenManager::GetAccountState 函数中的异步操作是等待登录状态的检查,这个操作可能需要一些时间才能完成。因此,通过将异步操作设置为等待事件 m_hEventLs,该函数可以在等待的过程中允许主线程或其他线程继续执行,而不会被阻塞。

    • 一旦异步操作完成,通过 ::SetEvent(sm_pAuthenManager->m_hEventLs),事件会被触发,通知主线程或等待的线程,以便它们可以继续执行相关的逻辑。

    • 这种异步操作的设置允许应用程序保持响应性,同时执行需要一些时间的操作,以提高用户体验。

  • onExtendLoginStateCallback 是一个回调函数,它的目的是处理异步操作的结果,并在需要的时候触发事件 sm_pAuthenManager->m_hEventLs 以通知等待事件的线程可以继续执行。如果没有 ::SetEvent(sm_pAuthenManager->m_hEventLs); 这段代码,等待事件的线程将不会被通知事件的触发,因此不会执行后续逻辑。

  • 具体来说,如果没有这段代码,onExtendLoginStateCallback 回调函数将仍然执行,并会设置结果码 nResultCode,但等待事件的线程将一直等待,不会被唤醒。因此,后续逻辑不会被执行,等待事件的线程将一直阻塞在等待事件的地方,导致应用程序无法继续执行所需的操作。

  • 所以,::SetEvent(sm_pAuthenManager->m_hEventLs); 这段代码是确保等待事件的线程能够被唤醒并执行后续逻辑的关键部分。

3、WaitForSingleObject

代码定义:

WINBASEAPI
DWORD
WINAPI
WaitForSingleObject(
    __in HANDLE hHandle,
    __in DWORD dwMilliseconds
    );

代码解释:

  • WaitForSingleObjectWindows API 中用于等待一个内核对象(如事件、互斥、信号量等)的函数。它会阻塞当前线程,直到指定的内核对象处于有信号状态或者等待超时。
    • 该函数的参数包括:

      • hHandle:要等待的内核对象的句柄,可以是事件、互斥、信号量等。
      • dwMilliseconds:等待的超时时间(以毫秒为单位)。如果为零,表示立即返回,不等待;如果为 INFINITE,表示永久等待,直到对象变为有信号状态。
    • WaitForSingleObject 的返回值包括:

      • WAIT_OBJECT_0:对象变为有信号状态。
      • WAIT_ABANDONED:对象为互斥对象,且之前拥有该互斥对象的线程意外终止,导致互斥对象的计数不再准确。通常情 况下,应用程序需要谨慎处理此返回值。
      • WAIT_TIMEOUT:等待超时,对象未变为有信号状态。
      • WAIT_FAILED:等待失败,可以调用 GetLastError 函数获取错误代码。
    • WaitForSingleObject 是多线程编程中用于线程同步的重要函数之一。它允许一个线程等待另一个线程通知某个事件的发生或者等待某个资源的可用性。通过合理使用超时参数和返回值,可以实现不同的等待策略,例如等待直到对象变为有信号状态、等待一段时间后超时返回、等待一段时间后轮询等。

举个例子:

	if (WAIT_TIMEOUT == WaitForSingleObject(m_hEventLs, GET_TICKET_TIME_OUT + SECOND_GET_TICKET_TIME_OUT))
	{
		TRACEE("AM -- g.tkt -- timeout!");
		return SDOL_ERRORCODE_GETTICKET_TIMEOUT;
	}

	return get_account_state_error_code;
  • 代码解释:

    • 这段代码的逻辑是首先等待事件对象 m_hEventLs,如果等待超时(即事件未在规定时间内触发),就会执行以下逻辑:

    • 打印错误消息,使用 TRACEE 函数输出 “AM – g.tkt – timeout!”,表示发生了等待超时。

    • 返回错误代码 SDOL_ERRORCODE_GETTICKET_TIMEOUT,表示等待超时错误。

    • 如果事件在规定时间内触发,WaitForSingleObject 不返回 WAIT_TIMEOUT,那么代码会跳过上述两个步骤,直接执行 return get_account_state_error_code;,返回 get_account_state_error_code,这个值可能是在 onExtendLoginStateCallback 回调函数中设置的。

  • 这段代码的作用是等待事件,如果事件超时未触发,则返回一个特定的错误代码,否则返回 get_account_state_error_code 的值,这个值可能在事件触发后的回调函数中设置。

4、WaitForMultipleObjects

代码定义:

WINBASEAPI
DWORD
WINAPI
WaitForMultipleObjects(
    __in DWORD nCount,
    __in_ecount(nCount) CONST HANDLE *lpHandles,
    __in BOOL bWaitAll,
    __in DWORD dwMilliseconds
    );

代码解释:

  • WaitForMultipleObjects 也是 Windows API 中用于等待多个内核对象的函数,它允许一个线程等待多个对象中的任何一个或全部。具体而言,它等待一个对象数组中的多个内核对象之一或所有内核对象都变为有信号状态。

    • 该函数的参数包括:

      • nCount:要等待的内核对象句柄数组中的对象数目。
      • lpHandles:一个包含要等待的内核对象句柄的数组。
      • bWaitAll:一个布尔值,如果为 TRUE,则只有当数组中的所有对象都变为有信号状态时,函数才会返回;如果为 FALSE,则只要数组中的任何一个对象变为有信号状态,函数就会返回。
      • dwMilliseconds:等待的超时时间(以毫秒为单位)。如果为零,表示立即返回,不等待;如果为 INFINITE,表示永久等待,直到对象变为有信号状态。
    • WaitForMultipleObjects 的返回值包括:

      • WAIT_OBJECT_0 到 WAIT_OBJECT_0 + nCount - 1:对象数组中的某个对象变为有信号状态。如果 bWaitAll 为 FALSE,则返回的值表示数组中的哪个对象发生了变化;如果 bWaitAll 为 TRUE,则返回 WAIT_OBJECT_0。
      • WAIT_TIMEOUT:等待超时,对象数组中的任何一个对象都未变为有信号状态。
      • WAIT_FAILED:等待失败,可以调用 GetLastError 函数获取错误代码。
    • 通过合理使用参数,可以实现多种等待策略,如等待任何一个对象变为有信号状态、等待所有对象都变为有信号状态、等待一段时间后超时返回等。这个函数在多线程编程和处理多个并发事件时非常有用。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐