C#多线程编程实例
问题的提出
所谓单个写入程序/多个阅读程序的线程同步问题 是指任意数量的线程访问共享资源时 写入程序(线程)需要修改共享资源 而阅读程序(线程)需要读取数据 在这个同步问题中 很容易得到下面二个要求
)当一个线程正在写入数据时 其他线程不能写 也不能读
)当一个线程正在读入数据时 其他线程不能写 但能够读
在数据库应用程序环境中经常遇到这样的问题 比如说 有n个最终用户 他们都要同时访问同一个数据库 其中有m个用户要将数据存入数据库 n m个用户要读取数据库中的记录
很显然 在这个环境中 我们不能让两个或两个以上的用户同时更新同一条记录 如果两个或两个以上的用户都试图同时修改同一记录 那么该记录中的信息就会被破坏
我们也不让一个用户更新数据库记录的同时 让另一用户读取记录的内容 因为读取的记录很有可能同时包含了更新和没有更新的信息 也就是说这条记录是无效的记录
实现分析
规定任一线程要对资源进行写或读操作前必须申请锁 根据操作的不同 分为阅读锁和写入锁 操作完成之后应释放相应的锁 将单个写入程序/多个阅读程序的要求改变一下 可以得到如下的形式
一个线程申请阅读锁的成功条件是 当前没有活动的写入线程
一个线程申请写入锁的成功条件是 当前没有任何活动(对锁而言)的线程
因此 为了标志是否有活动的线程 以及是写入还是阅读线程 引入一个变量m_nActive 如果m_nActive > 则表示当前活动阅读线程的数目 如果m_nActive= 则表示没有任何活动线程 m_nActive < 表示当前有写入线程在活动 注意m_nActive< 时只能取 的值 因为只允许有一个写入线程活动
为了判断当前活动线程拥有的锁的类型 我们采用了线程局部存储技术(请参阅其它参考书籍) 将线程与特殊标志位关联起来
申请阅读锁的函数原型为 public void AcquireReaderLock( int millisecondsTimeout ) 其中的参数为线程等待调度的时间 函数定义如下
public void AcquireReaderLock( int millisecondsTimeout ) { // m_mutext很快可以得到 以便进入临界区 m_mutex WaitOne( ); // 是否有写入线程存在 bool bExistingWriter = ( m_nActive < ); if( bExistingWriter ) { //等待阅读线程数目加 当有锁释放时 根据此数目来调度线程 m_nWaitingReaders++; } else { //当前活动线程加 m_nActive++; } m_mutex ReleaseMutex(); //存储锁标志为Reader System LocalDataStoreSlot slot = Thread GetNamedDataSlot(m_strThreadSlotName); object obj = Thread GetData( slot ); LockFlags flag = LockFlags None; if( obj != null ) flag = (LockFlags)obj ; if( flag == LockFlags None ) { Thread SetData( slot LockFlags Reader ); } else { Thread SetData( slot (LockFlags)((int)flag | (int)LockFlags Reader ) ); } if( bExistingWriter ) { //等待指定的时间 this m_aeReaders WaitOne( millisecondsTimeout true ); } }
它首先进入临界区(用以在多线程环境下保证活动线程数目的操作的正确性)判断当前活动线程的数目 如果有写线程(m_nActive< )存在 则等待指定的时间并且等待的阅读线程数目加 如果当前活动线程是读线程(m_nActive>= ) 则可以让读线程继续运行
申请写入锁的函数原型为 public void AcquireWriterLock( int millisecondsTimeout ) 其中的参数为等待调度的时间 函数定义如下
public void AcquireWriterLock( int millisecondsTimeout ) { // m_mutext很快可以得到 以便进入临界区 m_mutex WaitOne( ); // 是否有活动线程存在 bool bNoActive = m_nActive == ; if( !bNoActive ) { m_nWaitingWriters++; } else { m_nActive ; } m_mutex ReleaseMutex(); //存储线程锁标志 System LocalDataStoreSlot slot = Thread GetNamedDataSlot( myReaderWriterLockDataSlot ); object obj = Thread GetData( slot ); LockFlags flag = LockFlags None; if( obj != null ) flag = (LockFlags)Thread GetData( slot ); if( flag == LockFlags None ) { Thread SetData( slot LockFlags Writer ); } else { Thread SetData( slot (LockFlags)((int)flag | (int)LockFlags Writer ) ); } //如果有活动线程 等待指定的时间 if( !bNoActive ) this m_aeWriters WaitOne( millisecondsTimeout true ); }
它首先进入临界区判断当前活动线程的数目 如果当前有活动线程存在 不管是写线程还是读线程(m_nActive) 线程将等待指定的时间并且等待的写入线程数目加 否则线程拥有写的权限
释放阅读锁的函数原型为 public void ReleaseReaderLock() 函数定义如下
public void ReleaseReaderLock() { System LocalDataStoreSlot slot = Thread GetNamedDataSlot(m_strThreadSlotName ); LockFlags flag = (LockFlags)Thread GetData( slot ); if( flag == LockFlags None ) { return; } bool bReader = true; switch( flag ) { case LockFlags None: break; case LockFlags Writer: bReader = false; break; } if( !bReader ) return; Thread SetData( slot LockFlags None ); m_mutex WaitOne(); AutoResetEvent autoresetevent = null; this m_nActive ; if( this m_nActive == ) { if( this m_nWaitingReaders > ) { m_nActive ++ ; m_nWaitingReaders ; autoresetevent = this m_aeReaders; } else if( this m_nWaitingWriters > ) { m_nWaitingWriters ; m_nActive ; autoresetevent = this m_aeWriters ; } } m_mutex ReleaseMutex(); if( autoresetevent != null ) autoresetevent Set(); }
释放阅读锁时 首先判断当前线程是否拥有阅读锁(通过线程局部存储的标志) 然后判断是否有等待的阅读线程 如果有 先将当前活动线程加 等待阅读线程数目减 然后置事件为有信号 如果没有等待的阅读线程 判断是否有等待的写入线程 如果有则活动线程数目减 等待的写入线程数目减 释放写入锁与释放阅读锁的过程基本一致 可以参看源代码
注意在程序中 释放锁时 只会唤醒一个阅读程序 这是因为使用AutoResetEvent的原历 读者可自行将其改成ManualResetEvent 同时唤醒多个阅读程序 此时应令m_nActive等于整个等待的阅读线程数目
测试
测试程序取自 Net FrameSDK中的一个例子 只是稍做修改 测试程序如下
lishixinzhi/Article/program/net/201311/12614