一般在写多线程的程序的时候,都会碰到线程同步的问题。什么是线程同步?就是让多个干活的线程在访问到同一个变量(或者一堆变量)时有个先后次序。如果这几个线程没有同步,会发生什么情况呢?我们看看下面这个程序:

public class Program1

    {

        public int key;

        public string value;


        public Program1()

        {

            key = 5;

            value = "伍";

        }


        public void SetOdd()

        {

            while (true)

            {

                key = 5;

                Thread.Sleep(2000);  // 挂起线程,让其它线程执行,增加key和value不一致的机会

                value = "伍";

            }

        }


        public void SetEven()

        {

            while (true)

            {

                key = 4;

                Thread.Sleep(1000);  // 挂起线程,让其它线程执行,增加key和value不一致的机会

                value = "肆";

            }

        }


        public void start()

        {

            // 线程t1

            Thread t1 = new Thread(this.SetOdd);

            // 线程t2

            Thread t2 = new Thread(this.SetEven);

            t1.Start();

            t2.Start();


            int i = 1;


            while (true)

            {

                Thread.Sleep(50);

                if ((key == 5 && value.Equals("肆"))

                    || (key == 4 && value.Equals("伍")))

                {

                    Console.WriteLine(i + "\t" + key + ":" + value);

                    i++;

                }

            }

        }

    }


上面的程序可以通过下面两句代码执行:

Program1 x = new Program1();

    x.start();

这个程序在实际执行过程中,启动了三个线程。线程t1将key的值设为5,并把value的值设为“伍”;线程t2将key的值设为4,并把value的值设为“肆”。主线程则每隔一段时间(50毫秒左右,线程在sleep后不一定能马上占用cpu)查看key和value是否一致,如果key为5而value是“肆”或者key为4而value是“伍”,则把key和value打印在控制台上。上面的程序执行结果如下:

 

我们来看看为什么会出现这种情况,key和value的初始值非别是5和“伍”。在t1.Start()这句话执行后,线程t1被加到线程等待队列中,随后在t2.Start()执行后,线程t2被加到等待队列中,主线程继续往下走,不断轮询key和value的状态,如果发现他们两个不一致,就把他们在控制台打印出来。因为线程t1排在t2的前面,所以当cpu空闲的时候,t1先执行,但是t1启动后,在把key的值设为5后,就被挂起来了。这时系统发现cpu又空闲着,就告诉t2,说:“cpu空着,你可以去用了”,于是线程t2开始执行,但是在线程t2把key的值设为4后,它也被挂起来了。这个时候主线程检查key和value的状态,发现key为4,但是value却是“伍”。于是就把key和value在控制台上打印了出来。


实际上我们要求key和value必须保持一致,也就是说key和value的赋值操作必须连续被执行,中间不能被截断。怎么才能做到呢?这个就要用到.Net平台提供的线程同步机制。.Net平台的System.Threading 命名空间提供了用于同步线程的类,这些类包括 Mutex、Moniter、Interlocked、AutoResetEvent 和 ManualResetEvent,这些类怎么用不是本文的重点,这些知识大家可以阅读MSDN来获取。这里我们用lock关键字(其实是对moniter的封装)来实现同步机制。


public class Program2

    {

        public int key;

        public string value;


        public Program2()

        {

            key = 5;

            value = "伍";

        }


        public void SetOdd()

        {

            while (true)

            {

                lock (this)

                {

                    key = 5;

                    Thread.Sleep(2000);  // 挂起线程,让其它线程执行,增加key和value不一致的机会

                    value = "伍";

                }

            }

        }


        public void SetEven()

        {

            while (true)

            {

                lock (this)

                {

                    key = 4;

                    Thread.Sleep(1000);  // 挂起线程,让其它线程执行,增加key和value不一致的机会

                    value = "肆";

                }

            }

        }


        public void start()

        {

            // 线程t1

            Thread t1 = new Thread(this.SetOdd);

            // 线程t2

            Thread t2 = new Thread(this.SetEven);

            t1.Start();

            t2.Start();


            int i = 1;


            while (true)

            {

                Thread.Sleep(50);

                lock (this)

                {

                    if ((key == 5 && value.Equals("肆"))

                        || (key == 4 && value.Equals("伍")))

                    {

                        Console.WriteLine(i + "\t" + key + ":" + value);

                        i++;

                    }

                }

            }

        }

    }


看过上面的程序后,大家可以发现程序中所有读写key和value的部分(已经绿色字体标识出来)都被下面这样一段代码包着:

lock(this)

{

/* 这里是实际运算部分 */

}

这段是什么意思呢?打个比方,假如有一间屋子,一次只能进去一个人,屋前有一扇门,门上有一把锁,每个人进去后就把们锁起来,这样其他人就进不去了,只有等这个人出来后,其它人才能进去。上面的代码段就是一间屋子,this对象就是一把锁,每个线程就是一个人。当一个线程执行到“lock(this)”的时候,会检查屋子有没有被锁起来,如果没有锁上,就进去,否则在门口等待。


在Program2中,所有的读写key和value的部分都被锁了起来,只要有一个线程在读写key和value——进了屋子,那么其它线程必须等待,这样就保证了key和value的一致性。


Program2的三个线程不会打架了,但是Program2却看上去不太美观:

Ø Key和value暴露在外面,直接供线程访问,如果key和value有很多,那么肯定让人看花眼,因为它们太散了

Ø 线程t1和线程t2的锁都是Program2对象本身,如果有很多线程,那么它们肯定会在“屋子”外面排起长队。比如有key1和value1、key2和value2,我们只需要保证key1和value1一致、key2和value2一致,但是如果访问它们的线程都是用的同一把锁,那么在key1和value1被修改的时候,访问key2和value2的线程不得不等待


为了解决第一个问题,我们需要拿个包裹把key和value包起来,线程如果要访问key和value,那么从包裹里面拿就可以了。第二个问题,也好解决,一把锁不行,用两把锁。两个问题的解决方案能不能结合起来呢?当然可以,我们在一个包裹里面放一把锁,如果线程正在访问包裹里面的东西,包裹自动给加上锁,直到线程离开。下面我们看看这个包裹是什么样子的:


using System;

using System.Collections.Generic;

using System.Text;


namespace ConsoleApplication2

{

    class Package

    {

        public delegate void SynchronizedDelegate();        // 不带参数的同步代理

        public delegate void ParamsSynchronizedDelegate(params object[] objs);  // 带参数的同步代理


        // 公共区域

        private Dictionary<string, object> package = new Dictionary<string, object>();


        // 非线程安全

        public object this[string key]

        {

            get { return package[key]; }

            set

            {

                package[key] = value;

            }

        }


        // 设置公共区域变量,线程安全操作

        public void SynSetAttribute(string key, string obj)

        {

            lock(this)

            {

                package[key] = obj;

            }

        }


        // 取出公共区域变量,线程安全操作

        public object SynGetAttribute(string key)

        {

            lock (this)

            {

                return package[key];

            }

        }


        // 如果用户想让一系列连续的操作保持原子性

        // 那么最好定义一个代理,然后通过此方法执行

        public void SynchronizedExecute(SynchronizedDelegate method)

        {

            lock (this)

            {

                method();

            }

        }


        public void SynchronizedExecute(ParamsSynchronizedDelegate method)

        {

            lock (this)

            {

                method();

            }

        }

    }

}

这个package是个哈希表,专门用来存放东西。为了给让外界能够访问这个哈希表里面的东西,我们提供了访问单个值的接口,也提供了执行代码段的接口。有了个包裹,我们就可以着手改造我们的Program2了。


public class Program3

    {

        Package package = new Package();


        public Program3()

        {

            // 将线程要访问的变量都塞到包裹里

            package["key"] = 5;

            package["value"] = "伍";

        }


        public void SetOdd()

        {

            while (true)

            {

                // 用包裹来执行我们代码段,这样我们就不必关心加锁的问题

                package.SynchronizedExecute(delegate()

                {

                    /* 包裹里执行的代码不能是线程安全的

                     * 比如,这里的代码不能是

                     * package.SynSetAttribute("key", 5)

                     * package.SynSetAttribute("value", "伍")

                     * 否则会形成死锁

                     */

                    package["key"] = 5;

                    package["value"] = "伍";

                });

            }

        }


        public void SetEven()

        {

            while (true)

            {

                package.SynchronizedExecute(delegate()

                {

                    package["key"] = 4;

                    package["value"] = "肆";

                });

            }

        }


        public void start()

        {

            // 线程t1

            Thread t1 = new Thread(this.SetOdd);

            // 线程t2

            Thread t2 = new Thread(this.SetEven);

            t1.Start();

            t2.Start();


            int i = 1;


            while (true)

            {

                package.SynchronizedExecute(delegate()

                {

                    int key = (int)package["key"];

                    string value = (string)package["value"];


                    if ((key == 5 && value.Equals("肆"))

                        || (key == 4 && value.Equals("伍")))

                    {

                        Console.WriteLine(i + "\t" + key + ":" + value);

                        i++;

                    }

                });

            }

        }

    }


到这里,我们可以去喝杯咖啡,然后舒服在椅子上靠会了。

文章出处:http://www.diybl.com/course/4_webprogram/asp.net/asp_netshl/2007125/90632.html

posted on 2009-06-15 14:09  yxbsmx  阅读(238)  评论(0编辑  收藏  举报