您的位置:首页 > 其它

线程同步

2013-11-28 23:44 330 查看
一、用户方式中的线程同步

1.互锁函数

       可以使用InterlockedExchangeAdd函数对一个长变量以原子操作的方式递增一个值:

LONG InterlockedExchangedAdd(
PLONG plAddend,
LONG lIncrement
);

      可以使用InterlockedExchange和InterlockedExchangePointer函数以原子操作的方式用第二个参数的值替换第一个参数传递个当前值,两个函数都返回第一个函数的原始值。两个函数运用在32位的应用程序,都能用32位的替换32位。用在64位的应用程序,InterlockedExchange函数用于32位,InterlockedExchangePointer函数用于取代64位值,函数如下:

LONG InterlockedExchange(
PLONG plTarget,
LONG lValue
);
PVOID InterlockedExchangePointer(
PVOID *ppvTarget,
PVOID pvValue
);

      可以使用函数InterlockedCompareExchange和InterlockedCompareExchangePointer以原子访问的方式将第一个参数传递的当前值与第三个参数进行比较,如果值相同,用第二个参数的值替换当前值,如果不相同,保持当前值不变。两个函数都返回原始值。两个函数都能运行在32位的应用程序中。在64位的应用程序中,InterlockedCompareExchange用于比较32位的值,InterlockedCompareExchangePointer用于64位,函数如下:

PVOID InterlockedCompareExchange(
PLONG plDestination,
LONG lExchange,
LONG lCommand
);
PVOID InterlockedCompareExchangePointer(
PVOID *ppvDestination,
PVOID pvExchange,
PVOID pvCommand
);

      两个比较老的函数,可与运用上面的函数来实现。分别以原子操作的方式递增或递减1修改长整数的函数:

LONG InterlockedIncrement(PLONG plAddend);
LONG InterlockedDecrement(PLONG plAddend);

2.高速缓存行

 当有多个CPU时,高速缓存行可能造成同步问题。例如下面这个数据结构就是非常差:

struct CUSTINFO{
DWORD dwCustorID;// 大部分读
int c;  //读写
char Name[100];//大部分读
FILETIEM Data;//大部分写
};

因此为了同步问题,可以用于下面的方式改进:

// Determine the cache line size for the host CPU.
//为各种CPU定义告诉缓存行大小
#ifdef _X86_
#define CACHE_ALIGN  32
#endif
#ifdef _ALPHA_
#define CACHE_ALIGN  64
#endif
#ifdef _IA64_
#define CACHE_ALIGN  ??
#endif

#define CACHE_PAD(Name, BytesSoFar) \
BYTE Name[CACHE_ALIGN - ((BytesSoFar) % CACHE_ALIGN)]

struct CUSTINFO
{
DWORD    dwCustorID;     // Mostly read-only
char     Name[100];      // Mostly read-only

CACHE_PAD(bPad1, sizeof(DWORD) + 100);

int      c;      // Read-write
FILETIME Date;  // Read-write

CACHE_PAD(bPad2, sizeof(int) + sizeof(FILETIME));
};

3.关键代码段 

        在使用关键代码段前要先定义CRITICAL_SECTION结构体变量。然后要运用InitializeCriticalSection函数对CRITICAL_SECTION结构进行初始化:

VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);

        在编写使用共享资源的代码时,要在代码前加入下面的函数:  

VOID EnterCriticalSection(PCRITICAL_SECTION pcs);

         EnterCriticalSection函数会查看CRITICAL_SECTION结构的成员变量。这些变量指明哪个变量正在使用该资源,其会负责以下测试,这些测试都是以原子操作方式执行:

如果没有线程访问该资源,那么该函数就会更新成员变量,以指明该资源以被赋予访问权,然后该函数返回,线程继续运行。
如果成员变量指明调用线程已经获取了该资源的访问权,那么该函数会更新这些变量,以表明调用线程被赋予访问权多少次,然后该函数会返回,线程继续运行。一个线程多少次赋予访问权,就需要调用多次Leave函数才能释放该资源。
如果成员变量指明另一个线程已经被赋予了对该资源的访问权,那么调用线程就会进入等待状态。系统会记住该线程想获取该资源的访问权,并更新CRITICAL_SECTION结构成员变量,当线程释放该资源时,等待线程变为可调用状态。

        可以使用TryEnterCritcalSection函数来代替EnterCriticalSection函数,该函数会进行相同的测试,如果能够访问共享资源,其会返回TRUE,线程获取访问权,继续执行,释放该资源需要调用LeaveCriticalSection函数;如果不能获取访问共享资源的访问权,那么会返回FLASE,但是这与EnterCriticalSection函数不同,该函数不会让调用线程进入等待状态。其形式如下:

BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs);

         在共享代码结尾,需要使用LeaveCriticalSection函数。该函数会查看CRITICAL_SECTION结构的成员变量,将计数减1。如果计数为0,那么就会查看是否有其他线程调用EnterCriticalSection函数处于等待状态,如果有,那么就会更新成员变量,该线程处于可调度状态;如果没有,那么该函数更新成员变量,以表明没有线程正在访问该共享资源;如果计数不为0,该函数只会返回。函数形式如下:

VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);

        线程当无法获取共享资源时,会进入等待状态,会由用户方式进入内核方式,这样会付出时间和资源代价。因此,windows引入了将循环锁加入了代码段,我们可以运用InitializeCriticalSectionAndSpinCount函数来实现,其形式如下:

BOOL InitializeCriticalSectionAndSpinCount(
PCRITICAL_SECTION pcs,
DWORD dwSpinCount  //用于设置循环的次数
);

        可以使用SetCriticalSectionSpinCount函数来改变关键代码段循环次数:

BOOL SetCriticalSectionSpinCount(
PCRITICAL_SECTION pcs,
DWORD dwSpinCount  //用于设置循环的次数
);

       使用关键代码段的技巧:

每个共享资源使用一个CRITICAL_SECTION变量;
同时访问多个资源时,可以设置多个CRITICAL_SECTION变量;
不要长时间允许关键代码段
二、内核对象实现线程同步

       尽管用户方式相对于内核对象方式的线程同步速度快,但其有许多局限和不足:例如互锁函数只能用于单值;关键代码段只能在单个进程中线程同步,并且容易死锁。
1、等待函数

       等待函数可以让函数进入等待状态,知道等待的内核对象变为已通知状态。等待单个内核对象函数WaitForSingleObject:

DWORD WaitForSingleObject(
HANDLE hObject,
DWORD dwMilliseconds
);

hObject:其为能够变为未通知/通知状态的内核对象。
dwMilliseconds:其为等待函数等待的时间。如果其为INFINITE表示等待函数一直等待,除非内核对象变为已通知;也可以设置为其他值(以毫秒为单位),如果到等待时间结束前,内核对象还没变为已通知,那么就不会再等待,执行调用线程。
返回值:WAIT_OBJECT_0:表示内核对象已变为通知状态;WAIT_OTIMEOUT:表示超时;WAIT_FAILED:表示错误
      可以使用WaitForMultipleObjects函数等待多个内核对象,WaitForMultipleObjects以原子操作的方式检查内核对象的状态:

DWORD WaitForSingleObject(
DOWRD dwCount,
CONST HANDLE *phObjects,
BOOL fWaitAll,
DWORD dwMilliseconds
);

dwCount:表示等待的内核对象的数目
phObjects:等待内核对象句柄数组
fWaitAll:设为TRUE,表示所有内核对象都变为已通知状态才能返回;为FALSE,表示只要有一个内核对象变为已通知状态,就可以返回
dwMillisecond:等待时间。同WaitForSingleObject
返回值:WAIT_OTIMEOUT:表示超时;WAIT_FAILED:表示错误;如果fWaitAll为TRUE,并且所有的对象变为已通知状态,返回WAIT_OBJECT_0;如果fWaitAll为FALSE,如果一个内核对象变为已通知,会返回WAIT_OBJECT_0到WAIT_OBJECT_0+dwCount-1之间的一个值,表示已变为通知状态的内核对象在内核对象句柄数组中的下标。
2.事件内核对象

        事件内核对象包括一个引用计数、一个用于标识是自动重置还是人工重置的布尔值、一个用于标识是已通知状态还是未通知状态的布尔值。

        事件内核对象有两种类型:人工重置类型和自动重置类型。如果人工重置事件内核对象得到通知时,等待该事件的所有线程都变为可调度状态;如果是自动重置,等待该事件的线程只有一个线程能够变为可调度状态。

        通过函数CreateEvent函数创建事件内核对象:

HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa,
BOOL fManuaReset,
BOOL bInitialState,
PCTSTR  pszName
);

psa:设置内核对象的安全性;
fManuaReset:设置事件内核对象的类型:人工重置和自动重置。TRUE为人工重置,FALSE为自动重置;
fInitialState:为初始化事件对象的状态:TRUE为已通知状态,FALSE为未通知状态。
pszName:给事件对象命名。
       可以使用OpenEvent函数来打开已经存在的时间内核对象:

HANDLE OpenEvent(
DWORD fdwAcess,
BOOL fInherit,
PCTSTR pszName
);

       可以通过函数SetEvent将事件设为已通知状态,通过函数ResetEvent函数将事件设为未通知状态:

BOOL SetEvent(HANDLE hEvent);
BOOL ResetEvent(HANDLE hEvent);

       自动重置事件相对于人工重置事件有等待成功副作用:当成功等待该对象时,该对象将由已通知状态变为未通知状态。
       内核对象都需要调用CloseHandle函数来释放该进程使用的内核对象。

        PulseEvent函数可以让事件变为已通知状态,然后立即又变为未通知状态。其可以等待线程变为可调度。

BOOL PulseEvent(HANDLE hEvent);

3.等待定时器内核对象

        等待定时器内核对象就是在某个时间或按时间间隔发出通知的内核对象。等待定时器和事件一样,也分为人工重置定时器和自动重置定时器:如果人工重置定时器发出通知时,所有等待线程都处于可调度状态;如果自动重置定时器发出通知,那么等待线程中的一个线程可以处于可调度状态。

       可以使用函数CreateWaitableTimer创建等待定时器和使用函数OpenWaitableTimer函数打开等待定时器,函数如下:

HANDLE CreateWaitableTimer(
PSECURITY_ATTRIBUTES psa,
BOOL fManualReset,
PCTSTR pszName
);
HANDLE OpenWaitableTimer(
DWOD dwDesiredAccess,
BOOL fInherit,
PCTSTR pszName
);

       等待定时器总是在未通知状态下创建的,因此需要的调用SetWaitableTimer函数来设置定时器在何时成为已通知状态,函数如下:

BOOL SetWaitableTimer(
HANDLE hTimer,
const LARGE_INTEGER *pDueTime,
LONG lPeriod,
PTIMERAPCROUTINE pfnCompletionRoutine,
PVOID pvArgToCompletionRoutine,
BOOL fResume
);

hTimer:等待定时器句柄
pDueTime:指明定时器第一次何时报时
lPeriod:指明定时器隔多久后再次报时
pfnCompletionRoutine:回调函数地址
pvArgToCompletionRoutine:参数
回调函数的形式:

VOID APIENTRY name(PVOID pfnCompletionRoutine,DWORD dwTimerLowValue,DWORD dwTimerHeighValue)
{
}

        可以调用函数CancelWaitableTimer函数撤销定时器报时:

BOOL CancelWaitableTimer(HANDLE hTimer);

4.信标内核对象

        信标内核对象用于对资源计数。其包括一个引用计数、一个32位值用于表示最大资源数、一个32位值表示当前资源数。当前资源数大于0,小于最大资源数,且不为0时,发出信标信号。当前资源数为0时,不发出信标信号;当前资源数不能为负值,且当前资源数不能大于最大资源数。

       使用函数CreateSemahore创建信标内核对象,使用OpenSemahore函数打开一个已经存在的信标内核对象,函数如下:

HANDLE CreateSemaphore(
PSECURITY_ATTRIBUTE psa,
LONG lInitialCount,
LONG iMaximumCount,
PCTSTR pszName
);
HANDLE OpenSemaphore(
DW0RD fdwAccess,
BOOL bInheritHandle,
PCTSTR pszName
);

         等待函数对于信标内核对象的副作用:信标的当前资源数减1.

         可以使用函数ReleaseSemphore函数对当前资源数递增:  

LONG ReleaseSemaphore(
LONG lsem,
LONG lReleaseCount,//添加的资源数
PLONG plPreviousCount//原始当前资源数
);

注意:如果不对当前资源做修改的话,无法获取资源数。如果调用ReleaseSemphore法术的第二个参数设为0,第三个参数会返回0;如果设为一个大数,大于最大资源数,也会返回0.
5.互斥内核对象

        互斥内核对象包括一个引用计数、一个线程ID和一个计数器。互斥内核对象和关键代码的特性比较相似,但是互斥内核对象速度比较慢,当相比于关键代码段能够跨进程同步。线程D用于标识互斥内核对象被谁拥有,计数器用于记录该线程对互斥内核对象的拥有次数。互斥内核对象和关键代码段一样能够被一个线程多次拥有,如果一个线程想释放该互斥内核对象,需要对应调用释放函数的次数要和计数器的值相同。

        使用:

如果线程ID为0,表示该互斥内核对象没有被任何线程拥有,该互斥内核对象为已通知状态;
如果线程ID不为0,表示该互斥内核对象被一个线程拥有,为未通知状态;
如果一个线程试图获取该互斥内核对象,且线程ID不为0,那么系统会检查拥有该互斥内核对象的线程ID是否与试图拥有互斥对象的调用线程的ID是否相同,如果相同,该调用线程继续执行,并且互斥内核对象的计数器会加1;如果不相同,调用线程会进入等待状态。
       可以使用函数CreateMutex创建互斥内核对象,使用OpenMutex函数打开已经存在互斥内核对象,函数如下:

HANDLE CreateEvent(
PSECURITY_ATTRIBUTE psa,
LONG lInitialOwner,//用于设置初始状态
PCTSTR pszName
);
HANDLE OpenEvent(
DW0RD fdwAccess,
BOOL bInheritHandle,
PCTSTR pszName
);

       可以使用函数ReleaseEvent函数来释放对资源的访问权:

BOOL ReleaseEvent(HANDLE hEvent);

       线程调用ReleaseEvent函数释放互斥对象时,系统会将调用线程和内核对象的线程ID比较,如果相同,则计数器会减1,如果计数器的值变为0,则将线程ID置为0,如果不为0,保持线程ID不变;如果线程ID和调用线程ID不相同,该函数会返回给调用线程FALSE。

       如果拥有互斥对象的线程被终止运行,没有释放互斥对象,那么系统会认为该互斥对象被丢弃,系统会将线程ID复置为0,并将计数器也复置为0。然后系统会对等待线程进行处理。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: