线程同步

线程同步的方法:使用临界区对象,互斥函数,事件,信号量,互斥

临界区对象
  • 临界区对象是定义在数据段中的一个 CRITICAL_SECTION 结构
    • void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection );
    • // 指向数据段中定义的CRITICAL_SECTION 结构
  • 线程访问临界区中数据的时候,必须首先调用EnterCriticalSection 函数,申请进入临界区(又叫关键代码段),如果有另一个线程在临界区,EnterCriticalSection 函数会一直等待下去,直到其他线程离开临界区才返回
    • void EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection);
  • 操作完成的时候,还要将临界区交还给 Windows,这个工作由LeaveCriticalSection 函数来完成。
    • void LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection);
  • 当程序不再使用临界区对象的时候,必须使用DeleteCriticalSection 函数将它删除。
    • void DeleteCriticalSection( LPCRITICAL_SECTION lpCriticalSection);
  • 举例:两个线程同时使两个全局变量自增。
  • #include <stdio.h>
    #include <windows.h>
    #include <process.h>
    
    BOOL g_bContinue = TRUE;
    int g_nCount1 = 0;
    int g_nCount2 = 0;
    CRITICAL_SECTION g_cs; // 对存在同步问题的代码段使用临界区对象
    
    UINT __stdcall ThreadFunc(LPVOID);
    
    int main(int argc, char* argv[])
    {
    	UINT uId;
    	HANDLE h[2];
    	
    	// 初始化临界区对象
    	::InitializeCriticalSection(&g_cs);
    	
    	h[0] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
    	h[1] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
    	
    	// 等待1秒后通知两个计数线程结束,关闭句柄
    	Sleep(1000);
    	g_bContinue = FALSE;
    	::WaitForMultipleObjects(2, h, TRUE, INFINITE);
    	::CloseHandle(h[0]);
    	::CloseHandle(h[1]);
    	
    	// 删除临界区对象
    	::DeleteCriticalSection(&g_cs);
    	
    	printf("g_nCount1 = %d \n", g_nCount1);
    	printf("g_nCount2 = %d \n", g_nCount2);
    	
    	return 0;
    }
    
    UINT __stdcall ThreadFunc(LPVOID)
    {
    	while(g_bContinue)
    	{
    		::EnterCriticalSection(&g_cs);
    		g_nCount1++;
    		g_nCount2++;
    		::LeaveCriticalSection(&g_cs);
    	}
    	return 0;
    }
  • PS: __stdcall 是新标准C/C++函数的调用方法。从底层上说,使用这种调用方法参数的进栈顺序和标准C 调用(_cdecl 方法)是一样的,都是从右到左,但是__stdcall 采用自动清栈的方式,而_cdecl 采用的是手工清栈方式。Windows 规定,凡是由它来负责调用的函数都必须定义为__stdcall 类型。注意,如果没有显式说明的话,函数的调用方法是_cdecl。
  • 临界区对象能够很好地保护共享数据,但是它不能够用于进程之间资源的锁定,因为它不是内核对象。如果要在进程间维持线程的同步可以使用事件内核对象。
互斥函数
  • 互锁函数为同步访问多线程共享变量提供了一个简单的机制。如果变量在共享内存,不同进程的线程也可以使用此机制。用于互锁的函数有 InterlockedIncrement、InterlockedDecrement、InterlockedExchangeAdd、InterlockedExchangePointer 等
    • l InterlockedIncrement 函数递增(加1)指定的32 位变量。这个函数可以阻止其他线程同时使用此变量,函数原型如
      • LONGInterlockedDecrement( LONG volatile* Addend); // 指向要递减的变量
    • l InterlockedDecrement 函数同步递减(减1)指定的32 位变量,原型如下。
      • LONGInterlockedIncrement( LONG volatile* Addend); // 指向要递增的变量
    • 例如
    • UINT __stdcall ThreadFunc(LPVOID) // 03InterlockDemo 工程下
      { 
        while(g_bContinue)
          { ::InterlockedIncrement((long*)&g_nCount1);
          ::InterlockedIncrement((long*)&g_nCount2);
        }
        return 0;
      }
