C#使用Monitor类、Lock、Mutex和Semaphore进行多线程同步

在多线程中,为了使数据保持一致性必须要对数据或是访问数据的函数加锁,在数据库中这是很常见的,但是在程序中由于大部分都是单线程的程序,所以没有加锁的必要,但是在多线程中,为了保持数据的同步,一定要加锁,好在Framework中已经为我们提供了三个加锁的机制,分别是Monitor类、Lock关键字和Mutex类。 

其中Lock关键词用法比较简单(是Monitor的糖衣包装),Monitor类和Lock的用法差不多。这两个都是锁定数据或是锁定被调用的函数。而Mutex则多用于锁定多线程间的同步调用。简单的说,Monitor和Lock多用于锁定被调用端,而Mutex则多用于锁定调用端。


例如下面程序:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace MonitorLockMutex
{
    class Program
    {
        #region variable
        Thread thread1 = null;
        Thread thread2 = null;
        Mutex mutex = null;
        #endregion
        static void Main(string[] args)
        {
            Program p = new Program();
            p.RunThread();
            Console.ReadLine();
        }
        public Program()
        {
            mutex = new Mutex();
            thread1 = new Thread(new ThreadStart(thread1Func));
            thread2 = new Thread(new ThreadStart(thread2Func));
        }
        public void RunThread()
        {
            thread1.Start();
            thread2.Start();
        }
        private void thread1Func()
        {
            for (int count = 0; count < 10; count++)
            {
                TestFunc("Thread1 have run " + count.ToString() + " times");
                Thread.Sleep(30);
            }
        }
        private void thread2Func()
        {
            for (int count = 0; count < 10; count++)
            {
                TestFunc("Thread2 have run " + count.ToString() + " times");
                Thread.Sleep(100);
            }
        }
        private void TestFunc(string str)
        {
            DateTime now = System.DateTime.Now;
            Console.WriteLine("{0} {1}s {2}ms", str, now.Second, now.Millisecond);
            Thread.Sleep(50);
        }
    }
}
运行结果如下(因为配置和环境不同,在不同的机器上有不同的结果)

Thread2 have run 0 times 1s 419ms
Thread1 have run 0 times 1s 419ms
Thread1 have run 1 times 1s 500ms
Thread2 have run 1 times 1s 570ms
Thread1 have run 2 times 1s 580ms
Thread1 have run 3 times 1s 660ms
Thread2 have run 2 times 1s 720ms
Thread1 have run 4 times 1s 740ms
Thread1 have run 5 times 1s 820ms
Thread2 have run 3 times 1s 870ms
Thread1 have run 6 times 1s 900ms
Thread1 have run 7 times 1s 980ms
Thread2 have run 4 times 2s 20ms
Thread1 have run 8 times 2s 60ms
Thread1 have run 9 times 2s 140ms
Thread2 have run 5 times 2s 170ms
Thread2 have run 6 times 2s 320ms
Thread2 have run 7 times 2s 470ms
Thread2 have run 8 times 2s 620ms
Thread2 have run 9 times 2s 770ms

可以看出如果不加锁的话,这两个线程基本上是按照各自的时间间隔+TestFunc的执行时间(50mm)对TestFunc函数进行读取。因为线程在开始时需要分配内存,所以第0次的调用不准确,从第1~9次的调用可以看出,thread1的执行间隔约是80mm,thread2的执行间隔约是150mm。

现在将TestFunc修改如下: 

        private void TestFunc(string str)
        {
            lock (this)
            {
                DateTime now = System.DateTime.Now;
                Console.WriteLine("{0} {1}s {2}ms", str, now.Second, now.Millisecond);
                Thread.Sleep(50);
            }
        } 
或者是用Monitor也是一样的,如下: 

        private void TestFunc(string str)
        {
            Monitor.Enter(this);
            DateTime now = System.DateTime.Now;
            Console.WriteLine("{0} {1}s {2}ms", str, now.Second, now.Millisecond);
            Thread.Sleep(50);
            Monitor.Exit(this);
        } 
其中Enter和Exit都是Monitor中的静态方法。 
运行Lock结果如下:

