您的位置:首页 > 编程语言 > C#

C# 多线程详解 Part.04(Lock、Monitor、生产与消费)

2018-01-05 10:00 399 查看
系列1曾经说过:每个线程都有自己的资源,但代码区是共享的,即每个线程都可以执行相同的函数。

这可能带来的问题就是多个线程同时执行一个函数,并修改同一变量值,这将导致数据的混乱,产生不可预料的结果。看下面的示例:

privatevoidbtnThread_Click(objectsender,EventArgse)

{

Threadt1=newThread(ChangeTextBox);

t1.Start();

Threadt2=newThread(ChangeTextBox);

t2.Start();

}


voidChangeTextBox()

{

for(inti=0;i<10000;i++)

{

intnum=int.Parse(txtNum.Text);

num++;

txtNum.Text=num.ToString();

}

}






结果,计数非但不是20000,相差的还很远。这是因为CPU在线程切换的过程中,2个线程多次发生取出相同值进行运算。这显然不是我们想要达到的目的。


Lock

要解决这一问题也非常简单,只需为这段代码加上Lock锁定:

privatestaticobjectobj=newobject();

voidChangeTextBox()

{

for(inti=0;i<10000;i++)

{

lock(obj)

{

intnum=int.Parse(txtNum.Text);

num++;

txtNum.Text=num.ToString();

}

}

}






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

在C#中,关键字lock的定义:lock(expression)statement_block

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

再看一个Lock关键字的示例:

internalclassAccount

{

intbalance;

Randomr=newRandom();


internalAccount(intinitial)

{

balance=initial;

}


internalvoidWithdraw(intamount)

{

if(balance<0)

{

thrownewException("NegativeBalance");

}


lock(this)

{

//下面的代码保证在当前线程修改balance的值完成之前

//不会有其他线程也执行这段代码来修改balance的值

//因此,balance的值是不可能小于0的


Console.WriteLine("CurrentThread:"+Thread.CurrentThread.Name

+"balance:"+balance.ToString()+"amount:"+amount);

//如果没有lock关键字的保护,那么可能在执行完if的条件判断之后

//另外一个线程却执行了balance=balance-amount修改了balance的值

//而这个修改对这个线程是不可见的,所以可能导致这时if的条件已经不成立了

//但是,这个线程却继续执行balance=balance-amount,所以导致balance可能小于0

//去除lock块可以看出效果,程序会抛出异常

if(balance>=amount)

{

Thread.Sleep(5);

balance=balance-amount;

}

Console.WriteLine("CurrentThread:"+Thread.CurrentThread.Name

+"balance:"+balance.ToString()+"amount:"+amount);

}

}


internalvoidDoTransactions()

{

for(inti=0;i<100;i++)

{

Withdraw(r.Next(-50,100));

}

}

}


internalclassTest

{

staticinternalThread[]threads=newThread[10];

publicstaticvoidMain()

{

Accountacc=newAccount(0);

for(inti=0;i<10;i++)

{

Threadt=newThread(newThreadStart(acc.DoTransactions));

threads[i]=t;

threads[i].Name=i.ToString();

}


for(inti=0;i<10;i++)

{

threads[i].Start();

}


Console.ReadLine();

}

}


Lock语法简单易用。其本质是针对Monitor.Enter()和Monitor.Exit()的封装,是一个语法糖!


Monitor

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

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


下面代码说明了使用Monitor锁定一个对象的情形:

//表示对象的先进先出集合

QueueoQueue=newQueue();

try

{

//现在oQueue对象只能被当前线程操纵了

Monitor.Enter(oQueue);


//dosomething......

}

catch

{


}

finally

{

//释放锁

Monitor.Exit(oQueue);

}


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

任何一个被Monitor锁定的对象,内存中都保存着与它相关的一些信息:

现在持有锁的线程的引用
一个预备队列,队列中保存了已经准备好获取锁的线程
一个等待队列,队列中保存着当前正在等待这个对象状态改变的队列的引用

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


生产与消费

下面是一个展示如何使用lock关键字和Monitor类来实现线程的同步和通讯的例子,是一个典型的生产者与消费者问题。

在本例中,生产者线程和消费者线程是交替进行的,生产者写入一个数,消费者立即读取并且显示(注释中介绍了该程序的精要所在)。

///<summary>

///被操作的对象

///</summary>

publicclassCell

{

///<summary>

///Cell对象里的内容

///</summary>

intcellContents;


///<summary>

///状态标志:为true时可以读取,为false则正在写入

///</summary>

boolreaderFlag=false;


publicintReadFromCell()

{

lock(this)

{

if(!readerFlag)

{

try

{

//等待WriteToCell方法中调用Monitor.Pulse()方法

Monitor.Wait(this);

}

catch(SynchronizationLockExceptione)

{

Console.WriteLine(e);

}

catch(ThreadInterruptedExceptione)

{

Console.WriteLine(e);

}

}


//开始消费行为

Console.WriteLine("Consume:{0}",cellContents);

Console.WriteLine();


//重置readerFlag标志,表示消费行为已经完成

readerFlag=false;


//通知WriteToCell()方法(该方法在另外一个线程中执行,等待中)

Monitor.Pulse(this);

}

returncellContents;

}


publicvoidWriteToCell(intn)

{

lock(this)

{

if(readerFlag)

{

try

{

Monitor.Wait(this);

}

catch(SynchronizationLockExceptione)

{

//当同步方法(指Monitor类除Enter之外的方法)在非同步的代码区被调用

Console.WriteLine(e);

}

catch(ThreadInterruptedExceptione)

{

//当线程在等待状态的时候中止

Console.WriteLine(e);

}

}

cellContents=n;

Console.WriteLine("Produce:{0}",cellContents);

readerFlag=true;

Monitor.Pulse(this);//通知另外一个线程中正在等待的ReadFromCell()方法

}

}

}


///<summary>

///生产者

///</summary>

publicclassCellProd

{

///<summary>

///被操作的Cell对象

///</summary>

Cellcell;


///<summary>

///生产者生产次数,初始化为1

///</summary>

intquantity=1;


publicCellProd(Cellbox,intrequest)

{

cell=box;

quantity=request;

}


publicvoidThreadRun()

{

for(intlooper=1;looper<=quantity;looper++)

{

//生产者向操作对象写入信息

cell.WriteToCell(looper);

}

}

}


///<summary>

///消费者

///</summary>

publicclassCellCons

{

Cellcell;

intquantity=1;


publicCellCons(Cellbox,intrequest)

{

cell=box;

quantity=request;

}


publicvoidThreadRun()

{

intvalReturned;

for(intlooper=1;looper<=quantity;looper++)

{

valReturned=cell.ReadFromCell();//消费者从操作对象中读取信息

}

}

}


///<summary>

///测试类

///</summary>

publicclassMonitorSample

{

publicstaticvoidMain(String[]args)

{

//一个标志位,如果是0表示程序没有出错,如果是1表明有错误发生

intresult=0;


//下面使用cell初始化CellProd和CellCons两个类,生产和消费次数均为20次

Cellcell=newCell();

CellProdprod=newCellProd(cell,20);

CellConscons=newCellCons(cell,20);

Threadproducer=newThread(newThreadStart(prod.ThreadRun));

Threadconsumer=newThread(newThreadStart(cons.ThreadRun));


//生产者线程和消费者线程都已经被创建,但是没有开始执行

try

{

producer.Start();

consumer.Start();

producer.Join();

consumer.Join();

Console.ReadLine();

}

catch(ThreadStateExceptione)

{

//当线程因为所处状态的原因而不能执行被请求的操作

Console.WriteLine(e);

result=1;

}

catch(ThreadInterruptedExceptione)

{

//当线程在等待状态的时候中止

Console.WriteLine(e);

result=1;

}

//尽管Main()函数没有返回值,但下面这条语句可以向父进程返回执行结果

Environment.ExitCode=result;

}

}


这个简单的例子解决了多线程应用程序中可能出现的大问题,只要领悟了解决线程间冲突的基本方法,很容易把它应用到比较复杂的程序中去。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