Windows下多线程程序设计及其线程同步问题.doc_第1页
Windows下多线程程序设计及其线程同步问题.doc_第2页
Windows下多线程程序设计及其线程同步问题.doc_第3页
Windows下多线程程序设计及其线程同步问题.doc_第4页
Windows下多线程程序设计及其线程同步问题.doc_第5页
已阅读5页,还剩23页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

Windows下多线程程序设计及其线程同步问题1. 基本概念什么是线程?线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。什么是多线程?多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。多线程的好处: 可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。 多线程的不利方面: 线程也是程序,所以线程需要占用内存,线程越多占用内存也越多; 多线程需要协调和管理,所以需要CPU时间跟踪线程; 线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题; 线程太多会导致控制太复杂,最终可能造成很多Bug;任何程序在执行时,至少有一个主线程。一个直观印象的线程示例:/SystemThread.csusingSystem;usingSystem.Threading;namespaceThreadTestclassRunItstaticvoidMain(stringargs)Thread.CurrentThread.Name=SystemThread;/给当前线程起名为SystemThread Console.WriteLine(Thread.CurrentThread.Name+Status:+Thread.CurrentThread.ThreadState);Console.ReadLine();输出如下: System Threads Status:Running在这里,我们通过Thread类的静态属性CurrentThread获取了当前执行的线程,对其Name属性赋值“System Thread”,最后还输出了它的当前状态(ThreadState)。所谓静态属性,就是这个类所有对象所公有的属性,不管你创建了多少个这个类的实例,但是类的静态属性在内存中只有一个。很容易理解CurrentThread为什么是静态的虽然有多个线程同时存在,但是在某一个时刻,CPU只能执行其中一个。 在程序的头部,我们使用了如下命名空间: using System.Threading; 在.net framework class library中,所有与多线程机制应用相关的类都是放在System.Threading命名空间中的。如果你想在你的应用程序中使用多线程,就必须包含这个类。2. 多线程程序设计实例下面我们就动手来创建一个线程,使用Thread类创建线程时,只需提供线程入口即可。(线程入口使程序知道该让这个线程干什么事)在C#中,线程入口是通过ThreadStart代理(delegate)来提供的,你可以把ThreadStart理解为一个函数指针,指向线程要执行的函数,当调用Thread.Start()方法后,线程就开始执行ThreadStart所代表或者说指向的函数。 打开你的VS.net,新建一个控制台应用程序(Console Application),编写完全控制一个线程的代码示例:using System;using System.Threading;namespace THREAD public class Alpha public void Beta() while (true) Console.WriteLine(Alpha.Beta runtime + (System.DateTime.Now - Program.t).TotalMilliseconds); class Program public static DateTime t; static void Main(string args) Alpha oAlpha = new Alpha(); /file:/这里创建一个线程,使之执行Alpha类的Beta()方法 Thread oThread = new Thread(new ThreadStart(oAlpha.Beta); t = System.DateTime.Now; oThread.Start(); Console.WriteLine(Sleep begin ); Thread.Sleep(25); Console.WriteLine(Sleep time is : + (System.DateTime.Now - t).TotalMilliseconds); oThread.Abort(); oThread.Join(); Console.WriteLine(Alpha.Beta has finished); try Console.WriteLine(Try to restart the Alpha.Beta thread); oThread.Start(); catch (ThreadStateException) Console.Write(ThreadStateException trying to restart Alpha.Beta. ); Console.WriteLine(Expected since aborted threads cannot be restarted.); Console.ReadLine(); 这段程序包含两个类Alpha和Simple,在创建线程oThread时我们用指向Alpha.Beta()方法的初始化了ThreadStart代理(delegate)对象,当我们创建的线程oThread调用oThread.Start()方法启动时,实际上程序运行的是Alpha.Beta()方法:Alpha oAlpha = new Alpha(); Thread oThread = new Thread(new ThreadStart(oAlpha.Beta); oThread.Start();然后我们试图用Thread.Abort()方法终止线程oThread,注意后面的oThread.Join(),Thread.Join()方法使主线程等待,直到oThread线程结束。你可以给Thread.Join()方法指定一个int型的参数作为等待的最长时间。之后,我们试图用Thread.Start()方法重新启动线程oThread,但是显然Abort()方法带来的后果是不可恢复的终止线程,所以最后程序会抛出ThreadStateException异常。小贴士:(1)主线程Main()函数所有线程都是依附于Main()函数所在的线程的,Main()函数是C#程序的入口,起始线程可以称之为主线程。如果所有的前台线程都停止了,那么主线程可以终止,而所有的后台线程都将无条件终止。所有的线程虽然在微观上是串行执行的,但是在宏观上你完全可以认为它们在并行执行。(2)Thread.ThreadState 属性这个属性代表了线程运行时状态,在不同的情况下有不同的值,我们有时候可以通过对该值的判断来设计程序流程。ThreadState 属性的取值如下:Aborted:线程已停止; AbortRequested:线程的Thread.Abort()方法已被调用,但是线程还未停止; Background:线程在后台执行,与属性Thread.IsBackground有关; Running:线程正在正常运行; Stopped:线程已经被停止; StopRequested:线程正在被要求停止; Suspended:线程已经被挂起(此状态下,可以通过调用Resume()方法重新运行);SuspendRequested:线程正在要求被挂起,但是未来得及响应; Unstarted:未调用Thread.Start()开始线程的运行; WaitSleepJoin:线程因为调用了Wait(),Sleep()或Join()等方法处于封锁状态;3. 多线程共享资源时出现的问题【例1】有两个线程A和B共享变量count,A负责统计通过车辆的计数,即完成count=count+1的操作;B负责输出count中的值,并将其置0。设计代码如下:class CountMobile private int count = 0; public void CountM() int n = 0; while (n10) count+; Console.WriteLine(来一辆车,统计); Thread.Sleep(500); n+; public void PrintM() int n = 0; while (n10) Console.WriteLine(输出:0,count); count = 0; Thread.Sleep(2000); n+; class Program static void Main(string args) CountMobile CC = new CountMobile(); Thread td1 = new Thread(new ThreadStart(CC.CountM ); Thread td2 = new Thread(new ThreadStart(CC.PrintM ); td1.Start(); td2.Start(); Console.Read(); 程序运行结果如下:很明显车辆数被少统计了两个。执行多次,得到的结果可能还不一样。程序的执行变得不确定了。造成这个问题的原因就是因为CountM和PrintM两个并发线程共享了变量count。【例2】类SharedState演示了如何使用线程共享的状态,并保存一个整数值。public class SharedStateprivate int state = 0;public int Stateget return state; set state = value; 类Task包含方法DoTheTask(),该方法是新线程的入口点。在其实现代码中,将SharedState的State递增50000次。变量sharedState在这个类的构造函数中初始化:public class TaskSharedState sharedState;public Task(SharedState sharedState)this.sharedState = sharedState; public void DoTheTask() for (int i = 0; i 50000; i+) sharedState.State += 1; 在Main()方法中,创建一个SharedState对象,并传送给20个Thread对象的构造函数。在启动所有的线程后,Main()方法进入另一个循环,使20个线程处于等待状态,直到所有的线程都执行完毕为止。线程执行完毕后,把共享状态的合计值写入控制台。因为执行了50000个循环,有20个线程,所以写入控制台的值应是1000000。但是,事实常常并非如此。class Programstatic void Main() int numThreads = 20; SharedState state = new SharedState();Thread threads = new ThreadnumThreads;for (int i = 0; i numThreads; i+) threadsi = new Thread(new Task(state).DoTheTask);threadsi.Start();for (int i = 0; i numThreads; i+)threadsi.Join();Console.WriteLine(summarized 0, state.State);多次运行应用程序的结果如下所示:summarized 939270summarized 993799summarized 998304summarized 937630每次运行的结果都不同,但没有一个结果是正确的。调试版本和发布版本的区别很大。所使用的CPU类型不同,结果也不一样。如果将循环次数改为比较小的值,就会多次得到正确的值,但不是每次。这个应用程序非常小,很容易看出问题,但该问题的原因在大型应用程序中就很难确定。4. C#中的同步机制参见:/little%5Fgrass/blog/item/0cf220fad515c59159ee902a.html4.1 Lock用lock语句定义的对象表示,要等待指定对象的锁定解除。只能传送引用类型。锁定值类型只是锁定了一个副本,这是没有什么意义的。编译器会提供一个锁定值类型的错误。进行了锁定后 只有一个线程得到了锁定块,就可以运行lock语句块。在lock语句块的最后,对象的锁定被解除,另一个等待锁定的线程就可以获得该锁定块了。lock (obj)/ synchronized regionlock 确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。【锁住一段代码】class CountMobile private int count = 0; private Object thislock = new Object(); public void CountM() int n = 0; while (n10) lock (thislock) count+; Console.WriteLine(来一辆车,统计); Thread.Sleep(300); n+; public void PrintM() int n = 0; while (n10) lock (thislock) Console.WriteLine(输出:0, count); count = 0; Thread.Sleep(1000); n+; 例2:银行取款下例使用线程和lock。只要 lock 语句存在,语句块就是临界区并且 balance 永远不会是负数。using System;using System.Threading;class Account private Object thisLock = new Object(); int balance; Random r = new Random(); /生成一个随机数对象,用于产生随机数 public Account(int initial) /Account类的构造函数 balance = initial; int Withdraw(int amount) /取钱,amount参数表示取钱的数量 / This condition will never be true unless the lock statement / is commented out: if (balance = amount) Console.WriteLine(Balance before Withdrawal : + balance); Console.WriteLine(Amount to Withdraw : - + amount); balance = balance - amount; Console.WriteLine(Balance after Withdrawal : + balance); return amount; else return 0; / transaction rejected public void DoTransactions() for (int i = 0; i 100; i+) Withdraw(r.Next(1, 100); class Test static void Main() Thread threads = new Thread10; Account acc = new Account(1000); for (int i = 0; i 10; i+) Thread t = new Thread(new ThreadStart(acc.DoTransactions); threadsi = t; for (int i = 0; i 10; i+) threadsi.Start(); Console.Read();扩充知识【有点难,可根据自己的情况选读】在以前编程中遇到lock的问题总是使用lock(this)一锁了之,出问题后翻看MSDN突然发现下面几行字:通常,应避免锁定 public 类型,否则实例将超出代码的控制范围。常见的结构 lock (this)、lock (typeof (MyType) 和 lock (myLock) 违反此准则:如果实例可以被公共访问,将出现 lock (this) 问题。如果 MyType 可以被公共访问,将出现 lock (typeof (MyType) 问题。由于进程中使用同一字符串的任何其他代码将共享同一个锁,所以出现 lock(“myLock”) 问题。来看看lock(this)的问题:如果有一个类Class1,该类有一个方法用lock(this)来实现互斥: public void Method2() lock (this) System.Windows.Forms.MessageBox.Show(Method2 End); 如果在同一个Class1的实例中,该Method2能够互斥的执行。但是如果是2个Class1的实例分别来执行Method2,是没有互斥效果的。因为这里的lock,只是对当前的实例对象进行了加锁。 Lock(typeof(MyType)锁定住的对象范围更为广泛,由于一个类的所有实例都只有一个类型对象(该对象是typeof的返回结果),锁定它,就锁定了该对象的所有实例,微软现在建议(原文请参考:/china/MSDN/library/enterprisedevelopment/softwaredev/SDaskgui06032003.mspx?mfr=true)不要使用lock(typeof(MyType),因为锁定类型对象是个很缓慢的过程,并且类中的其他线程、甚至在同一个应用程序域中运行的其他程序都可以访问该类型对象,因此,它们就有可能代替您锁定类型对象,完全阻止您的执行,从而导致你自己的代码的挂起。 锁住一个字符串更为神奇,只要字符串内容相同,就能引起程序挂起。原因是在.NET中,字符串会被暂时存放,如果两个变量的字符串内容相同的话,.NET会把暂存的字符串对象分配给该变量。所以如果有两个地方都在使用lock(“my lock”)的话,它们实际锁住的是同一个对象。到此,微软给出了个lock的建议用法:锁定一个私有的static 成员变量。 .NET在一些集合类中(比如ArrayList,HashTable,Queue,Stack)已经提供了一个供lock使用的对象SyncRoot,用Reflector工具查看了SyncRoot属性的代码,在Array中,该属性只有一句话:return this,这样和lock array的当前实例是一样的。ArrayList中的SyncRoot有所不同 get if (this._syncRoot = null) Interlocked.CompareExchange(ref this._syncRoot, new object(), null); return this._syncRoot; 其中Interlocked类是专门为多个线程共享的变量提供原子操作(如果你想锁定的对象是基本数据类型,那么请使用这个类),CompareExchange方法将当前syncRoot和null做比较,如果相等,就替换成new object(),这样做是为了保证多个线程在使用syncRoot时是线程安全的。集合类中还有一个方法是和同步相关的:Synchronized,该方法返回一个对应的集合类的wrapper类,该类是线程安全的,因为他的大部分方法都用lock来进行了同步处理,比如Add方法: public override void Add(object key, object value) lock (this._table.SyncRoot) this._table.Add(key, value); 这里要特别注意的是MSDN提到:从头到尾对一个集合进行枚举本质上并不是一个线程安全的过程。即使一个集合已进行同步,其他线程仍可以修改该集合,这将导致枚举数引发异常。若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合: Queue myCollection = new Queue(); lock(myCollection.SyncRoot) foreach (Object item in myCollection) / Insert your code here. 4.2 MonitorMonitor类可以锁定一个对象,一个线程只有得到这把锁才可以对该对象进行操作。对象锁机制保证了在可能引起混乱的情况下一个时刻只有一个线程可以访问这个对象。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()方法通知等待队列中的第一个线程,于是该线程被转移到预备队列中,当对象锁被释放时,在预备队列中的线程可以立即获得对象锁。class CountMobile private int count = 0; private Object thislock = new Object(); public void CountM() int n = 0; while (n10) Monitor.Enter(thislock); count+; Console.WriteLine(来一辆车,统计); Monitor.Exit(thislock); Thread.Sleep(300); n+; public void PrintM() int n = 0; while (n10) Monitor.Enter(thislock); Console.WriteLine(输出:0, count); count = 0; Monitor.Exit(thislock); Thread.Sleep(1000); n+; 小贴士:与C#的lock语句相比,Monitor类的主要优点是:可以添加一个等待获得锁定的超时值。这样就不会无限期地等待获得锁定,而可以使用TryEnter方法,给它传送一个超时值,确定等待获得锁定的最长时间。如果得到了obj的锁定,TryEnter方法就返回true,访问由对象obj锁定的状态。如果另一个线程锁定obj的时间超过了500毫秒,TryEnter方法就返回false,线程不再等待,而是执行其他操作。也许在以后,该线程会尝试再次获得该锁定。if (Monitor.TryEnter(obj, 500)try / acquired the lock/ synchronized region for objfinallyMonitor.Exit(obj);else/ didnt get the lock, do something else例2:这个程序的思路是共同做一件事情(从一个ArrayList中删除元素),如果执行完成了,两个线程都停止执行。usingSystem;usingSystem.Collections;usingSystem.Collections.Generic;usingSystem.Threading;/在开发中经常会遇到线程的例子,如果某个后台操作比较费时间,我们就可以启动一个线程去执行那个费时的操作,同时程序继续执行。在某些情况下可能会出现多个线程的同步协同的问题,下面的例子就展示了在两个线程之间如何协同工作。/这个程序的思路是共同做一件事情(从一个ArrayList中删除元素),如果执行完成了,两个线程都停止执行。/原发地址:/zhoufoxcn/publicclassThreadDemoprivateThreadthreadOne;privateThreadthreadTwo;privateArrayListstringList;privateeventEventHandlerOnNumberClear;/数据删除完成引发的事件publicstaticvoidMain()ThreadDemodemo=newThreadDemo(1000);demo.Action();Console.Read();publicThreadDemo(intnumber)Randomrandom=newRandom(1000000);stringList=newArrayList(number);for(inti=0;inumber;i+)stringList.Add(random.Next().ToString();threadOne=newThread(newThreadStart(Run);/两个线程共同做一件事情threadTwo=newThread(newThreadStart(Run);/两个线程共同做一件事情threadOne.Name=线程1;threadTwo.Name=线程2;OnNumberClear+=newEventHandler(ThreadDemo_OnNumberClear);/开始工作/publicvoidAction()threadOne.Start();threadTwo.Start();/共同做的工作/privatevoidRun()stringstringValue=null;while(true)Monitor.Enter(this);/锁定,保持同步stringValue=(string)stringList0;Console.WriteLine(Thread.CurrentThread.Name+删除了+stringValue);stringList.RemoveAt(0);/删除ArrayList中的元素if(stringList.Count=0)OnNumberClear(this,newEventArgs();/引发完成事件Monitor.Exit(this);/取消锁定Thread.Sleep(5);/执行完成之后,停止所有线程voidThreadDemo_OnNumberClear(objectsender,EventArgse)Console.WriteLine(执行完了,停止了所有线程的执行。);threadTwo.Abort();threadOne.Abort();内容补充【根据情况自己选读】lock关键字比Monitor简洁,其实lock就是对Monitor的Enter和Exit的一个封装。另外Monitor还有几个常用的方法:TryEnter能够有效的决绝长期死等的问题,如果在一个并发经常发生,而且持续时间长的环境中使用TryEnter,可以有效防止死锁或者长时间的等待。比如我们可以设置一个等待时间bool gotLock = Monitor.TryEnter(myobject,1000),让当前线程在等待1000秒后根据返回的bool值来决定是否继续下面的操作。Pulse以及PulseAll还有Wait方法是成对使用的,它们能让你更精确的控制线程之间的并发,MSDN关于这3个方法的解释很含糊,有必要用一个具体的例子来说明一下: using System.Threading; public class Program static object ball = new object(); public static void Main() Thread threadPing = new Thread( new ThreadStart(ThreadPingProc ); Thread threadPong = new Thread(new ThreadStart(ThreadPongProc) ); threadPing.Start(); threadPong.Start(); static void ThreadPongProc() System.Console.WriteLine(ThreadPong: Hello!); lock ( ball ) for (int i = 0; i 5; i+) System.Console.WriteLine(ThreadPong: Pong ); Monitor.Pulse( ball ); Monitor.Wait( ball ); System.Console.WriteLine(ThreadPong: Bye!); static void ThreadPingProc() System.Console.WriteLine(ThreadPing: Hello!); lock ( ball ) for(int i=0; i 5; i+) System.Console.WriteLine(ThreadPing: Ping ); Monitor.Pulse( ball ); Monitor.Wait( ball ); System.Console.WriteLine(ThreadPing: Bye!); 当threadPing进程进入ThreadPingProc锁定ball并调用Monitor.Pulse( ball );后,它通知threadPong从阻塞队列进入准备队列,当threadPing调用Monitor.Wait( ball )阻塞自己后,它放弃了了对ball的锁定,所以threadPong得以执行。PulseAll与Pulse方法类似,不过它是向所有在阻塞队列中的进程发送通知信号,如果只有一个线程被阻塞,那么请使用Pulse方法。4.3 Mutex在使用上,Mutex与上述的Monitor比较接近,不过Mutex不具备Wait,Pulse,PulseAll的功能,因此,我们不能使用Mutex实现类似的唤醒的功能,不过Mutex有一个比较大的特点,Mutex是跨进程的,因此我们可以在同一台机器甚至远程的机器上的多个进程上使用同一个互斥体。考

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论