`
897371388
  • 浏览: 526697 次
文章分类
社区版块
存档分类
最新评论

[C#学习笔记之多线程2]多线程同步与并发访问共享资源工具—Lock、Monitor、Mutex、Semaphore

 
阅读更多

“线程同步”的含义


当一个进程启动了多个线程时,如果需要控制这些线程的推进顺序(比如A线程必须等待B和C线程执行完毕之后才能继续执行),则称这些线程需要进行“线程同步(thread synchronization)”。

线程同步的道理虽然简单,但却是给多线程开发带来复杂性的根源之一。当线程同步不好时,有可能会出现一种特殊的情形——死锁(Dead Lock)


“死锁”的含义


死锁表示系统进入了一个僵化状态,所有线程都没有执行完毕,但却谁也没法继续执行。究其根源,是因为“进程推进顺序不当”和“资源共享”。如例:

1)进程推进顺序不当造成死锁

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace JoinLeadToDeadlock
{
    class Program
    {
        static Thread mainThread;
        static void Main(string[] args)
        {
            Console.WriteLine("主线程开始运行");
            mainThread = Thread.CurrentThread;

            Thread ta = new Thread(new ThreadStart(ThreadAMethod));
            ta.Start();  //线程A开始执行
            Console.WriteLine("主线程等待线程A结束……");
            ta.Join();    //等待线程A结束
            Console.WriteLine("主线程退出");
        }

        static void ThreadAMethod()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine(Convert.ToString(i) + ": 线程A正在执行");
                Thread.Sleep(1000);
            }
            Console.WriteLine("线程A等待主线程退出……");
            mainThread.Join();  //等待主线程结束
        }
    }
}
在该例中,主线程mainThread先开始执行,然后启动线程ta,线程ta执行结束前又要等待mainThread线程执行结束,这样就出现了“交叉等待”的局面,必然死锁!


2)共享资源造成死锁

所谓“共享资源”,指的是多个线程可以同时访问的数据结构、文件等信息实体。

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;


namespace SharedResourceLeadToDeadlock
{
    class Program
    {
        //共享资源
        static SharedResource R1 = new SharedResource();
        static SharedResource R2 = new SharedResource();
        
        static void Main(string[] args)
        {
            Thread th1 = new Thread(UseSharedResource1);
            Thread th2 = new Thread(UseSharedResource2);
            th1.Start();
            th2.Start();
            //等待两线程运行结束
            th1.Join();
            th2.Join();
        }

        static void UseSharedResource1()
        {
            System.Console.WriteLine("线程{0}申请使用资源R1", Thread.CurrentThread.ManagedThreadId);
            Monitor.Enter(R1);  //对R1加锁
            System.Console.WriteLine("线程{0}独占使用资源R1", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(1000);
            System.Console.WriteLine("线程{0}申请使用资源R2", Thread.CurrentThread.ManagedThreadId);
            Monitor.Enter(R2);  //对R2加锁
            System.Console.WriteLine("线程{0}独占使用资源R2", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(1000);
            System.Console.WriteLine("线程{0}资源R2使用完毕,放弃", Thread.CurrentThread.ManagedThreadId);
            Monitor.Exit(R2);   //对R2解锁
            System.Console.WriteLine("线程{0}资源R1使用完毕,放弃", Thread.CurrentThread.ManagedThreadId);
            Monitor.Exit(R1);  //对R1解锁
        }

        static void UseSharedResource2()
        {
            System.Console.WriteLine("线程{0}申请使用资源R2", Thread.CurrentThread.ManagedThreadId);
            Monitor.Enter(R2);   //对R2加锁
            System.Console.WriteLine("线程{0}独占使用资源R2", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(500);
            System.Console.WriteLine("线程{0}申请使用资源R1", Thread.CurrentThread.ManagedThreadId);
            Monitor.Enter(R1);   //对R1加锁
            System.Console.WriteLine("线程{0}独占使用资源R1", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(500);
            System.Console.WriteLine("线程{0}资源R1使用完毕,放弃", Thread.CurrentThread.ManagedThreadId);
            Monitor.Exit(R1);  //对R1解锁
            System.Console.WriteLine("线程{0}资源R2使用完毕,放弃", Thread.CurrentThread.ManagedThreadId);
            Monitor.Exit(R2);   //对R2解锁
        }
    }

    class SharedResource
    {
    }
}

在该例中,线程th1执行时先申请使用R1,然后再申请使用R2,而线程th2执行时先申请R2,然后再申请R1,这样对于线程th1和th2,就会造成各自拥有一个对方需要的资源部释放,而又同时申请一个对方已经占有的资源,必然会造成死锁。


多线程数据存取错误


当多个线程访问同一个数据时,如果不对读和写的顺序作出限定,例如一个线程正在读而另一个数据尝试写,则读数据的线程得到的数据就可能出错。这也是多线程带来的问题。如例:
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace SharedResourceLeadToDataError
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread[] ths = new Thread[4];
            for (int i = 0; i < 4; i++)
            {
                ths[i]=new Thread(increaseCount);
                ths[i].Start();
            }
            System.Console.ReadKey();
        }

        static void increaseCount()
        {
            Random ran = new Random();
            Thread.Sleep(ran.Next(100, 5000));
            int beginNum = SharedResource.Count;
            System.Console.WriteLine("线程 {0} 读到的起始值为 {1}  ", Thread.CurrentThread.ManagedThreadId, beginNum );
            for (int i = 0; i < 10000; i++)
            {
               beginNum ++;
            }
            SharedResource.Count = beginNum;
            System.Console.WriteLine("线程 {0} 结束,SharedResource.Count={1}", Thread.CurrentThread.ManagedThreadId,SharedResource.Count);
        }
    }

    class SharedResource
    {
        public static int Count = 0;
    }
}

四个线程同时读写共享变量ShareResource.Count,由于未对读写进行控制,所以必然会造成数据存取错误!

线程同步与并发访问控制手段


正如为了解决车辆交通问题,人们建立了红绿灯的交通控制手段一样,可以为线程设定一套控制机制,以实现线程间的同步,以及保证以正确的顺序来访问共享资源。为了保护应用程序的资源不被破坏,为多线程程序提供了三种加锁的机制,分别是:Monitor类、Lock关键字和Mutex类。

1、Monitor类


(1)使用方法


  • Monitor对象的Enter方法可用于向共享资源申请一把“独占锁”。当一个线程拥有特定共享资源的独占锁时,尝试访问同一共享资源的其他线程只能等待。
  • Monitor对象的Exit方法用于释放锁。
  • 要注意:Enter与Exit方法必须严格配对,否则,有可能出现死锁情况。
  • Monitor可以锁定单个对象,也可以锁定一个类型的静态字段或属性
1).Monitor.Enter(共享资源对象);
2).Monitor.Enter(typeof(共享资源类型));
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

//展示用Monitor访问共享资源
namespace UseMonitor1
{
    class Program
    {
        static void Main(string[] args)
        {
            SharedResource obj = new SharedResource();

            Thread[] ths = new Thread[4];
            for (int i = 0; i < 4; i++)
            {
                ths[i] = new Thread(increaseCount);
                ths[i].Start(obj);
            }
            System.Console.ReadKey();
        }
        static void increaseCount(Object obj)
        {
           //访问实例字段
            VisitDynamicField(obj);
            //访问静态字段
            VisitStaticField();
        }

        //访问静态字段
        private static void VisitStaticField()
        {
            //访问静态字段
            Monitor.Enter(typeof(SharedResource));

            int beginNumber = SharedResource.StaticCount;
            System.Console.WriteLine("线程 {0} 读到的StaticCount起始值为 {1}  ", Thread.CurrentThread.ManagedThreadId, beginNumber);
            for (int i = 0; i < 10000; i++)
            {
                beginNumber++;
            }
            SharedResource.StaticCount = beginNumber;
            System.Console.WriteLine("线程 {0} 结束, SharedResource.StaticCount={1}",
            Thread.CurrentThread.ManagedThreadId, SharedResource.StaticCount);

            Monitor.Exit(typeof(SharedResource));
        }

        //访问实例字段
        private static void VisitDynamicField(Object obj)
        {
            Monitor.Enter(obj);

            int beginNumber = (obj as SharedResource).DynamicCount;
            System.Console.WriteLine("线程 {0} 读到的DynamicCount起始值为 {1}  ", Thread.CurrentThread.ManagedThreadId, beginNumber);
            for (int i = 0; i < 10000; i++)
            {
                beginNumber++;
            }
            (obj as SharedResource).DynamicCount = beginNumber;
            System.Console.WriteLine("线程 {0} 结束,Obj.DynamicCount={1}",
            Thread.CurrentThread.ManagedThreadId, (obj as SharedResource).DynamicCount);

            Monitor.Exit(obj);
        }
    }
    //共享资源类
    class SharedResource
    {
        public int DynamicCount = 0;        //多线程共享的实例字段
        public static int StaticCount = 0;  //多线程共享的静态字段
    }
}
Monitor类的使用模板:

Monitor.Enter(共享资源对象); //申请对象锁

//得到了对象锁,可以对共享资源进行访问,
//其他线程只能等待
//访问共享资源
//对共享资源的访问完成,释放对象锁,
//让其他线程有机会访问共享资源

Monitor.Exit(obj);

(2)Monitor的特殊注意之处:


Monitor一般只用于访问引用类型的共享资源,如果将其施加于值类型变量,则值类型变量将会被装箱,而当调用Exit方法时,虽然是同一个值类型变量,但实际上此值类型变量又会被第二次装箱,这将导致Enter方法所访问的对象与Exit方法所访问的不是同一个,Monitor对象将会引发SynchronizationLockException。
因此,不要将Monitor用于值类型!

(3)Monitor.Wait()和Monitor.Pulse()


Wait()释放对象上的锁,以便允许其他线程锁定和访问该对象。在其他线程访问对象时,调用线程将等待。
Pulse(),PulseAll()向一个或多个等待线程发送信号。该信号通知等待线程锁定对象的状态已更改,并且锁的所有者准备释放该锁。等待线程被放置在对象的就绪队列中以便它可以最后接收对象锁。一旦线程拥有了锁,它就可以检查对象的新状态以查看是否达到所需状态。PulseAll与Pulse方法类似,不过它是向所有在阻塞队列中的进程发送通知信号,如果只有一个线程被阻塞,那么请使用Pulse方法。
注意:Pulse、PulseAll和Wait方法必须从同步的代码块内调用。
例1:
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace UseMonitor2
{
    class Program
    {
        static void Main(string[] args)
        {
            //创建共享资源
            SharedResource obj = new SharedResource();
            //创建线程对象并启动
            Thread tha = new Thread(ThreadMethodA);
            Thread thb = new Thread(ThreadMethodB);
            tha.Start(obj);
            thb.Start(obj);

            //程序暂停
            System.Console.ReadKey();
        }

        static void ThreadMethodA(Object obj)
        {
            Monitor.Enter(obj);
            (obj as SharedResource).DynamicCount += 100;
            System.Console.WriteLine("线程A完成工作,obj.DynamicCount={0}", (obj as SharedResource).DynamicCount);
            Monitor.Pulse(obj); //通知B线程进入准备队列
            Monitor.Exit(obj);
        }

        static void ThreadMethodB(Object obj)
        {
             Monitor.Enter(obj);
            //A线程还未工作,因为字段保持初始值0
            //如果注释掉此条件判断语句,则有可能会发生死锁
            if((obj as SharedResource).DynamicCount == 0)
                Monitor.Wait(obj);//将本线程阻塞,进入阻塞队列等待
            (obj as SharedResource).DynamicCount += 100;
            System.Console.WriteLine("线程B完成工作,obj.DynamicCount={0}", (obj as SharedResource).DynamicCount);
            Monitor.Exit(obj);
        }
    }

    //共享资源类
    class SharedResource
    {
        public int DynamicCount = 0;        //多线程共享的实例字段
    }
}

例2:
using System.Threading;
public class Program 
{
   static object ball = new object();
   public static void Main() 
   {
      Thread threadPing = new Thread( ThreadPingProc );
      Thread threadPong = new Thread( 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: Hello!
ThreadPing: Ping
ThreadPong: Hello!
ThreadPong: Pong
ThreadPing: Ping
ThreadPong: Pong
ThreadPing: Ping
ThreadPong: Pong
ThreadPing: Ping
ThreadPong: Pong
ThreadPing: Ping
ThreadPong: Pong
ThreadPing: Bye!
当threadPing进程进入ThreadPingProc锁定ball并调用Monitor.Pulse( ball )后,它通知threadPong从阻塞队列进入准备队列,当threadPing调用Monitor.Wait( ball )阻塞自己后,它放弃了了对ball的锁定,所以threadPong得以执行。
因此,可以借助Monitor.Pulse()来控制进程的推进顺序。
//A线程执行的代码
lock(obj)
{
   //访问共享资源obj
   Monitor.Pulse(obj); //通知B 线程可以访问共享资源obj了
}
---------------------------------------------------------------
//B线程执行的代码
lock(obj)
{
   Monitor.Wait(obj); //等待A 线程完成
   //访问共享资源obj
}

2、Lock关键字

C#使用Lock关键字来简化Monitor的用法。lock就是对Monitor的Enter和Exit的一个封装,而且使用起来更简洁,因此Monitor类的Enter()和Exit()方法的组合使用可以用lock关键字替代。

lock(obj)
{
//访问共享资源代码段
}
等价于:
Monitor.Enter(obj);
//访问共享资源代码段
Monitor.Exit(obj);

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

//展示用Monitor访问共享资源
namespace UseMonitor1
{
    class Program
    {
        static void Main(string[] args)
        {
            SharedResource obj = new SharedResource();

            Thread[] ths = new Thread[4];
            for (int i = 0; i < 4; i++)
            {
                ths[i] = new Thread(increaseCount);
                ths[i].Start(obj);
            }
            System.Console.ReadKey();
        }
        static void increaseCount(Object obj)
        {
           //访问实例字段
            VisitDynamicField(obj);
            //访问静态字段
            VisitStaticField();
        }

        //访问静态字段
        private static void VisitStaticField()
        {
            //访问静态字段
            lock (typeof(SharedResource))
            {
                int beginNumber = SharedResource.StaticCount;
                System.Console.WriteLine("线程 {0} 读到的StaticCount起始值为 {1}  ", Thread.CurrentThread.ManagedThreadId, beginNumber);
                for (int i = 0; i < 10000; i++)
                {
                    beginNumber++;
                }
                SharedResource.StaticCount = beginNumber;
                System.Console.WriteLine("线程 {0} 结束, SharedResource.StaticCount={1}",
                Thread.CurrentThread.ManagedThreadId, SharedResource.StaticCount);
            }
        }

        //访问实例字段
        private static void VisitDynamicField(Object obj)
        {
            lock (obj)
            {
                int beginNumber = (obj as SharedResource).DynamicCount;
                System.Console.WriteLine("线程 {0} 读到的DynamicCount起始值为 {1}  ", Thread.CurrentThread.ManagedThreadId, beginNumber);
                for (int i = 0; i < 10000; i++)
                {
                    beginNumber++;
                }
                (obj as SharedResource).DynamicCount = beginNumber;
                System.Console.WriteLine("线程 {0} 结束,Obj.DynamicCount={1}",
                Thread.CurrentThread.ManagedThreadId, (obj as SharedResource).DynamicCount);
            }
        }
    }
    //共享资源类
    class SharedResource
    {
        public int DynamicCount = 0;        //多线程共享的实例字段
        public static int StaticCount = 0;  //多线程共享的静态字段
    }
}

3、自旋锁SpinLock


当一个线程需要访问共享资源时,它可以调用SpinLock.Enter或SpinLock.TryEnter方法申请独占锁,如果暂时不能获得锁(这时可能运行于另一个CPU核上的线程正在访问共享资源),当前线程就会“空转”若干个时钟周期,然后再次尝试。在这个过程中,线程的状态仍是Running,从而避免了操作系统进行一次线程上下文切换所带来的开销。

public class MyType
{
    //创建自旋锁对象
    private SpinLock _spinLock = new SpinLock();
    //将被多线程执行的代码,
    //由于使用了自旋锁,可以保证被“锁定”代码一次只会被一个线程执行
    public void DoWork()
    {
        bool lockTaken = false; 
        try
        {
            _spinLock.Enter(ref lockTaken); //申请获取“锁”
            // 获得了锁,在此书写工作代码,这些工作代码不会同时被两个线程执行
        }
        finally 
        { 
        //工作完毕,或者发生异常时,检查一下当前线程是否占有了锁
        //如果占有了锁,释放它,以避免出现死锁的情况。
            if (lockTaken)  _spinLock.Exit();
        }
    }
}

4、实现原子操作——Interlocked类


Interlocked类是一种互锁操作,提供对多个线程共享的变量进行同步访问的方法,互锁操作具有原子性,即整个操作时不能由相同变量上的另一个互锁操作所中断的单元。
这个类提供了Increment、Decrement、Add静态方法用于对int或long型变量的递增、递减或相加操作。还提供了Exchange(为整型或引用对象赋值)、CompareExchange(比较后再对整型或引用对象赋值),用于为整型或引用类型的赋值提供原子操作。
在大多数计算机上,增加变量操作不是一个原子操作,需要执行下列步骤:
  1. 将实例变量中的值加载到寄存器中。
  2. 增加或减少该值。
  3. 在实例变量中存储该值。
如果不使用 Increment 和 Decrement,线程会在执行完前两个步骤后被抢先。 然后由另一个线程执行所有三个步骤。 当第一个线程重新开始执行时,它覆盖实例变量中的值,造成第二个线程执行增减操作的结果丢失。

利用Interlocked类类解决生产者-消费者关系中的竞争条件问题:(例子来自《周长发——c#面向对象编程》
// Interlocked.cs
// Interlocked示例

using System;
using System.Threading;

class Test
{
    private long bufferEmpty = 0;
    private string buffer = null;

    static void Main()
    {
        Test t = new Test();
        // 进行测试
        t.Go();
    }

    public void Go()
    {
        Thread t1 = new Thread(new ThreadStart(Producer));
        t1.Name = "生产者线程";
        t1.Start();

        Thread t2 = new Thread(new ThreadStart(Consumer));
        t2.Name = "消费者线程";
        t2.Start();

        // 等待两个线程结束
        t1.Join();
        t2.Join();
    }

    // 生产者方法
    public void Producer()
    {
        Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);

        try
        {
            for (int j = 0; j < 16; ++j)
            {
                // 等待共享缓冲区为空
                while (Interlocked.Read(ref bufferEmpty) != 0) 
                    Thread.Sleep(100);

                // 构造共享缓冲区
                Random r = new Random();
                int bufSize = r.Next() % 64;
                char[] s = new char[bufSize];
                for (int i = 0; i < bufSize; ++i)
                {
                    s[i] = (char)((int)'A' + r.Next() % 26);
                }
                buffer = new string(s);

                Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name, buffer);

                // 互锁加一,成为1,标志共享缓冲区已满
                Interlocked.Increment(ref bufferEmpty);

                // 休眠,将时间片让给消费者
                Thread.Sleep(10);
            }

            Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
        }
        catch (System.Threading.ThreadInterruptedException)
        {
            Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
        }
    }

    // 消费者方法
    public void Consumer()
    {
        Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);

        try
        {
            for (int j = 0; j < 16; ++j)
            {
                while (Interlocked.Read(ref bufferEmpty) == 0)
                    Thread.Sleep(100);

                // 打印共享缓冲区
                Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name, buffer);

                // 互锁减一,成为0,标志共享缓冲区已空
                Interlocked.Decrement(ref bufferEmpty);

                // 休眠,将时间片让给生产者
                Thread.Sleep(10);
            }

            Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
        }
        catch (System.Threading.ThreadInterruptedException)
        {
            Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
        }
    }
}

5、Mutex类


Mutex与Monitor类似,需要注意的是Mutex分两种:一种是本地Mutex一种是系统级Mutex,系统级Mutex可以用来进行跨进程间的线程的同步。尽管 mutex 可以用于进程内的线程同步,但是使用 Monitor 通常更为可取,因为监视器是专门为 .NET Framework 而设计的,因而它可以更好地利用资源。相比之下,Mutex 类是 Win32 构造的包装。尽管 mutex 比监视器更为强大,但是相对于 Monitor 类,它所需要的互操作转换更消耗计算资源。
一个线程要想访问共享资源,它必须调用Mutex对象的Wait系列方法之一提出申请。当申请得到批准的线程完成了对于共享资源的访问后,它调用Mutex对象的ReleaseMutex()方法释放对于共享资源的访问权。

利用多线程模拟3个人在ATM上多次提款操作:
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace UseATM
{
    class Program
    {
        static ATM OneATM=new ATM(); //共享资源
        static void Main(string[] args)
        {
            //向公共帐号存款2万

            Console.Write("输入公司公共帐户的金额:");
            int PublicAcountMoney =Convert.ToInt32(Console.ReadLine());
            OneATM.Deposit(PublicAcountMoney);

            Console.Write("输入ATM中的现金额:");
            int ATMLeftMoney = Convert.ToInt32(Console.ReadLine());
            OneATM.SetATMLeftMoney(ATMLeftMoney);

            System.Console.WriteLine("\n敲任意键从公共帐户中取钱,ESC键退出……\n");

            while (System.Console.ReadKey(true).Key !=ConsoleKey.Escape)
            {
                System.Console.WriteLine("");
                Thread One = new Thread(WithDrawMoney);
                Thread Two = new Thread(WithDrawMoney);
                Thread Three = new Thread(WithDrawMoney);

                //随机生成一个要提款的数额,最少100元,最高5000元
                Random ran = new Random();
                One.Start(ran.Next(100, 5000));
                Two.Start(ran.Next(100, 5000));
                Three.Start(ran.Next(100, 5000));

                //等三人取完钱
                One.Join();
                Two.Join();
                Three.Join();

                System.Console.WriteLine("公共账号剩余{0}元,ATM中可提现金:{1}", OneATM.QueryPublicAccount(),OneATM.QueryATMLeftAccount());
            }
        }

        //线程函数
        static void WithDrawMoney(object amount)
        {
            switch(OneATM.WithDraw((int)amount))
            {
                case WithDrawState.Succeed:
                    System.Console.WriteLine("成功取出{0}元。",amount );
                    break;
                case WithDrawState.ATMHasNotEnoughCash:
                    System.Console.WriteLine("ATM中现金不足,无法支取{0}元。", amount);
                    break ;
                case WithDrawState.AccountHasNotEnoughMoney:
                    System.Console.WriteLine("帐户中没钱了!无法取出{0}元",amount);
                    break ;
            }              
        }
    }

    //自助取款机
    class ATM
    {
        private int PublicAcountLeftMoney;//帐户剩余的钱
        private int ATMLeftMoney;//提款机剩余的钱

        //同步信息号量
        private Mutex m = new Mutex();

        //取钱
        public WithDrawState WithDraw(int amount)
        {
            m.WaitOne();
            //公共帐号钱不够
            if (PublicAcountLeftMoney < amount)
            {
                m.ReleaseMutex();
                return WithDrawState.AccountHasNotEnoughMoney;
            }
            //ATM现金不够
            if (ATMLeftMoney < amount)
            {
                m.ReleaseMutex();
                return WithDrawState.ATMHasNotEnoughCash;
            }
            //用户可以提取现金
            ATMLeftMoney -= amount;
            PublicAcountLeftMoney -= amount;
            m.ReleaseMutex();
            return WithDrawState.Succeed;
        }
        //存钱
        public void Deposit(int amount)
        {
            m.WaitOne();
            PublicAcountLeftMoney += amount;
            m.ReleaseMutex();
        }

        /// <summary>
        /// 设置ATM的现金金额
        /// </summary>
        /// <param name="amount"></param>
        public void SetATMLeftMoney(int amount)
        {
            Interlocked.Exchange(ref ATMLeftMoney, amount);
        }
        //获取还剩余多少钱
        public int QueryPublicAccount()
        {
            return PublicAcountLeftMoney;
        }

        /// <summary>
        /// 查询ATM剩余多少钱
        /// </summary>
        /// <returns></returns>
        public int QueryATMLeftAccount()
        {
            return ATMLeftMoney;
        }
    }
    //取款状态
    public enum WithDrawState
    {
        Succeed,        //取钱成功
        AccountHasNotEnoughMoney, //账号中没钱了
        ATMHasNotEnoughCash  //ATM中没有足够的现金
    }
}
可能的运行结果:
输入公司公共帐户的金额:200000
输入ATM中的现金额:6000000

敲任意键从公共帐户中取钱,ESC键退出……


成功取出1249元。
成功取出643元。
成功取出4958元。
公共账号剩余193150元,ATM中可提现金:5993150

成功取出1168元。
成功取出3650元。
成功取出2707元。
公共账号剩余185625元,ATM中可提现金:5985625

成功取出3866元。
成功取出402元。
成功取出2397元。
公共账号剩余178960元,ATM中可提现金:5978960

成功取出4485元。
成功取出1701元。
成功取出3354元。
公共账号剩余169420元,ATM中可提现金:5969420

Mustex与Monitor有一个很大的区别:
Mutex可以用来同步属于不同应用程序或者进程的线程,而Monitor没有这个能力。

为了说明这个区别,我们将生产者和消费者线程分别放在两个应用程序中,在两个应用程序中都各自创建一个同名的Mutex对象,并利用他们来对生产者和消费者线程同步:
生产者线程所在应用程序代码:
// Mutex1.cs
// Mutex1示例

using System;
using System.IO;
using System.Threading;
using System.Diagnostics;

class Test
{
    static void Main()
    {
        Test t = new Test();
        // 进行测试
        t.Go();
    }

    public void Go()
    {
        // 创建并启动线程
        Thread t1 = new Thread(new ThreadStart(Producer));
        t1.Name = "生产者线程";
        t1.Start();

        // 等待线程结束
        t1.Join();

        Console.WriteLine("按Enter键退出...");
        Console.Read();
    }

    // 生产者方法
    public void Producer()
    {
        Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);

        // 创建互斥体
        Mutex mutex = new Mutex(false, "CSharp_Mutex_test");

        // 启动消费者进程
        Process.Start("Mutex2.exe");

        for (int j = 0; j < 16; ++j)
        {
            try
            {
                // 进入互斥体
                mutex.WaitOne();

                FileStream fs = new FileStream(@"d:\text.txt", FileMode.OpenOrCreate, FileAccess.Write);
                StreamWriter sw = new StreamWriter(fs);
                // 构造字符串
                Random r = new Random();
                int bufSize = r.Next() % 64;
                char[] s = new char[bufSize];
                for (int i = 0; i < bufSize; ++i)
                {
                    s[i] = (char)((int)'A' + r.Next() % 26);
                }
                string str = new string(s);
                // 将字符串写入文件
                sw.WriteLine(str);
                sw.Close();

                Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name, str);
            }
            catch (System.Threading.ThreadInterruptedException)
            {
                Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
                break;
            }
            finally
            {
                // 退出互斥体
                mutex.ReleaseMutex();
            }

            // 休眠,将时间片让给消费者
            Thread.Sleep(1000);
        }

        // 关闭互斥体
        mutex.Close();
        Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
    }
}
消费者线程所在应用程序代码:
// Mutex2.cs
// Mutex2示例

using System;
using System.IO;
using System.Threading;

class Test
{
    static void Main()
    {
        Test t = new Test();
        // 进行测试
        t.Go();
    }

    public void Go()
    {
        // 创建并启动线程
        Thread t2 = new Thread(new ThreadStart(Consumer));
        t2.Name = "消费者线程";
        t2.Start();

        // 等待线程结束
        t2.Join();

        Console.WriteLine("按Enter键退出...");
        Console.Read();
    }

    // 消费者方法
    public void Consumer()
    {
        Console.WriteLine("{0}:开始执行", Thread.CurrentThread.Name);

        // 创建互斥体
        Mutex mutex = new Mutex(false, "CSharp_Mutex_test");

        for (int j = 0; j < 16; ++j)
        {
            try
            {
                // 进入互斥体
                mutex.WaitOne();

                StreamReader sr = new StreamReader(@"d:\text.txt");
                string s = sr.ReadLine();
                sr.Close();

                // 显示字符串的值
                Console.WriteLine("{0}:{1}", Thread.CurrentThread.Name, s);
            }
            catch (System.Threading.ThreadInterruptedException)
            {
                Console.WriteLine("{0}:被终止", Thread.CurrentThread.Name);
                break;
            }
            finally
            {
                // 退出互斥体
                mutex.ReleaseMutex();
            }

            // 休眠,将时间片让给消费者
            Thread.Sleep(1000);
        }

        // 关闭互斥体
        mutex.Close();
        Console.WriteLine("{0}:执行完毕", Thread.CurrentThread.Name);
    }
}

我们分别编译这两个文件,然后运行Mutex1,他会在另一个窗口中启动Mutex2,可能的结果如下:



6、Semaphore


Semaphore可以限制可同时访问某一资源或资源池的线程数。
Semaphore类在内部维护一个计数器,当一个线程调用Semaphore对象的Wait系列方法时,此计数器减一,只要计数器还是一个正数,线程就不会阻塞。当计数器减到0时,再调用Semaphore对象Wait系列方法的线程将被阻塞,直到有线程调用Semaphore对象的Release()方法增加计数器值时,才有可能解除阻塞状态。

示例说明:
图书馆都配备有若干台公用计算机供读者查询信息,当某日读者比较多时,必须排队等候。UseLibraryComputer实例用多线程模拟了多人使用多台计算机的过程

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace UseLibraryComputer
{
    class Program
    {
        //图书馆拥有的公用计算机
        private const int ComputerNum = 3;
        private static Computer[] LibraryComputers;
        //同步信号量
        public static  Semaphore sp = new Semaphore( ComputerNum, ComputerNum);

        static void Main(string[] args)
        {
            //图书馆拥有ComputerNum台电脑
            LibraryComputers = new Computer[ComputerNum];
            for (int i = 0; i <ComputerNum; i++)
                LibraryComputers[i] = new Computer("Computer"+(i+1).ToString());
            int peopleNum = 0;
            Random ran=new Random();
            Thread user;
            System.Console.WriteLine("敲任意键模拟一批批的人排队使用{0}台计算机,ESC键结束模拟……" ,ComputerNum);
            //每次创建若干个线程,模拟人排队使用计算机
            while (System.Console.ReadKey().Key  != ConsoleKey.Escape)
            {
                peopleNum = ran.Next(0, 10);
                System.Console.WriteLine("\n有{0}人在等待使用计算机。",peopleNum );

                for (int i = 1; i <= peopleNum; i++)
                {
                    user = new Thread(UseComputer);
                    user.Start("User" + i.ToString());
                }
            }
        }

        //线程函数
        static void UseComputer(Object UserName)
        {
            sp.WaitOne();//等待计算机可用
            
            //查找可用的计算机
            Computer cp=null;
            for (int i = 0; i < ComputerNum; i++)
                if (LibraryComputers[i].IsOccupied == false)
                {
                    cp = LibraryComputers[i];
                    break;
                }
            //使用计算机工作
            cp.Use(UserName.ToString());

            //不再使用计算机,让出来给其他人使用
            sp.Release();
        }
    }

    class Computer
    {
        public readonly string ComputerName = "";
        public Computer(string Name)
        {
            ComputerName = Name;
        }
        //是否被占用
        public  bool IsOccupied = false;
        //人在使用计算机
        public  void Use(String userName)
        {
            System.Console.WriteLine("{0}开始使用计算机{1}", userName,ComputerName);
            IsOccupied = true;
            Thread.Sleep(new Random().Next(1, 2000)); //随机休眠,以模拟人使用计算机
            System.Console.WriteLine("{0}结束使用计算机{1}", userName,ComputerName);
            IsOccupied = false;
        }
    }
}
可能的运行结果:

(2012/5/31 22:51 It's time to go to sleep......)睡觉

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics