系统中的所有线程都要访问系统资源,一个线程霸占某个资源,其他需要该资源的线程就不能完成自己的任务;另外如一个线程在读取某块内存中的数据,而另一个线程又正在修改这块内存的值,这同样不是我们想要的,所以线程之间必须要有一套自己的规则,不然就凌乱了。线程之间需要通信,如A线程霸占某个B线程需要的资源X,在A占用期间,B线程只能等待,或处于挂起状态,当A线程用完资源X后,系统会告诉线程B,资源X可以用了,或是将处于挂起状态的线程B唤醒,然后线程B就获得对资源X的控制权,其他想用资源X的线程就得经历B刚才的遭遇。当多个线程同时需要某个资源时必须遵守下面两个规则:
1:多个线程“同时”访问资源,不能破坏资源的完整性。
2:一个线程需要通知其他线程某项任务已经完成。
原子访问:Interlocked系列函数。多线程编程大部分情况与原子访问有关,即一个线程在访问某个资源时,确保没有其他线程能访问该资源。
增量函数InterlockedExchangeAdd结构如下:
InterlockedExchangeAdd(
unsigned long volatile *Addend,//被增量变量的地址
unsigned long Value//增量值
)
Volatile表示每次都成内存中读取数据,而不会从高速缓存中读取数据,如一个全局变量,在一个多线程函数中被修改,在多核CPU中,这个变量可能在多个CPU的高速缓存中都有副本,如果不用volatile修饰,那么可能会因为优化的原因,CPU不会读内存中的数据,而是直接从高速缓存中读取数据,在这种情况下,很可能这个值已经被修改了,这样CPU读取到的不是最新的数据,程序肯定会出错,用volatile修饰后,这个变量的所有高速缓存就会失效,就不会出现这种问题。在多线程编程中volatile作用非常大,效率也最高。但他就是只能修饰单个变量,不能修饰代码段。
InterlockedExchangeAdd执行的速度是非常快的,只需要占用几个CPU周期。用InterlockedExchangeAdd来修改某个变量的值,好像有点大材小用了,因为用Volatile就足够了,简单迅速。但在实现旋转锁时InterlockedExchange就非常有用。旋转锁的代码大致如下:
bool sourceIsUse=false; void fun() { //一直等待直到资源可用 while(InterlockedExchange(&sourceIsUse,true)==true) { Sleep(0); } //访问资源的操作 ...... //资源用好了,打开锁,让其他等待的资源访问 InterlockedExchange(&sourceIsUse,false); }
InterlockedExchange:将第一个参数的值修改成第二个参数的值,返回第一个参数原来的值。在第一个线程就来的时候,它顺利的闯过了While循环,并上了锁,导致while始终为true,后来的线程就一直在while里面打转,当前面的线程用完之后,他就会把锁打开,然后新来的线程就可以跳出while循环,并上锁(在等待时一直在上锁),开锁独占资源了,新来的线程又开始等待。就像大厦前门的旋转门,一拨人进去之后,后面的人就只能在外面等,等里面的人出去之后,后面的人也就可以进去了,周而复始。
高速缓存行。当CPU从内存中读取一个字节时,它并不是真的只读一个字节,而是读取一个高速缓存行,一个高速缓存行可能是32个字节、64个字节或是128个字节,它始终读取的字节数是32的整数倍,这样CPU就不用非常频繁的读取内存,从而提高程序的性能,当CPU访问某块内存是它会访问这块内存旁边的内存的概率是非常大的,于是就一起读了。更多关于数据对齐的信息请看我的文章《数据对齐》。
高级线程同步。刚刚简单的说了一下旋转锁,现在又来说旋转锁的坏,旋转锁的问题在于等待的线程一直在执行毫无用处的该死的死循环,浪费CPU的时间,这肯定是不能容忍的,虽然曾经一度容忍过它。当一个线程需要某个资源,而这个资源被另一个线程占用时,如果这个线程等了一会儿还不能获得这个资源,那么这个线程就应该被切换到等待状态,让系统充当该线程的代理,当该资源可以被使用时,系统就会将该线程唤醒,然后该线程就可以独占该资源。而实现这一功能的就是关键段。
关键段。关键段是一小段代码,在执行之前需要独占对一些共享资源的访问,这种方式可以让多行代码以原子的方式进行访问,当有一个线程对访问这段代码时其他线程只能等待。使用关键段的步骤如下:
CRITICAL_SECTION g_cs;//构造一个CRITICAL_SECTION实例
InitializeCriticalSection(&g_cs);//初始化g_cs的成员
EnterCriticalSection(&g_cs);//进入关键段
LeaveCriticalSection(&g_cs);//离开关键段
DeleteCriticalSection(&g_cs);//清理g_cs
EnterCriticalSection会检查结构CRITICAL_SECTION的成员变量,这些成员表示是否有线程正在访问资源,以及哪个线程正在访问资源,EnterCriticalSection会进行一些测试。如果没有线程正在访问资源,EnterCriticalSection会更新变量成员,以表示已经有线程正在访问资源,并马上从EnterCriticalSection返回,继续执行关键段中的代码,如果变量成员表示已经有线程正在访问资源,那么EnterCriticalSection会使用一个事件内核对象把线程切换成等待状态,等待状态的线程是不会浪费CPU的时间的,系统会记住这个线程想要使用这个资源,一旦当前线程调用LeaveCriticalSection,系统会自动更新CRITICAL_SECTION的成员变量,并将等待的线程切换成可调度状态。
LeaveCriticalSection会检查结构CRITICAL_SECTION的成员变量并将计数器减一,如果计数器变为0,LeaveCriticalSection会更新成员变量表示现在没有线程访问资源,若有等待的线程,则将等待的线程切换成可调度的状态。
当一个线程进入关键段时,若有线程正在访问关键段,那么系统就会将新的线程切换成等待状态,这意味着将线程从用户模式切换成内核模式,这个切换的开销大约是1000个CPU周期,这个开销其实是很大的,所以在EnterCriticalSection内部使用旋转锁,并不是马上将线程切换成等待状态,而是先用旋转锁试探一些,看线程是否释放了对资源的访问,如果释放了,新的线程就不用被切换成等待状态了,就可以直接访问资源了,也就是说花了旋转锁轮询的时间,如果旋转锁轮询了一段时间,线程还是没有释放资源,对不起系统就不会让它继续轮询了,因为系统也不知道还要轮询多久,毕竟轮询一直都是在消耗CPU的时间,系统会停止轮询,将新的线程切换成等待状态,当前一个资源释放对资源的访问,系统会将新的线程切换成可调度状态。
Silm读/写锁。SRWLock的目的和关键段是一样的,就是对资源的保护,不让其他线程访问。不同的是,它区分线程是读线程还是写线程。我们都是知道,一个资源可以同时被多个线程同时读,就是不能同时读,或是读写。也是是说写必须是独占的方式,而读可以以共享的方式访问。
读写锁调用的函数如下,跟关键段差不多,我就不废话了。
RTL_SRWLOCK lock;
InitializeSRWLock(&lock);
AcquireSRWLockExclusive(&lock);//独占的方式访问
ReleaseSRWLockExclusive(&lock);
AcquireSRWLockShared(&lock);//共享的方式访问
ReleaseSRWLockShared(&lock);
作者:陈太汉
博客:http://www.cnblogs.com/hlxs/