C#多线程学习(三) 生产者和消费者 1——解决线程间冲突的关键

前面说过,每个线程都有自己的资源,但是代码区是共享的,即每个线程都可以执行相同的函数。这可能带来的问题就是几个线程同时执行一个函数,导致数据的混乱,产生不可预料的结果,因此我们必须避免这种情况的发生。


C#提供了一个关键字lock,它可以把一段代码定义为互斥段(critical section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在C#中,关键字lock定义如下:

lock(expression) statement_block

expression代表你希望跟踪的对象,通常是对象引用。
    如果你想保护一个类的实例,一般地,你可以使用this;
    如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了。

而statement_block就是互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。


下面是一个使用lock关键字的典型例子,在注释里说明了lock关键字的用法和用途。
示例如下:

(一并提供有关C#中Random类的使用方法)

using System;
using System.Threading;

namespace ThreadSimple
{
    
internal class Account
     {
        
int balance;
         Random r
= new Random();
        
        
internal Account( int initial)
         {
             balance
= initial;
         }

        
internal int Withdraw( int amount)
         {
            
if (balance < 0 )
             {
                
// 如果balance小于0则抛出异常
                 throw new Exception( " Negative Balance " );
             }
            
// 下面的代码保证在当前线程修改balance的值完成之前
            
// 不会有其他线程也执行这段代码来修改balance的值
            
// 因此,balance的值是不可能小于0 的
             lock ( this )
             {
                 Console.WriteLine(
" Current Thread: " + Thread.CurrentThread.Name);
                
// 如果没有lock关键字的保护,那么可能在执行完if的条件判断之后
                
// 另外一个线程却执行了balance=balance-amount修改了balance的值
                
// 而这个修改对这个线程是不可见的,所以可能导致这时if的条件已经不成立了
                
// 但是,这个线程却继续执行balance=balance-amount,所以导致balance可能小于0
                 if (balance >= amount)
                 {
                     Thread.Sleep(
5 );
                     balance
= balance - amount;
                    
return amount;
                 }
                
else
                 {
                    
return 0 ; // transaction rejected
                   }
             }
         }
        
internal void DoTransactions()
         {
            
for ( int i = 0 ; i < 100 ; i ++ )
             Withdraw(r.Next(
- 50 , 100 ));
         }
     }

    
internal class Test
     {
        
static internal Thread[] threads = new Thread[ 10 ];
        
public static void Main()
         {
             Account acc
= new Account ( 0 );
            
for ( int i = 0 ; i < 10 ; i ++ )
             {
                 Thread t
= new Thread( new ThreadStart(acc.DoTransactions));
                 threads[i]
= t;
             }
            
for ( int i = 0 ; i < 10 ; i ++ )
                 threads[i].Name
= i.ToString();
            
for ( int i = 0 ; i < 10 ; i ++ )
                 threads[i].Start();
             Console.ReadLine();
         }
     }
}


Monitor 类锁定一个对象

当多线程公用一个对象时,也会出现和公用代码类似的问题,这种问题就不应该使用lock关键字了,这里需要用到System.Threading中的一个类Monitor,我们可以称之为监视器,Monitor提供了使线程共享资源的方案。

  Monitor类可以锁定一个对象,一个线程只有得到这把锁才可以对该对象进行操作。对象锁机制保证了在可能引起混乱的情况下一个时刻只有一个线程可以访问这个对象。
Monitor必须和一个具体的对象相关联,但是由于它是一个静态的类,所以不能使用它来定义对象,而且它的所有方法都是静态的,不能使用对象来引用。下面代码说明了使用Monitor锁定一个对象的情形:

......
Queue oQueue=new Queue();
......
Monitor.Enter(oQueue);
......//现在oQueue对象只能被当前线程操纵了
Monitor.Exit(oQueue);//释放锁

如上所示,当一个线程调用Monitor.Enter()方法锁定一个对象时,这个对象就归它所有了,其它线程想要访问这个对象,只有等待它使用Monitor.Exit()方法释放锁。为了保证线程最终都能释放锁,你可以把Monitor.Exit()方法写在try-catch-finally结构中的finally代码块里。

对于任何一个被Monitor锁定的对象,内存中都保存着与它相关的一些信息:
其一是现在持有锁的线程的引用;
其二是一个预备队列,队列中保存了已经准备好获取锁的线程;
其三是一个等待队列,队列中保存着当前正在等待这个对象状态改变的队列的引用。

当拥有对象锁的线程准备释放锁时,它使用Monitor.Pulse()方法通知等待队列中的第一个线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁。


下面是一个展示如何使用lock关键字和Monitor类来实现线程的同步和通讯的例子,也是一个典型的生产者与消费者问题。
这个例程中,生产者线程和消费者线程是交替进行的,生产者写入一个数,消费者立即读取并且显示(注释中介绍了该程序的精要所在)。

用到的系统命名空间如下:
using System;
using System.Threading;

首先,定义一个被操作的对象的类Cell,在这个类里,有两个方法:ReadFromCell()和WriteToCell。消费者线程将调用ReadFromCell()读取cellContents的内容并且显示出来,生产者进程将调用WriteToCell()方法向cellContents写入数据。

示例如下:

public class Cell
{
        
int cellContents; // Cell对象里边的内容
         bool readerFlag = false ; // 状态标志,为true时可以读取,为false则正在写入
         public int ReadFromCell( )
         {
            
lock ( this ) // Lock关键字保证了什么,请大家看前面对lock的介绍
             {
                
if ( ! readerFlag) // 如果现在不可读取
                 {
                    
try
                     {
                        
// 等待WriteToCell方法中调用Monitor.Pulse()方法
                         Monitor.Wait( this );
                     }
                    
catch (SynchronizationLockException e)
                     {
                         Console.WriteLine(e);
                     }
                    
catch (ThreadInterruptedException e)
                     {
                         Console.WriteLine(e);
                     }
                 }
                 Console.WriteLine(
" Consume: {0} " ,cellContents);
                 readerFlag
= false ;
                
// 重置readerFlag标志,表示消费行为已经完成
                 Monitor.Pulse( this );
                
// 通知WriteToCell()方法(该方法在另外一个线程中执行,等待中)
             }
            
return cellContents;
         }
    
        
public void WriteToCell( int n)
         {
            
lock ( this )
             {
                
if (readerFlag)
                 {
                    
try
                     {
                         Monitor.Wait(
this );
                     }
                    
catch (SynchronizationLockException e)
                     {
                            
// 当同步方法(指Monitor类除Enter之外的方法)在非同步的代码区被调用
                         Console.WriteLine(e);
                     }
                    
catch (ThreadInterruptedException e)
                     {
                            
// 当线程在等待状态的时候中止
                         Console.WriteLine(e);
                     }
                 }
                 cellContents
= n;
                 Console.WriteLine(
" Produce: {0} " ,cellContents);
                 readerFlag
= true ;
                 Monitor.Pulse(
this );
                
// 通知另外一个线程中正在等待的ReadFromCell()方法
             }
         }
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值