C# 多线程详解 Part.04(Lock、Monitor、生产与消费)
2018-01-05 10:00
399 查看
系列1曾经说过:每个线程都有自己的资源,但代码区是共享的,即每个线程都可以执行相同的函数。
这可能带来的问题就是多个线程同时执行一个函数,并修改同一变量值,这将导致数据的混乱,产生不可预料的结果。看下面的示例:
结果,计数非但不是20000,相差的还很远。这是因为CPU在线程切换的过程中,2个线程多次发生取出相同值进行运算。这显然不是我们想要达到的目的。
要解决这一问题也非常简单,只需为这段代码加上Lock锁定:
C#提供了一个关键字lock,它可以把一段代码定义为互斥段(criticalsection),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。
在C#中,关键字lock的定义:lock(expression)statement_block
expression代表你希望跟踪的对象,通常是对象引用。如果你想保护一个类的实例,你可以使用this;如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了。而statement_block就是互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。
再看一个Lock关键字的示例:
Lock语法简单易用。其本质是针对Monitor.Enter()和Monitor.Exit()的封装,是一个语法糖!
当多个线程公用一个对象时,也会出现和公用代码类似的问题,这就需要用到System.Threading中的Monitor类,我们可以称之为监视器,Monitor提供了使线程共享资源的方案。
Monitor类可以锁定一个对象,一个线程只有得到这把锁才可以对该对象进行操作。对象锁机制保证了在可能引起混乱的情况下,一个时刻只有一个线程可以访问这个对象。Monitor必须和一个具体的对象相关联,但是由于它是一个静态的类,所以不能使用它来定义对象,而且它的所有方法都是静态的,不能使用对象来引用。
下面代码说明了使用Monitor锁定一个对象的情形:
如上所示,当一个线程调用Monitor.Enter()方法锁定一个对象时,这个对象就归它所有了,其它线程想要访问这个对象,只有等待它使用Monitor.Exit()方法释放锁。为了保证线程最终都能释放锁,你可以把Monitor.Exit()方法写在try-catch-finally结构中的finally代码块里。(Lock关键字就是这个步骤的语法糖)
任何一个被Monitor锁定的对象,内存中都保存着与它相关的一些信息:
现在持有锁的线程的引用
一个预备队列,队列中保存了已经准备好获取锁的线程
一个等待队列,队列中保存着当前正在等待这个对象状态改变的队列的引用
当拥有对象锁的线程准备释放锁时,它使用Monitor.Pulse()方法通知等待队列中的第一个线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁。
下面是一个展示如何使用lock关键字和Monitor类来实现线程的同步和通讯的例子,是一个典型的生产者与消费者问题。
在本例中,生产者线程和消费者线程是交替进行的,生产者写入一个数,消费者立即读取并且显示(注释中介绍了该程序的精要所在)。
这个简单的例子解决了多线程应用程序中可能出现的大问题,只要领悟了解决线程间冲突的基本方法,很容易把它应用到比较复杂的程序中去。
这可能带来的问题就是多个线程同时执行一个函数,并修改同一变量值,这将导致数据的混乱,产生不可预料的结果。看下面的示例:
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;
}
}
这个简单的例子解决了多线程应用程序中可能出现的大问题,只要领悟了解决线程间冲突的基本方法,很容易把它应用到比较复杂的程序中去。
相关文章推荐
- C# 多线程详解 Part.04(Lock、Monitor、生产与消费)
- 多线程__【线程间通信】【等待唤醒机制】【多生产多消费】【Lock&Condition接口】
- c# 线程同步: 详解lock,monitor,同步事件和等待句柄以及mutex
- c# 线程同步:详解lock,monitor,同步事件和等待句柄以及mutex
- c# 线程同步: 详解lock,monitor,同步事件和等待句柄以及mutex
- C# 多线程(lock,Monitor,Mutex,同步事件和等待句柄)
- c# 线程同步: 详解lock,monitor,同步事件和等待句柄以及mutex
- java中多线程模拟(多生产,多消费,Lock实现同步锁,替代synchronized同步代码块)
- C# 多线程(lock,Monitor,Mutex,同步事件和等待句柄) (转)
- lock Mutex Monitor 之间的区别与详解, .net 多线程 同步异步操作,锁
- C# 线程同步: 详解lock,monitor,同步事件和等待句柄以及mutex
- lock Mutex Monitor 之间的区别与详解, .net 多线程 同步异步操作,锁
- c# 线程同步: 详解lock,monitor,同步事件和等待句柄以及mutex
- c# 线程同步: 详解lock,monitor,同步事件和等待句柄以及mutex (转)
- C# 多线程(lock,Monitor,Mutex,同步事件和等待句柄)
- c# 线程同步: 详解lock,monitor,同步事件和等待句柄以及mutex
- C# 多线程中的lock,Monitor.pulse(all)&wait
- <转载>c# 线程同步: 详解lock,monitor,同步事件和等待句柄以及mutex
- C# 多线程(lock,Monitor,Mutex,同步事件和等待句柄)
- c# 线程同步: 详解lock,monitor,同步事件和等待句柄以及mutex(ZT)