事件内核对象
  • 事件内核对象用于主线程和工作线程的通信。事件对象(event)是一种抽象的对象,它也有未受信(nonsignaled)和受信(signaled)两种状态,编程人员也可以使用WaitForSingleObject 函数等待其变成受信状态。
  • 事件对象包含3 个成员:nUsageCount (使用计数)、bManualReset(是否人工重置)和bSignaled(是否受信)。
    • 成员 nUsageCount 记录了当前的使用计数,初始状态为1,当使用计数为0 的时候,Windows 就会销毁此内核对象占用的资源;
    • 成员 bManualReset 指定在一个事件内核对象上等待的函数返回之后,Windows 是否重置这个对象为未受信状态;
    • 成员 bSignaled 指定当前事件内核对象是否受信。下面要介绍的操作事件内核对象的函数会影响这些成员的值。
  • 基本函数
    • HANDLE CreateEvent(
    •   LPSECURITY_ATTRIBUTES lpEventAttributes, // 用来定义事件对象的安全属性
    •   BOOL bManualReset,  // 指定是否需要手动重置事件对象为未受信状态。
    •   BOOL bInitialState,      // 指定事件对象创建时的初始状态
    •   LPCWSTR lpName);     // 事件对象的名称
      • 参数 bManualReset 对应着内核对象中的 bManualReset 成员。
      • 自动重置(auto-reset)和人工重置(manual-reset)是事件内核对象两种不同的类型。
        • 当一个人工重置的事件对象受信以后,所有等待在这个事件上的线程都会变为可调度状态;
        • 可是当一个自动重置的事件对象受信以后,Windows 仅允许一个等待在该事件上的线程变成可调度状态,然后就自动重置此事件对象为未受信状态。
      • bInitialState 参数对应着 bSignaled 成员。将它设为TRUE,则表示事件对象创建时的初始化状态为受信(bSignaled = TRUE);设为FALSE 时,状态为未受信(bSignaled = FALSE)。
      • lpName 参数用来指定事件对象的名称。
    • HANDLE OpenEvent (
    •   DWORD dwDesiredAccess, // 指定想要的访问权限
    •   BOOL bInheritHandle, // 指定返回句柄是否可被继承
    •   LPCWSTR lpName); // 要打开的事件对象的名称
    • 不使用此内核对象的时候,应该调用 CloseHandle 函数释放它占用的资源。
    • 事件对象被建立后,程序可以通过SetEvent 和ResetEvent 函数来设置它的状态。
      • BOOL SetEvent( HANDLE hEvent ); // 将事件状态设为 “受信(sigaled)”;
      • BOOL ResetEvent(HANDLE hEvent ); // 将事件状态设为 “未受信(nonsigaled)”;
    • 当在这样的事件对象上等待的函数(比如,WaitForSingleObject 函数)返回时,Windows 会自动重置事件
      对象为未受信状态。
    • 举例:主线程通过将事件状态设为“受信”来通知子线程开始工作。
    • #include <stdio.h>
      #include <windows.h>
      #include <process.h>
      
      HANDLE g_hEvent;
      UINT __stdcall ChildFunc(LPVOID);
      
      int main(int argc, char* argv[])
      {
      	HANDLE hChildThread;
      	UINT uId;
      
      	// 创建一个自动重置的(auto-reset events),未受信的(nonsignaled)事件内核对象
      	g_hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL);
      
      	hChildThread = (HANDLE)::_beginthreadex(NULL, 0, ChildFunc, NULL, 0, &uId);
      
      	// 通知子线程开始工作
      	printf("Please input a char to tell the Child Thread to work: \n");
      	getchar();
      	::SetEvent(g_hEvent);
      
      	// 等待子线程完成工作,释放资源
      	::WaitForSingleObject(hChildThread, INFINITE);
      	printf("All the work has been finished. \n");
      	::CloseHandle(hChildThread);
      	::CloseHandle(g_hEvent);
      	return 0;
      }
      
      UINT __stdcall ChildFunc(LPVOID)
      {
      	::WaitForSingleObject(g_hEvent, INFINITE);
      	printf("  Child thread is working...... \n");
      	::Sleep(5*1000); // 暂停5秒,模拟真正的工作
      	return 0;
      }
信号量内核对象
  • 信号量 (Semaphore) 内核对象对线程的同步方式与前面几种方法不同,它允许多个线程在同一时刻访问同一资源,但是需要限制同一时刻访问的最大线程数目。
  • 主要用到的函数:CreateSemaphore、OpenSemaphore、ReleaseSemaphore、WaitForSingleObject、WaitForMultipleObject。
    • CreateSemaphore(  
      • LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,    // 安全属性指针
      • LONG lInitialCount,                                                  // 初始计数
      • LONG lMaximumCount,                                           // 最大计数,不超过4294967295
      • LPCTSTR lpName)                                                    // 对象名指针
    • HANDLE OpenSemaphore(
      • DWORD dwDesiredAccess, // 访问标志
      • BOOL bInheritHandle,// 继承标志
      • LPCTSTR lpName)// 信号量名
    • BOOL ReleaseSemaphore(
      • HANDLE hSemaphore, // 信号量句柄
      • LONG lReleaseCount,// 计数递增数量
      • LPLONG lpPreviousCount)// 先前计数
    • WaitForSingleObject、WaitForMultipleObject 主要是用在视图进入共享资源的线程函数入口处,主要用来判断信号量的当前可用资源计数是否允许本线程的进入。只有在当前可用资源计数值大于0时,被监视的信号量内核对象才会得到通知。
互斥内核对象
  • 互斥内核对象能够保证多个线程对同一共享资源的互斥访问。
  • 互斥内核对象的主要函数:CreateMutex, OpenMutex, ReleaseMutex, WaitforSingleOject, WaitForMultipleObjects等。在使用互斥对象前,首先言通过 CreateMutex 或 OpenMutex 创建或打开一个互斥对象。
    • HANDLE CreateMutex(
      • LPSECURITY_ATTRIBUTES lpMutexAttributes,// 安全属性指针
      • BOOL bInitialOwner,// 初始拥有者
      • LPCTSTR lpName)// 互斥对象名
        • bInitialOwner 一般将它初始为 false,表示无线程占有
    • HANDLE OpenMutex(
      • DWORD dwDesiredAccess// 访问标志
      • BOOL bInheritHandle,// 继承标志
      • LPCTSTR lpName)// 互斥对象名
    • BOOL ReleaseMutex(HANDLE hMutex);
      • 当前访问资源的线程不再访问次资源而离开时,释放互斥对象
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值