Thread1 have run 0 times 12s 204ms
Thread2 have run 0 times 12s 254ms
Thread1 have run 1 times 12s 304ms
Thread1 have run 2 times 12s 384ms
Thread2 have run 1 times 12s 434ms
Thread1 have run 3 times 12s 484ms
Thread1 have run 4 times 12s 564ms
Thread2 have run 2 times 12s 614ms
Thread1 have run 5 times 12s 664ms
Thread1 have run 6 times 12s 744ms
Thread2 have run 3 times 12s 794ms
Thread1 have run 7 times 12s 844ms
Thread1 have run 8 times 12s 924ms
Thread2 have run 4 times 12s 974ms
Thread1 have run 9 times 13s 24ms
Thread2 have run 5 times 13s 124ms
Thread2 have run 6 times 13s 274ms
Thread2 have run 7 times 13s 424ms
Thread2 have run 8 times 13s 574ms
Thread2 have run 9 times 13s 724ms

让我们分析一下结果,同样从第1次开始。相同线程间的调用时间间隔为线程执行时间+TestFunc调用时间,不同线程间的调用时间间隔为TestFunc调用时间。例如:连续两次调用thread1之间的时间间隔约为30+50=80;连续两次调用thread2之间的时间间隔约为100+50=150mm。调用thread1和thread2之间的时间间隔为50mm。因为TestFunc被lock住了,所以一个thread调用TestFunc后,当其它的线程也同时调用TestFunc时,后来的线程即进被排到等待队列中等待,直到拥有访问权的线程释放这个资源为止。 
这就是锁定被调用函数的特性,即只能保证每次被一个线程调用。

下面让我们看看Mutex类的使用方法,以及与Monitor和Lock的区别。 
将代码修改如下:

        private void thread1Func()
        {
            for (int count = 0; count < 10; count++)
            {
                mutex.WaitOne();
                TestFunc("Thread1 have run " + count.ToString() + " times");
                mutex.ReleaseMutex();
            }
        }
        private void thread2Func()
        {
            for (int count = 0; count < 10; count++)
            {
                mutex.WaitOne();
                TestFunc("Thread2 have run " + count.ToString() + " times");
                mutex.ReleaseMutex();
            }
        }
        private void TestFunc(string str)
        {
            DateTime now = System.DateTime.Now;
            Console.WriteLine("{0} {1}s {2}ms", str, now.Second, now.Millisecond);
            Thread.Sleep(50);
        }

Thread1 have run 0 times 48s 871ms
Thread1 have run 1 times 48s 922ms
Thread1 have run 2 times 48s 972ms
Thread1 have run 3 times 49s 22ms
Thread1 have run 4 times 49s 72ms
Thread1 have run 5 times 49s 122ms
Thread1 have run 6 times 49s 172ms
Thread1 have run 7 times 49s 222ms
Thread2 have run 0 times 49s 272ms
Thread2 have run 1 times 49s 322ms
Thread2 have run 2 times 49s 372ms
Thread2 have run 3 times 49s 422ms
Thread2 have run 4 times 49s 472ms
Thread1 have run 8 times 49s 522ms
Thread1 have run 9 times 49s 572ms
Thread2 have run 5 times 49s 622ms
Thread2 have run 6 times 49s 672ms
Thread2 have run 7 times 49s 722ms
Thread2 have run 8 times 49s 772ms
Thread2 have run 9 times 49s 822ms

可以看出,Mutex只能互斥线程间的调用,但是不能互斥本线程的重复调用,即thread1中waitOne()只对thread2中的waitOne()起到互斥的作用,但是thread1并不受本wainOne()的影响,可以调用多次,只是在调用结束后调用相同次数的ReleaseMutex()就可以了。

(From MSDN: The thread that owns a mutex can request the same mutex in repeated calls toWaitOne without blocking its execution. However, the thread must call the ReleaseMutex method the same number of times to release ownership of the mutex.)

那么如何使线程按照调用顺序来依次执行呢?共享同一把锁lock(this)在两个线程里边锁定被调用资源可以吗?改代码如下: 

        private void thread1Func()
        {
            for (int count = 0; count < 10; count++)
            {                
                lock (this)
                {                   
                    TestFunc("Thread1 have run " + count.ToString() + " times");
                    Thread.Sleep(30);
                }
            }
        }
        private void thread2Func()
        {
            for (int count = 0; count < 10; count++)
            {
                lock (this)
                {                    
                    TestFunc("Thread2 have run " + count.ToString() + " times");
                    Thread.Sleep(100);
                }
            }
        }

        private void TestFunc(string str)
        {
            DateTime now = System.DateTime.Now;
            Console.WriteLine("{0} {1}s {2}ms", str, now.Second, now.Millisecond);
            Thread.Sleep(50);
        }

I wrote up a test to see if this was true, and it seems to indicate yes.

Thread1 have run 0 times 27s 15ms
Thread2 have run 0 times 27s 95ms
Thread1 have run 1 times 27s 245ms
Thread2 have run 1 times 27s 325ms
Thread1 have run 2 times 27s 475ms
Thread2 have run 2 times 27s 555ms
Thread1 have run 3 times 27s 705ms
Thread2 have run 3 times 27s 785ms
Thread1 have run 4 times 27s 935ms
Thread2 have run 4 times 28s 15ms
Thread1 have run 5 times 28s 165ms
Thread2 have run 5 times 28s 245ms
Thread1 have run 6 times 28s 395ms
Thread2 have run 6 times 28s 475ms
Thread1 have run 7 times 28s 625ms
Thread2 have run 7 times 28s 705ms
Thread1 have run 8 times 28s 855ms
Thread2 have run 8 times 28s 935ms
Thread1 have run 9 times 29s 85ms
Thread2 have run 9 times 29s 165ms

貌似正确,但是我们绝对不能依赖于这样来实现顺序调用。这不是一个关键区域从逻辑上应该能解决的多线程问题。

About the whole FIFO argument, some background info is necessary: the CLR does not provide synchronization. It is the CLR  host that provides this as a  service to the CLR runtime...

Note:     http://stackoverflow.com/questions/1330879/are-threads-waiting-on-a-lock-fifo

要解决按照调用顺序来依次执行的问题,老老实实通过互发信号量来实现吧!

    class Program
    {
        #region variable
        Thread thread1 = null;
        Thread thread2 = null;       
        Semaphore signal1 = null;
        Semaphore signal2 = null;
        #endregion
        static void Main(string[] args)
        {
            Program p = new Program();
            p.RunThread();            
            Console.ReadLine();
        }
        public Program()
        {
            signal1 = new Semaphore(0, 1);
            signal2 = new Semaphore(0, 1);           
            thread1 = new Thread(new ThreadStart(thread1Func));
            thread2 = new Thread(new ThreadStart(thread2Func));
        }
        public void RunThread()
        {
            thread1.Start();
            thread2.Start();
            
            Console.WriteLine("Main thread calls Release() for signal2. So Thread2 starts firtly.");
            signal2.Release();
        }

        private void thread1Func()
        {
            for (int count = 0; count < 10; count++)
            {
                signal1.WaitOne();   

                //控制区域-start
                TestFunc("Thread1 have run " + count.ToString() + " times");
                Thread.Sleep(30);
                //控制区域-end

                signal2.Release();                
            }
        }
        private void thread2Func()
        {
            for (int count = 0; count < 10; count++)
            {
                signal2.WaitOne();

                //控制区域-start
                TestFunc("Thread2 have run " + count.ToString() + " times");
                Thread.Sleep(100);
                //控制区域-end

                signal1.Release();
            }
        }
        private void TestFunc(string str)
        {
            DateTime now = System.DateTime.Now;
            Console.WriteLine("{0} {1}s {2}ms", str, now.Second, now.Millisecond);
            Thread.Sleep(50);
        }
    }
输出如下:

Main thread calls Release() for signal2. So Thread2 starts firtly.
Thread2 have run 0 times 4s 725ms
Thread1 have run 0 times 4s 876ms
Thread2 have run 1 times 4s 956ms
Thread1 have run 1 times 5s 106ms
Thread2 have run 2 times 5s 186ms
Thread1 have run 2 times 5s 336ms
Thread2 have run 3 times 5s 416ms
Thread1 have run 3 times 5s 566ms
Thread2 have run 4 times 5s 646ms
Thread1 have run 4 times 5s 796ms
Thread2 have run 5 times 5s 876ms
Thread1 have run 5 times 6s 26ms
Thread2 have run 6 times 6s 106ms
Thread1 have run 6 times 6s 256ms
Thread2 have run 7 times 6s 336ms
Thread1 have run 7 times 6s 486ms
Thread2 have run 8 times 6s 566ms
Thread1 have run 8 times 6s 716ms
Thread2 have run 9 times 6s 796ms
Thread1 have run 9 times 6s 946ms

Note: There is no guaranteed order, such as FIFO or LIFO, in which blocked threads enter the same semaphore. So use two semaphore here. A thread can enter the semaphore multiple times, by calling theWaitOne method repeatedly, but if and only if there are enough number of entries, or else, it will be blocked.(注意与上面Mutex的不同!

你可能感兴趣的:(多线程,Monitor,Semaphore,Lock,mutex)