热爱测试,主要研究性能测试和自动化测试方面的技术,希望与同样对测试有热情的你一同进步成长

c#多线程的学习(2)(转)

上一篇 / 下一篇  2007-07-27 16:09:55 / 个人分类:软件开发

 
假设这样一种情况,两个线程同时维护一个队列,如果一个线程对队列中添加元素,而另外一个线程从队列中取用元素,那么我们称添加元素的线程为生产者,称取用元素的线程为消费者。生产者与消费者问题看起来很简单,但是却是多线程应用中一个必须解决的问题,它涉及到线程之间的同步和通讯问题。
%a"D7w@ S(D0CR0C0
0p0m(^&rm!Y$`0
前面说过,每个线程都有自己的资源,但是代码区是共享的,即每个线程都可以执行相同的函数。但是多线程环境下,可能带来的问题就是几个线程同时执行一个函数,导致数据的混乱,产生不可预料的结果,因此我们必须避免这种情况的发生。C#提供了一个关键字lock,它可以把一段代码定义为互斥段(critical section),互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待。在C#中,关键字lock定义如下:
lock(expression) statement_block
 
expression代表你希望跟踪的对象,通常是对象引用。一般地,如果你想保护一个类的实例,你可以使用this;如果你希望保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了。而statement_block就是互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。51Testing软件测试网CU.D[Sy;r'L
  下面是一个使用lock关键字的典型例子,我将在注释里向大家说明lock关键字的用法和用途:
//lock.cs
A$qp6v1YE_.pE0using System;
\h8?mkx0using System.Threading;
51Testing软件测试网vI%j[ G
51Testing软件测试网 K/A0c!Jn bwY'YC?Ly)uZ
internal class Account
7G^!h-E ~"vk#n#y mm0{51Testing软件测试网1W_IM;S(w
  int balance;51Testing软件测试网 e*a(b-~"SK'na
  Random r = new Random();51Testing软件测试网L)E)I*W0^+VkA'F I I1]
  internal Account(int initial)51Testing软件测试网8z$^U:pM }
  {51Testing软件测试网ZL8\;y d0NGv$S.Pi
  balance = initial;
,kw&rf,QctG.x1I(G0
  }51Testing软件测试网c4w:z}%]D1O4KM

5tE5v(~R9|.vW?0
  internal int Withdraw(int amount)
ki#a@+V0
  {
bO/I$r'}/H(B(Q'o0
  if (balance < 0)51Testing软件测试网!Y$x)S"Dml#yZ
  {51Testing软件测试网6YV2k;A'z
    file://如果balance小于0则抛出异常
TfnJT1dv3g-i j0
    throw new Exception("Negative Balance");
u$I6u}w"MT'DT1i0
  }51Testing软件测试网\spr;pU0]]
  //下面的代码保证在当前线程修改balance的值完成之前
-Sq5WmkH K9R0
  //不会有其他线程也执行这段代码来修改balance的值
pi]Y8}{D0
  //因此,balance的值是不可能小于051Testing软件测试网S ~%w(}y)}$|"qW
  lock (this)
]:^ q N no dW't6^0
  {
)[0\,M4RV6Xt*`_(o-v_0
    Console.WriteLine("Current Thread:"+Thread.CurrentThread.Name);
7Y8hz9nn)?,T.b0
    file://如果没有lock关键字的保护,那么可能在执行完if的条件判断之后51Testing软件测试网&^cC1dd(S
    file://另外一个线程却执行了balance=balance-amount修改了balance的值
C Bu![lEr0
    file://而这个修改对这个线程是不可见的,所以可能导致这时if的条件已经不成立了51Testing软件测试网8QO oe1f3hZ)T
    file://但是,这个线程却继续执行balance=balance-amount,所以导致balance可能小于051Testing软件测试网U2x|,cf_ bP3Sa
    if (balance >= amount)51Testing软件测试网fc6r#I-y&B M&ui
    {
`d p^ YN$QfgL0
    Thread.Sleep(5);
lY']O2A'clMOt0
    balance = balance - amount;51Testing软件测试网QIQ%j;?{ Y
    return amount;51Testing软件测试网y6t0r.Jy~
    }
(bR0^2jc$Z0
    else
r^-p-m,xO5\0
    {
5Aw-q&jk[xi0
    return 0; // transaction rejected
8T-KKR?0
    }51Testing软件测试网p ZhL1K
  }51Testing软件测试网$mSB"r7{J
  }51Testing软件测试网Y8hKS}V(AjB
  internal void DoTransactions()51Testing软件测试网R?/l Y^k'h*N
  {
CS5X D Iz+P0
  for (int i = 0; i < 100; i++)
t y$OF.m0
    Withdraw(r.Next(-50, 100));
pu1M Z4Y!@#[J0
  }
Z| H1k(e4VMX9z0
}
x4hns-QPp0
,|A8nx0?i4S }0internal class Test51Testing软件测试网,q/o w[5Rs
{51Testing软件测试网BF&?Z A
  static internal Thread[] threads = new Thread[10];
m1|U3X"v$qE b0
  public static void Main()
K m1VoB3X0
  {
u/wkF4xo+q0
  Account acc = new Account (0);
'V+]:A5P*D!O0
  for (int i = 0; i < 10; i++)51Testing软件测试网0h!G!u3q&wU-XF&u;c
  {51Testing软件测试网D V.R7i B a+^ID%j$eC
    Thread t = new Thread(new ThreadStart(acc.DoTransactions));
R.H5O4bC%q1h^0
    threads[i] = t;
3z@wd8nt0
  }
M7\!c3q1I7{5H0
  for (inti = 0; i < 10; i++)
v!s:x2]x.v8J3p_ D0
    threads[i].Name=i.ToString();
MX iH3F e:x*P4EW0
  for (int i = 0; i < 10; i++)51Testing软件测试网 B;h"W`"m"Qt6\7@,J
    threads[i].Start();
l8E)JS7|d`0
  Console.ReadLine();
G;Y0\9hO@5j0
  }51Testing软件测试网CK&c*Q%vk~1F
}

zzLL3K(lP rW051Testing软件测试网] LK yd d
而多线程公用一个对象时,也会出现和公用代码类似的问题,这种问题就不应该使用lock关键字了,这里需要用到System.Threading中的一个类Monitor,我们可以称之为监视器,Monitor提供了使线程共享资源的方案。51Testing软件测试网2xP x/ts Zu+q-^ e

,B%R.eWt7o5ZK0
  Monitor类可以锁定一个对象,一个线程只有得到这把锁才可以对该对象进行操作。对象锁机制保证了在可能引起混乱的情况下一个时刻只有一个线程可以访问这个对象。Monitor必须和一个具体的对象相关联,但是由于它是一个静态的类,所以不能使用它来定义对象,而且它的所有方法都是静态的,不能使用对象来引用。下面代码说明了使用Monitor锁定一个对象的情形:
......
OR!?1crR N;{0Queue ōQueue=new Queue();51Testing软件测试网1s[ t4{"{6@q
......51Testing软件测试网 dX Xi@Y5l0jk0`
Monitor.Enter(oQueue);
?9Gk#\\(?,j6Y0......//
现在oQueue对象只能被当前线程操纵了51Testing软件测试网0QgX)P]
Monitor.Exit(oQueue);//释放锁
51Testing软件测试网h*B(}o,[X*o_(~8[k
如上所示,当一个线程调用Monitor.Enter()方法锁定一个对象时,这个对象就归它所有了,其它线程想要访问这个对象,只有等待它使用Monitor.Exit()方法释放锁。为了保证线程最终都能释放锁,你可以把Monitor.Exit()方法写在try-catch-finally结构中的finally代码块里。对于任何一个被Monitor锁定的对象,内存中都保存着与它相关的一些信息,其一是现在持有锁的线程的引用,其二是一个预备队列,队列中保存了已经准备好获取锁的线程,其三是一个等待队列,队列中保存着当前正在等待这个对象状态改变的队列的引用。当拥有对象锁的线程准备释放锁时,它使用Monitor.Pulse()方法通知等待队列中的第一个线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁。51Testing软件测试网7Q1G%te+}

%l[ ^ VqM v x0
下面是一个展示如何使用lock关键字和Monitor类来实现线程的同步和通讯的例子,也是一个典型的生产者与消费者问题。这个例程中,生产者线程和消费者线程是交替进行的,生产者写入一个数,消费者立即读取并且显示,我将在注释中介绍该程序的精要所在。用到的系统命名空间如下:
using System;51Testing软件测试网 V/e@ } pa#c`
using System.Threading;
首先,我们定义一个被操作的对象的类Cell,在这个类里,有两个方法:ReadFromCell()WriteToCell。消费者线程将调用ReadFromCell()读取cellContents的内容并且显示出来,生产者进程将调用WriteToCell()方法向cellContents写入数据。
public class Cell51Testing软件测试网.r!d&R [B/p8o8CG5ow;e
{
*Z+zm[7` u%|0
  int cellContents; // Cell对象里边的内容
h)IEe"|-Vz0X0
  bool readerFlag = false; //状态标志,为true时可以读取,为false则正在写入51Testing软件测试网#O B*hw$s[F^(F7SJG
  public int ReadFromCell( )
UmS(x/h/S8DMx,m0
  {
2VH4acJ0
  lock(this) // Lock关键字保证了什么,请大家看前面对lock的介绍51Testing软件测试网:yPg A A d j1gV5C
  {51Testing软件测试网?'e+Z-B| U"_ p@
    if (!readerFlag)//如果现在不可读取
g@UAZ2t za0}0
    {
'\?!q8LE0
    try51Testing软件测试网$w#HN1}-Q/F
    {
S7b/H o6E7A0
      file://等待WriteToCell方法中调用Monitor.Pulse()方法
|+g1w%f/cm0
      Monitor.Wait(this);51Testing软件测试网+YWQh){!^ }7d
    }
]Ahp8f%U1`t:ex0
    catch (SynchronizationLockException e)51Testing软件测试网`%ko`:vO l
    {
3r1UMrQp0
      Console.WriteLine(e);
v,e$v3kV*M)k+z0
    }
|:UF8X.b f#Tdr e0
    catch (ThreadInterruptedException e)51Testing软件测试网e4a8E\IC'eO&P
    {
.wn'~7Su'b0
      Console.WriteLine(e);51Testing软件测试网n no;s OW:i,BYe-Y;_
    }
F0B#k-Ms3PaX0
    }
O ~&l(?%XCY)dGu0
    Console.WriteLine("Consume: {0}",cellContents);51Testing软件测试网/l!c6`\0lC$`
    readerFlag = false; file://重置readerFlag标志,表示消费行为已经完成
MJ!X2}F Je0
    Monitor.Pulse(this); file://通知WriteToCell()方法(该方法在另外一个线程中执行,等待中)51Testing软件测试网9c cqE`/tF
  }51Testing软件测试网6e*mk$zZf*d Uz i
  return cellContents;
)q3t0{*~,dwW0
  }51Testing软件测试网d2vjGRRW5F b [(j

(fy+Tf9t0
  public void WriteToCell(int n)51Testing软件测试网NPyk$h(kpQ
  {51Testing软件测试网#_QI1S#Tu/G/?/t?a
  lock(this)
2LM%Y8? S*f4jO1eo8K0
  {51Testing软件测试网i.S,h f.[p8q5L
    if (readerFlag)
Q6k8i_#m6Pb~0
    {
?D#[ro{7L+q0
    try
#v:vm5_N0?4{0
    {
Z y!f9^p `0
      Monitor.Wait(this);51Testing软件测试网d1m}l0j|-Q
    }
7Lm)R3iJ6O0
    catch (SynchronizationLockException e)51Testing软件测试网j"A(P.nDP v
    {
reFK7ZDR:h0
      file://当同步方法(指Monitor类除Enter之外的方法)在非同步的代码区被调用51Testing软件测试网9kpZ\URtT
      Console.WriteLine(e);51Testing软件测试网Z'LJKH|
    }
9w0a\ZYU0
    catch (ThreadInterruptedException e)
3`Y!h ~a0
    {
n"PtFm%\\E*[0
      file://当线程在等待状态的时候中止
"dc~vd0mio0
      Console.WriteLine(e);
)o`3d]Ts0
    }
yB^J1jv9}Ut0
    }51Testing软件测试网0k[9Lu8}
    cellContents = n;51Testing软件测试网jH)UxC
    Console.WriteLine("Produce: {0}",cellContents);
6fk/B9f#j3Z {8s9`/i0
    readerFlag = true;
/qTDc&Z;O-r8b&D0
    Monitor.Pulse(this); file://通知另外一个线程中正在等待的ReadFromCell()方法51Testing软件测试网a wD[P;s"H3t5@
  }51Testing软件测试网n#xoc,J)F HU;d\
  }51Testing软件测试网"S:heX^'cena P
}

A*u6C(Y,W},`n0
下面定义生产者CellProd和消费者类CellCons,它们都只有一个方法ThreadRun(),以便在Main()函数中提供给线程的ThreadStart代理对象,作为线程的入口。
然后在下面这个类MonitorSampleMain()函数中我们要做的就是创建两个线程分别作为生产者和消费者,使用CellProd.ThreadRun()方法和CellCons.ThreadRun()方法对同一个Cell对象进行操作。
public class MonitorSample
2liG_ @%Dt8~P0{
tH@pU[0
  public static void Main(String[] args)51Testing软件测试网%o4um_?;XuO1Wf s
  {
pEQ~ U2t%]B,X0
  int result = 0; file://一个标志位,如果是0表示程序没有出错,如果是1表明有错误发生
[4pG G:hCe0
  Cell cell = new Cell( );
8Vz_2CCw2RX,I051Testing软件测试网{YF;Vq"A.z2j
  //下面使用cell初始化CellProdCellCons两个类,生产和消费次数均为20
^e3v:gM-Qm*rg0
  CellProd prod = new CellProd(cell, 20);
2j6_/b i#^Zo0
  CellCons cons = new CellCons(cell, 20);
;v4vs&NB2Z)I~3|Z051Testing软件测试网%vW3nO&ct
  Thread producer = new Thread(new ThreadStart(prod.ThreadRun));51Testing软件测试网2X0MF!|zq0j!O~2Fk
  Thread consumer = new Thread(new ThreadStart(cons.ThreadRun));51Testing软件测试网1Ko.Q#Uyz]7E)`4E
  //生产者线程和消费者线程都已经被创建,但是没有开始执行51Testing软件测试网zl4f U"W t;\O4x.m

J!RE;x)BXd7l V0
  try51Testing软件测试网]2vl)xB,T%L
  {51Testing软件测试网-}*U/?\._:c2q#cn(L
    producer.Start( );51Testing软件测试网b%Y!XL4h7Lz
    consumer.Start( );
'O|Jcs,C(n&k0
um]&fz(e0
    producer.Join( );
4D@m? I0
    consumer.Join( );
HAx ^$p9NE`)n0
    Console.ReadLine();
)h!um @7U8W-v0
  }
3t4GR8@-P!J\'WgL0
  catch (ThreadStateException e)51Testing软件测试网/e,@3t+E v2T
  {51Testing软件测试网b|!w?e8G.Z
    file://当线程因为所处状态的原因而不能执行被请求的操作51Testing软件测试网/W'z e1[!r}4\V~
    Console.WriteLine(e);
;NZ(ta5Hg0
    result = 1;51Testing软件测试网#MOG2Q(~ | f$IR'I
  }
)x6j6s5cgZV0
  catch (ThreadInterruptedException e)51Testing软件测试网j V/YCt.aC$O V \
  {
r5Wf(w-n&~G0
    file://当线程在等待状态的时候中止51Testing软件测试网(M*}^#?$B
    Console.WriteLine(e);
sf}P&I4n&z0
    result = 1;51Testing软件测试网1F;]XY a(BC A \u-W
  }51Testing软件测试网*m^PL f
  //尽管Main()函数没有返回值,但下面这条语句可以向父进程返回执行结果
#C!PD Qg;k0
  Environment.ExitCode = result;51Testing软件测试网8M]"W%}'LL S
  }51Testing软件测试网 |p,v#}!n*g
}
public class CellProd51Testing软件测试网(G!t0Rz"S2Ay3U
{
dS?0b;jY9R2S)r0
  Cell cell; //被操作的Cell对象51Testing软件测试网4}-Lbq(S)A7h5h4G
  int quantity = 1; //生产者生产次数,初始化为151Testing软件测试网&p#o5z3of,B
51Testing软件测试网fH0Ift1r u_m
  public CellProd(Cell box, int request)
/S&i-g{;`H~dW#u\0
  {51Testing软件测试网l@)lw f"mPg!?.V
  //构造函数
@/cK;x9bcR)iYF0
  cell = box;
c.K-b5oxe8T:aQM N0
  quantity = request;
'@G H;Ywna0
  }
Z-Cn.}-gk}0
  public void ThreadRun( )51Testing软件测试网7M7~t}F;c4E
  {51Testing软件测试网R fw~a4GoE }
  for(int looper=1; looper<=quantity; looper++)51Testing软件测试网_'_ l ClhD3v
    cell.WriteToCell(looper); file://生产者向操作对象写入信息51Testing软件测试网~:aq\ i8|D"c
  }
9]9c&aE wvn QU0
}51Testing软件测试网/pj9FT#Q^x.~3G4Wp

9}4~E9O$?0public class CellCons51Testing软件测试网"m|j{#s-a
{
a"d bn!MB0
  Cell cell;51Testing软件测试网j8Bk[2X"[ _
  int quantity = 1;
T.{;@ [A.R w%N)p0
3V R'[5i2k&P3I L0
  public CellCons(Cell box, int request)
-d:?L ~"k[y0oJ@0
  {
8p\i$_wz&ShB0
  cell = box;
J6w7V)BQs*Ls"X0
  quantity = request;
]-JHtj-Wd o*j1L8F8M5U0
  }51Testing软件测试网8J l5q8Yni%o
  public void ThreadRun( )51Testing软件测试网-Z+X&dV U[8Og
  {51Testing软件测试网5f~ P:T-j7Y
  int valReturned;
*W!u wt!t j0
  for(int looper=1; looper<=quantity; looper++)
-^k2]!^H;h!DzS0
    valReturned=cell.ReadFromCell( );//消费者从操作对象中读取信息
B'u2KAwib0
  }51Testing软件测试网%J+_jI v2?ps2D
}
大家可以看到,在上面的例程中,同步是通过等待Monitor.Pulse()来完成的。首先生产者生产了一个值,而同一时刻消费者处于等待状态,直到收到生产者的脉冲(Pulse)”通知它生产已经完成,此后消费者进入消费状态,而生产者开始等待消费者完成操作后将调用Monitor.Pulese()发出的脉冲。它的执行结果很简单:
Produce: 1
R:S#aj Q)?0Consume: 151Testing软件测试网(B'] J+J6P/},S~;q
Produce: 2
s4c\G-UxR0Consume: 2
t!Q0wWE!}}c6fzK0Produce: 351Testing软件测试网e-R7mTQ)p*GHJ z
Consume: 3
1^7j9a!Q+X TLC0...51Testing软件测试网FukeqU2k"X
...51Testing软件测试网9D$S})l|T{.FQQ
Produce: 20
FG({Z)S}(|Y0Consume: 20
事实上,这个简单的例子已经帮助我们解决了多线程应用程序中可能出现的大问题,只要领悟了解决线程间冲突的基本方法,很容易把它应用到比较复杂的程序中去。 

TAG: 软件开发

 

评分:0

我来说两句

Open Toolbar