进阶系列(11)—— C#多线程

自古美人都是妖i 提交于 2019-12-21 23:51:38

一、多线程的相关概念

1.进程:是操作系统结构的基础;是一个正在执行的程序;计算机中正在运行的程序实例;可以分配给处理器并由处理器执行的一个实体;由单一顺序的执行显示,一个当前状态和一组相关的系统资源所描述的活动单元。

2.线程:线程是程序中一个单一的顺序控制流程。是程序执行流的最小单元。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

3.多线程:在单个程序中同时运行多个线程完成不同的工作,称为多线程。

理解:其实更容易理解一点进程与线程的话,可以举这样一个例子:把进程理解成为一个运营着的公司,然而每一个公司员工就可以叫做一个线程。每个公司至少要有一个员工,员工越多,如果你的管理合理的话,公司的运营速度就会越好。这里官味一点话就是说。cpu大部分时间处于空闲时间,浪费了cpu资源,多线程可以让一个程序“同时”处理多个事情,提高效率。

(一)单线程问题演示

创建一个WinForm应用程序,这里出现的问题是,点击按钮后如果在弹出提示框之前,窗体是不能被拖动的。

 private void button1_Click(object sender, EventArgs e)
        {
            for (int i = 0; i < 10000000000; i++)  
            {
                i += 1;
            }
            MessageBox.Show("出现后能拖动,提示没出现之前窗体不能被拖动");
        }

原因:运行这个应用程序的时候,窗体应用程序自带一个叫做UI的线程,这个线程负责窗体界面的移动大小等。如果点击按钮则这个线程就去处理这个循环计算,而放弃了其它操作,故而窗体拖动无响应。这就是单线程带来的问题。

解决办法:使用多线程,我们自己创建线程。把计算代码放入我们自己写的线程中,UI线程就能继续做他的界面响应了。

(二)线程的创建

 线程的实现:线程一定是要执行一段代码的,所以要产生一个线程,必须先为该线程写一个方法,这个方法中的代码,就是该线程中要执行的代码,然而启动线程时,是通过委托调用该方法的。线程启动是,调用传过来的委托,委托就会执行相应的方法,从而实现线程执行方法。

 //创建线程  
        private void button1_Click(object sender, EventArgs e)
        {
            //ThreadStart是一个无参无返回值的委托。
            ThreadStart ts = new ThreadStart(js);
            //初始化Thread的新实例,并通过构造方法将委托ts做为参数赋初始值。
            Thread td = new Thread(ts);   //需要引入System.Threading命名空间
            //运行委托
            td.Start();
        }
        //创建的线程要执行的函数。
        void js()
        {
            for (int i = 0; i < 1000000000; i++)
            {
                i += 1;
            }
            MessageBox.Show("提示出现前后窗体都能被拖动");
        }

把这个计算写入自己写的线程中,就解决了单线程中的界面无反应缺陷。

小结:创建线程的4个步骤:

1.编写线程索要执行的方法。

2.引用System.Threading命名空。

3.实例化Thread类,并传入一个指向线程所要运行方法的委托。

4.调用Start()方法,将该线程标记为可以运行的状态,但具体执行时间由cpu决定。

 (三)方法重入(多个线程执行一个方法)

 由于线程可与同属一个进程的其它线程共享进程所拥有的全部资源。

所以多个线程同时执行一个方法的情况是存在的,然而这里不经过处理的话会出现一点问题,线程之间先后争抢资源,致使数据计算结果错乱。

public partial class 方法重入 : Form
    {
        public 方法重入()
        {
            InitializeComponent();

            //设置TextBox类的这个属性是因为,开启ui线程,
            //微软设置检测不允许其它线程对ui线程的数据进行访问,这里我们把检测关闭,也就允许了其它线程对ui线程数据的访问。
            //如果检测不设置为False,则报错。
            TextBox.CheckForIllegalCrossThreadCalls = false;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            textBox1.Text = "0";
            //开启第一个线程,对js方法进行计算
            ThreadStart ts = new ThreadStart(js);
            Thread td = new Thread(ts);
            td.Start();

            //开启第二个线程,对js方法进行计算
            ThreadStart ts1 = new ThreadStart(js);
            Thread td1 = new Thread(ts1);
            td1.Start();
        }
        //多线程要重入的方法。
        void js()
        {
            int a = Convert.ToInt32(textBox1.Text);
            for (int i = 0; i < 2000; i++)
            {
                a++;
                textBox1.Text = a.ToString();
            }
        }
    }

 

出错现象:点击按钮后TextBox1中数据为2000+或2000,如果你看到的数据一直是2000说明你的计算机cpu比较牛X,这样的话你想看到不是2000的话,你可以多点击几次试试,真不行的话,代码中给TextBox1赋值为0,换做在界面中给textBox1数值默认值为0试试看。

出错原因:两个进程同时计算这个方法,不相干扰应该每个线程计算的结果都是2000的,但是这里的结果输出却让人以外,原因是第一个两个线程同时计算,并不是同时开始计算,而是根据cpu决定的哪个先开始,哪个后开始,虽然相差时间不多,但后开始的就会取用先开始计算过的数据计算,这样就会导致计算错乱。

解决办法:解决这个的一个简单办法解释给方法加锁,加锁的意思就是第一个线程取用过这个资源完毕后,第二个线程再来取用此资源。形成排队效果。

下面给方法加锁。

//多线程要重入的方法,这里加锁。
        void js()
        {
            lock (this)
            {
                int a = Convert.ToInt32(textBox1.Text);
                for (int i = 0; i < 2000; i++)
                {
                    a++;
                    textBox1.Text = a.ToString();
                }
            }
        }

给方法加过锁后,线程一前一后取用资源,就能避免不可预计的错乱结果,第一个线程计算为2000,第二个线程计算就是从2000开始,这里的结果就为4000。

小结:多线程可以同时运行,提高了cpu的效率,这里的同时并不是同时开始,同时结束,他们的开始是由cpu决定的,时间相差不大,但会有不可预计的计算错乱,这里要注意类似上面例子导致的方法重入问题。

(四)前台线程后台线程

 .Net的公用语言运行时能区分两种不同类型的线程:前台线程和后台线程。这两者的区别就是:应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。

问题:关闭了窗口,消息框还能弹出。

 private void button1_Click(object sender, EventArgs e)
        { 
            //开启一个线程,对js方法进行计算
            ThreadStart ts2 = new ThreadStart(js);
            Thread td2 = new Thread(ts2);             
            td2.Start();

        }        
        void js()
        {
            for (int i = 0; i < 2000000000; i++)  //如果看不出效果这里的2后面多加0
            {
                i++;
            }
            MessageBox.Show("关闭了窗口我还是要出来的!");
        }

原因:.Net环境使用Thread建立线程,线程默认为前台线程。即线程属性IsBackground=false,而前台线程只要有一个在运行则应用程序不关闭,所以知道弹出消息框后应用程序才算关闭。

解决办法:在代码中设置td2.IsBackground=true;

 (五)线程执行带参数的方法

 //创建一个执行带参数方法的线程
        private void button1_Click(object sender, EventArgs e)
        {
            //ParameterizedThreadStart这是一个参数类型为object的委托
            ParameterizedThreadStart pts=new ParameterizedThreadStart(SayHello);
            Thread td2 = new Thread(pts);
            td2.Start("张三");  //参数值先入这里
        }
        void SayHello(object name)
        {
            MessageBox.Show("你好,"+name.ToString()+"!");
        } 

(六)线程执行带多参数的方法

 其实还是带一参数的方法,只不过是利用参数类型为object的好处,这里将类型传为list类型,貌似多参。

 //创建一个执行带多个参数的方法线程
        private void button1_Click(object sender, EventArgs e)
        {
            List<string> list = new List<string> { "张三", "李四", "王五" };
            //ParameterizedThreadStart这是一个参数类型为object的委托
            ParameterizedThreadStart pts=new ParameterizedThreadStart(SayHello);
            Thread td2 = new Thread(pts);
            td2.Start(list);  //参数值先入这里
        }
        void SayHello(object list)
        {
            List<string> lt = list as List<string>;
            for (int i = 0; i < lt.Count; i++)
            {
                MessageBox.Show("你好," + lt[i].ToString() + "!");
            }
        } 

 二、线程的重要属性

(一)确定多线程的结束时间,thread的IsAlive属性

在多个线程运行的背景下,了解线程什么时候结束,什么时候停止是很有必要的。

案例:老和尚念经计时,2本经书,2个和尚念,一人一本,不能撕破,最短时间念完,问老和尚们念完经书最短需要多长时间。

分析:首先在开始念经的时候给计时,记为A,最后在记下慢和尚念完经书时的时间,记为B。求B-A

代码:IsAlive属性:标识此线程已启动并且尚未正常终止或中止,则为 true,再念,没念完,努力中;否则为 false,念完啦,歇着。

 //和尚1,和尚2
        public Thread td1, td2;

        public void StarThread()
        {
            //开启一个线程执行Hello方法,即和尚1念菠萝菠萝蜜
            ThreadStart ts = new ThreadStart(Hello);
            td1 = new Thread(ts);
            td1.Start();
        }
        public void StarThread1()
        {
            //开启一个线程执行Welcome方法,即和尚2念大金刚经
            ThreadStart ts = new ThreadStart(Welcome);
            td2 = new Thread(ts);
            td2.Start();
        }
        public string sayh="", sayw="";

        //菠萝菠萝蜜
        public void Hello()
        {
            //念
            sayh = "Hellow everyone ! ";
        }

        //大金刚经
        public  void Welcome()
        {   
            //念
            sayw = "Welcome to ShangHai ! ";
            //偷懒10秒
            Thread.Sleep(10000);
        }

        protected void btn_StarThread_Click(object sender, EventArgs e)
        {
            //记时开始,预备念        
            Response.Write("开始念的时间: "+DateTime.Now.ToString() + "</br>");
            //和尚1就位
            StarThread();
            //和尚2就位
            StarThread1();

            int i = 0;
            while (i == 0)
            {
                //判断线程的IsAlive属性
                //IsAlive标识此线程已启动并且尚未正常终止或中止,则为 true;否则为 false。
                //如果两个都为false说明,线程结束终止
                if (!td1.IsAlive && !td2.IsAlive)
                {
                    i++;
                    if (i == 1)
                    {
                        //念得内容,绕梁三尺。
                        Response.Write("我们年的内容: "+(sayh + " + " + sayw) + "</br>");
                        Response.Write("念完时的时间: "+DateTime.Now.ToString());
                        Response.End();
                    }
                }
            }
        }

(二)、线程优先级,thread的ThreadPriority属性

线程优先级区别于线程占有cpu时间的多少,当然优先级越高同等条件下占有的cpu时间越多。级别高的执行效率要高于级别低的。

优先级有5个级别:Lowest<BelowNormal<Normal<AboveNormal<Highest;默认为Normal。

案例:老和尚娶媳妇。佛祖说:你们3个和尚,清修刻苦,现特许你们娶媳妇啦,不过娶媳妇的只能是你们三个中间的一人。条件是我手中的经书谁能先念完,谁可以娶。

分析:和尚平时都很刻苦,各有特点,平时和尚1在lowest环境下念经,和尚2在normal环境下念经,和尚3在Highest环境下念经。

 protected void btn_StarThread_Click(object sender, EventArgs e)
        {
            Write();
        }

        //i为和尚1念的页数
        //j为和尚2念的页数
        //k为和尚3念的页数
        //c为经书总页数
        int i=0,j=0,k=0,c=10000000;

        //和尚1念经
        public void Jsi()
        {
            while (i <= c)
            {
                i+=1;
            }
        }
        //和尚2念经
        public void Jsj()
        {
            while (j <= c)
            {
                j+=1;
            }
        }
        //和尚3念经
        public void Jsk()
        {
            while (k <= c)
            {
                k+=1;
            }
        }
        public void Write()
        {
            //开启线程计算i
            ThreadStart sti = new ThreadStart(Jsi);
            Thread tdi = new Thread(sti);
            //设置线程优先级为Lowest。和尚1在Lowest环境下念经
            tdi.Priority = ThreadPriority.Lowest;

            //开启线程计算j
            ThreadStart stj = new ThreadStart(Jsj);
            Thread tdj = new Thread(stj);
            //设置线程优先级为Normal。和尚2在Normal环境下念经
            tdj.Priority = ThreadPriority.Normal;            

            //开启线程计算k
            ThreadStart stk = new ThreadStart(Jsk);
            Thread tdk = new Thread(stk);
            //设置线程优先级为Highest。和尚3在Highest环境下念经
            tdk.Priority = ThreadPriority.Highest;         
            
            //开始
            tdj.Start();
            tdk.Start();
            tdi.Start();
            int s = 0;
            while (s==0)
            {                
                if (k > c)
                {
                    s++;
                    Response.Write("比赛结束,结果如下:</br></br>");
                    Response.Write("和尚1在Lowest环境下念经:" + i + "页</br>和尚2在Normal环境下念经:" + j + "页</br>和尚3在Highest环境下念经:" + k + "页</br></br>");
                    Response.Write("佛祖又说:你念或者不念,苍老师,就在那里!");
                    Response.End();
                }
            }
        }

复制代码

为啦方便期间,从这以后,我要用控制台程序演示,操控线程。

(三)线程通信之Monitor类

如果,你的线程A中运行锁内方法时候,需要去访问一个暂不可用资源B,可能在B上耗费很长的等待时间,那么这时候你的线程A,将占用锁内资源,阻塞其它线程访问锁定内容,造成性能损失。你该怎么解决这样子的问题呢?这样,让A暂时放弃锁,停留在锁中的,允许其它线程访问锁,而等B资源可用时,通知A让他继续锁内的操作。是不是解决啦问题,这样就用到啦这段中的Monitor类,提供的几个方法:Wait(),Pulse(),PulseAll(),这几个方法只能在当前锁定中使用。

Wait():暂时中断运行锁定中线程操作,释放锁,时刻等待着通知复活。

Pulse():通知等待该锁线程队列中的第一个线程,此锁可用。

PulseAll():通知所有锁,此锁可用。

案例:嵩山少林和尚开会。主持人和尚主持会议会不停的上舞台讲话,方丈会出来宣布大会开始,弟子们开始讨论峨眉山怎么走。

分析:主持人一个线程,方丈一个线程,弟子们一个线程,主持人贯彻全场。

 public class MutexSample
    {
        static void Main()
        {
            comm com = new comm();
            com.dhThreads();
            Console.ReadKey();
        }
    }
    public class comm
    {
        //状态值:0时主持人和尚说,1时方丈说,2时弟子们说,3结束。
        int sayFla;
        //主持人上台
        int i = 0;
        public void zcrSay()
        {
            lock (this)
            {
                string sayStr;
                if (i == 0)
                {
                    //让方丈说话
                    sayFla = 1;
                    sayStr = Thread.CurrentThread.Name+"今晚,阳光明媚,多云转晴,方丈大师,程祥云而来,传扬峨眉一隅,情况如何,还请方丈闪亮登场。";
                    Console.WriteLine(sayStr);
                    i++;
                    //此时sayFla=1通知等待的方丈线程运行
                    Monitor.Pulse(this);  
                    //暂时锁定主持人,暂停到这里,释放this让其它线程访问
                    Monitor.Wait(this);
                    
                }
                //被通知后,从上一个锁定开始运行到这里
                if (i == 1)
                {
                    //让弟子说话
                    sayFla = 2;
                    sayStr = Thread.CurrentThread.Name + "看方丈那幸福的表情,徜徉肆恣,愿走的跟他去吧。下面请弟子们各抒己见";
                    Console.WriteLine(sayStr);
                    i++;
                    //此时sayFla=12通知等待的弟子线程运行
                    Monitor.Pulse(this);  
                     //暂时锁定主持人,暂停到这里,释放this让其它线程访问
                    Monitor.Wait(this);                    
                }
                //被通知后,从上一个锁定开始运行到这里
                if (i == 2)
                {
                    sayFla = 3;
                    sayStr = Thread.CurrentThread.Name + "大会结束!方丈幸福!!苍老师你在哪里?!!放开那女孩 ...";
                    Console.WriteLine(sayStr);
                    i++;
                    Monitor.Wait(this); 
                }
            }
        }
        //方丈上台
        public void fzSay()
        {
            lock (this)
            {
                while (true)
                {
                    if (sayFla != 1)
                    {
                        Monitor.Wait(this);
                    }
                    if (sayFla == 1)
                    {
                        Console.WriteLine(Thread.CurrentThread.Name + "蓝蓝的天空,绿绿的湖水,我看见,咿呀呀呀,看见一老尼,咿呀呀,在水一方。愿意来的一起来,不愿来的苍老师给你们放寺里。。咿呀呀,我走啦。。。");
                        //交给主持人
                        sayFla = 0;
                        //通知主持人线程,this可用
                        Monitor.Pulse(this);
                    }
                }                
            }
        }
        //弟子上台
        public void dzSay()
        {
            lock (this)
            {
                while (true)
                {
                    if (sayFla != 2)
                    {
                        Monitor.Wait(this);  
                    }
                    if (sayFla == 2)
                    {
                        Console.WriteLine(Thread.CurrentThread.Name + "果真如此的话,还是方丈大师自己去吧!! 祝福啊  .... ");
                        //交给主持人
                        sayFla = 0;
                        Monitor.Pulse(this);
                    }
                }
                
            }
        } 
        public void dhThreads()
        {            
                Thread zcrTd = new Thread(new ThreadStart(zcrSay));
                Thread fzTd = new Thread(new ThreadStart(fzSay));
                Thread dzTd = new Thread(new ThreadStart(dzSay));
                zcrTd.Name = "主持人:";
                fzTd.Name = "方丈:";
                dzTd.Name = "弟子:";                
                zcrTd.Start();
                fzTd.Start();
                dzTd.Start();
        }
    }

(四)线程排队之Join

多线程,共享一个资源,先后操作资源。Join()方法,暂停当前线程,直到指定线程运行完毕,才唤醒当前线程。如果没有Join,多线程随机读取公用资源,没有先后次序。

案例:两个和尚念一本经书,老和尚年前半本书,小和尚念后半本书,小和尚调皮,非要先念,就给老和尚用迷魂药啦。。

分析:一本书6页,小和尚4-6,老和尚1-3,两个和尚,两个线程。

 public class 连接线程Join
    {
        //小和尚
        public static Thread litThread;
        //老和尚
        public static Thread oldThread;

        //老和尚念经
        static void oldRead()
        {
            //老和尚被小和尚下药
            litThread.Join();  //暂停oldThread线程,开始litThread,直到litThread线程结束,oldThread才继续运行,如果不适用Join将小和尚一句,老和尚一句,随即没有规则的。
            for (int i = 1; i <= 3; i++)
            {
                Console.WriteLine(i);
            }
        }
        //小和尚念经
        static void litRead()
        { 
            for (int i = 4; i <= 6; i++)
            {
                Console.WriteLine(i);
            }
        }
        static void Main(string[] args)
        {
            oldThread = new Thread(new ThreadStart(oldRead));
            litThread = new Thread(new ThreadStart(litRead));

            oldThread.Start();
            // FristThread.Join();   //暂停oldThread线程,开始litThread,直到litThread线程结束,oldThread才继续运行
            litThread.Start();
            Console.ReadKey();
        }
    }

(五)多线程互斥锁Mutex

 互斥锁是一个同步的互斥对象,适用于,一个共享资源,同时只能有一个线程能够使用。

共享资源加互斥锁,需要两部走:1.WaitOne(),他将处于等待状态知道可以获取资源上的互斥锁,获取到后,阻塞主线程运行,直到释放互斥锁结束。2.ReleaseMutex(),释放互斥锁,是其它线程可以获取该互斥锁。

案例:和尚写日记。最近寺庙香火不旺,为啦节约用水,方丈发话两个和尚用一个本子写日记。

分析:好比多个线程写日志,同时只能有一个线程写入日志文件。

 public class 多线程互斥锁Mutex
    {
        static void Main(string[] args)
        {
            IncThread ict = new IncThread("大和尚", 3);
            DecThread dct = new DecThread("小和尚", 3);
            Console.ReadKey();
        }
    }
    class SharedRes
    {
        public static int count = 0;
        //初始化互斥锁,没被获取
        public static Mutex mtx = new Mutex();
        ////初始化互斥锁,被主调线程获取
        //public static Mutex mtx = new Mutex(true);
    }

    class IncThread
    {
        int num;
        public Thread thrd;
        public IncThread(string name ,int n)
        {
            thrd = new Thread(new ThreadStart(this.run));
            thrd.Name = name;
            num = n;
            thrd.Start();
        }
        //写日记,过程
        void run()
        {
            Console.WriteLine(thrd.Name + " , 等待互斥锁 。");
            SharedRes.mtx.WaitOne();           
            Console.WriteLine(thrd.Name + " ,获得互斥锁 。");
            do
            {
                Thread.Sleep(500);
                SharedRes.count++;
                Console.WriteLine("今天我 " + thrd.Name + " 比较强,这样写吧 :" + SharedRes.count);
                num--;
            }while(num>0);
           Console.WriteLine(thrd.Name + " , 释放互斥锁 。");
           SharedRes.mtx.ReleaseMutex();
        }
    }
    class DecThread
    {
        int num;
        public Thread thrd;

        public DecThread(string name, int n)
        {
            thrd = new Thread(new ThreadStart(this.run));
            thrd.Name = name;
            num = n;
            thrd.Start();
        }
        //写日记,过程
        void run()
        {           
            Console.WriteLine(thrd.Name + ", 等待互斥锁 。");
            SharedRes.mtx.WaitOne();
            Console.WriteLine(thrd.Name + " ,获得互斥锁 。");
            do
            {
                Thread.Sleep(500);
                SharedRes.count--;
                Console.WriteLine("今天我 " + thrd.Name + " 比较衰,这样写吧 :" + SharedRes.count);
                num--;
            } while (num > 0);
            Console.WriteLine(thrd.Name + " , 释放互斥锁 。");
            SharedRes.mtx.ReleaseMutex();
        }
    }

(六)信号量semaphore

类似于互斥锁,只不过他可以指定多个线程来访问,共享资源。在初始化信号量的同时,指定多少个线程可以访问,假如允许2个线程访问,而却有3个线程等待访问,那么他将只允许2个访问,一旦已访问的2个线程中有一个访问完成释放信号量,那么没有访问的线程立马可以进入访问。

案例:和尚抓鸡,3个和尚抓鸡,只有两只鸡,那么鸡圈管理员只允许2个和尚先进,抓到说三句话放下,出来,让第三个和尚进去抓。

分析:三个线程,初始信号量允许2个线程访问。

public class 信号量semaphore
    {
       static void Main(string[] args)
       {
           MyThread td1 = new MyThread("降龙");
           MyThread td2 = new MyThread("伏虎");
           MyThread td3 = new MyThread("如来");           
           td1.td.Start();
           td2.td.Start();
           td3.td.Start();
           Console.ReadKey();
       }    
    }

  public class MyThread
   {
       //初始化,新号量,允许2个线程访问,最大2也是2个。
       public static Semaphore sem = new Semaphore(2,2);
       public Thread td;
       public MyThread(string name)
       {
           td = new Thread(new ThreadStart(this.Run));
           td.Name = name;
       }
       //过程很美好
       public void Run()
       {
           Console.WriteLine(td.Name+",等待一个信号量。");
           sem.WaitOne();
           Console.WriteLine(td.Name+",已经获得新号量。");
           Thread.Sleep(500);           //很有深意的三句话
           char[] cr = { 'a','o','e'};
           foreach (char v in cr)
           {
               Console.WriteLine(td.Name+",输出v: "+v);
               Thread.Sleep(300);
           }
           Console.WriteLine(td.Name+",释放新号量。");
           sem.Release();
       }
   } 

 

三 线程同步

(一)什么是线程安全:

     线程安全是指在当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。

   线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。

为什么要实现同步呢,下面的例子我们拿著名的单例模式来说吧。看代码

public class Singleton
    {
        private static Singleton instance; 
        private Singleton()   //私有函数,防止实例
        {

        } 
        public static Singleton GetInstance()
        {
            if (instance == null)
            {
                instance = new Singleton();
            }
            return instance;
        }
    }

       单例模式就是保证在整个应用程序的生命周期中,在任何时刻,被指定的类只有一个实例,并为客户程序提供一个获取该实例的全局访问点。但上面代码有一个明显的问题,那就是假如两个线程同时去获取这个对象实例,那。。。。。。。。

我们对代码进行修改:

public class Singleton
{
       private static Singleton instance;
       private static object obj=new object(); 
       private Singleton()        //私有化构造函数
       {

       } 
       public static Singleton GetInstance()
       {
               if(instance==null)
               {
                      lock(obj)      //通过Lock关键字实现同步
                      {
                             if(instance==null)
                             {
                                     instance=new Singleton();
                             }
                      }
               }
               return instance;
       }
}

经过修改后的代码。加了一个 lock(obj)代码块。这样就能够实现同步了,假如不是很明白的话,咱们看后面继续讲解~

(二)使用Lock关键字实现线程同步 

  首先创建两个线程,两个线程执行同一个方法,参考下面的代码:

static void Main(string[] args)
        {
            Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
            threadA.Name = "王文建";
            Thread threadB = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
            threadB.Name = "生旭鹏";
            threadA.Start();
            threadB.Start();
            Console.ReadKey();
        }
        public static void ThreadMethod(object parameter)
        { 
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i);
                Thread.Sleep(300);
            }
        }

执行结果:


通过上面的执行结果,可以很清楚的看到,两个线程是在同时执行ThreadMethod这个方法,这显然不符合我们线程同步的要求。我们对代码进行修改如下:

static void Main(string[] args)
        {
            Program pro = new Program();
            Thread threadA = new Thread(pro.ThreadMethod); //执行的必须是无返回值的方法 
            threadA.Name = "王文建";
            Thread threadB = new Thread(pro.ThreadMethod); //执行的必须是无返回值的方法 
            threadB.Name = "生旭鹏";
            threadA.Start();
            threadB.Start();
            Console.ReadKey();
        }
        public void ThreadMethod(object parameter)
        {
            lock (this)             //添加lock关键字
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i);
                    Thread.Sleep(300);
                }
            } 
        }

执行结果:

我们通过添加了 lock(this) {...}代码,查看执行结果实现了我们想要的线程同步需求。但是我们知道this表示当前类实例的本身,那么有这么一种情况,我们把需要访问的方法所在的类型进行两个实例A和B,线程A访问实例A的方法ThreadMethod,线程B访问实例B的方法ThreadMethod,这样的话还能够达到线程同步的需求吗。

static void Main(string[] args)
        {
            Program pro1 = new Program();                    
            Program pro2 = new Program();                   
            Thread threadA = new Thread(pro1.ThreadMethod); //执行的必须是无返回值的方法 
            threadA.Name = "王文建";
            Thread threadB = new Thread(pro2.ThreadMethod); //执行的必须是无返回值的方法 
            threadB.Name = "生旭鹏";
            threadA.Start();
            threadB.Start();
            Console.ReadKey();
        }
        public void ThreadMethod(object parameter)
        {
            lock (this)
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i);
                    Thread.Sleep(300);
                }
            }
        }

执行结果:

我们会发现,线程又没有实现同步了!lock(this)对于这种情况是不行的!所以需要我们对代码进行修改!修改后的代码如下:

private static object obj = new object();
        static void Main(string[] args)
        {
            Program pro1 = new Program();                    
            Program pro2 = new Program();                   
            Thread threadA = new Thread(pro1.ThreadMethod); //执行的必须是无返回值的方法 
            threadA.Name = "王文建";
            Thread threadB = new Thread(pro2.ThreadMethod); //执行的必须是无返回值的方法 
            threadB.Name = "生旭鹏";
            threadA.Start();
            threadB.Start();
            Console.ReadKey();
        }
        public void ThreadMethod(object parameter)
        {
            lock (obj)
            {
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i);
                    Thread.Sleep(300);
                }
            }
        }

通过查看执行结果。会发现代码实现了我们的需求。那么 lock(this) 和lock(Obj)有什么区别呢? 

lock(this) 锁定 当前实例对象,如果有多个类实例的话,lock锁定的只是当前类实例,对其它类实例无影响。所有不推荐使用。 
lock(typeof(Model))锁定的是model类的所有实例。 
lock(obj)锁定的对象是全局的私有化静态变量。外部无法对该变量进行访问。 
lock 确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。 
所以,lock的结果好不好,还是关键看锁的谁,如果外边能对这个谁进行修改,lock就失去了作用。所以一般情况下,使用私有的、静态的并且是只读的对象。

总结:

1、lock的是必须是引用类型的对象,string类型除外。

2、lock推荐的做法是使用静态的、只读的、私有的对象。

3、保证lock的对象在外部无法修改才有意义,如果lock的对象在外部改变了,对其他线程就会畅通无阻,失去了lock的意义。

     不能锁定字符串,锁定字符串尤其危险,因为字符串被公共语言运行库 (CLR)“暂留”。 这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。通常,最好避免锁定 public 类型或锁定不受应用程序控制的对象实例。例如,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。而且lock(this)只对当前对象有效,如果多个对象之间就达不到同步的效果。lock(typeof(Class))与锁定字符串一样,范围太广了。

(三)使用Monitor类实现线程同步      

      Lock关键字是Monitor的一种替换用法,lock在IL代码中会被翻译成Monitor. 

   lock(obj)
          {
            //代码段
          } 
            //就等同于 
    Monitor.Enter(obj); 
                //代码段
    Monitor.Exit(obj);  

           Monitor的常用属性和方法:

    Enter(Object) 在指定对象上获取排他锁。

    Exit(Object) 释放指定对象上的排他锁。

    Pulse 通知等待队列中的线程锁定对象状态的更改。

    PulseAll 通知所有的等待线程对象状态的更改。

    TryEnter(Object) 试图获取指定对象的排他锁。

    TryEnter(Object, Boolean) 尝试获取指定对象上的排他锁,并自动设置一个值,指示是否得到了该锁。

    Wait(Object) 释放对象上的锁并阻止当前线程,直到它重新获取该锁。

               常用的方法有两个,Monitor.Enter(object)方法是获取锁,Monitor.Exit(object)方法是释放锁,这就是Monitor最常用的两个方法,在使用过程中为了避免获取锁之后因为异常,致锁无法释放,所以需要在try{} catch(){}之后的finally{}结构体中释放锁(Monitor.Exit())。

Enter(Object)的用法很简单,看代码

     static void Main(string[] args)
        {                
            Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
            threadA.Name = "A";
            Thread threadB = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
            threadB.Name = "B";
            threadA.Start();
            threadB.Start();
            Thread.CurrentThread.Name = "C";
            ThreadMethod();
            Console.ReadKey();
        }
        static object obj = new object();
        public static void ThreadMethod()
        {
            Monitor.Enter(obj);      //Monitor.Enter(obj)  锁定对象
            try
            {
                for (int i = 0; i < 500; i++)
                {
                    Console.Write(Thread.CurrentThread.Name); 
                }
            }
            catch(Exception ex){   }
            finally
            { 
                Monitor.Exit(obj);  //释放对象
            } 
        } 
复制代码

     TryEnter(Object)TryEnter() 方法在尝试获取一个对象上的显式锁方面和 Enter() 方法类似。然而,它不像Enter()方法那样会阻塞执行。如果线程成功进入关键区域那么TryEnter()方法会返回true. 和试图获取指定对象的排他锁。看下面代码演示:

      我们可以通过Monitor.TryEnter(monster, 1000),该方法也能够避免死锁的发生,我们下面的例子用到的是该方法的重载,Monitor.TryEnter(Object,Int32),。

static void Main(string[] args)
        {                
            Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
            threadA.Name = "A";
            Thread threadB = new Thread(ThreadMethod); //执行的必须是无返回值的方法 
            threadB.Name = "B";
            threadA.Start();
            threadB.Start();
            Thread.CurrentThread.Name = "C";
            ThreadMethod();
            Console.ReadKey();
        }
        static object obj = new object();
        public static void ThreadMethod()
        {
            bool flag = Monitor.TryEnter(obj, 1000);   //设置1S的超时时间,如果在1S之内没有获得同步锁,则返回false        //上面的代码设置了锁定超时时间为1秒,也就是说,在1秒中后,       //lockObj还未被解锁,TryEntry方法就会返回false,如果在1秒之内,lockObj被解锁,TryEntry返回true。我们可以使用这种方法来避免死锁
            try
            {
                if (flag)
                {
                    for (int i = 0; i < 500; i++)
                    {
                        Console.Write(Thread.CurrentThread.Name); 
                    }
                }
            }
            catch(Exception ex)
            {

            }
            finally
            {
                if (flag)
                    Monitor.Exit(obj);
            } 
        } 

 Monitor.Wait和Monitor()Pause()

Pulse 向等待对象发出信号,当前拥有指定对象上的锁的线程调用此方法以便向队列中的下一个线程发出锁的信号。接收到脉冲后,等待线程就被移动到就绪队列中。在调用 Pulse 的线程释放锁后,就绪队列中的下一个线程(不一定是接收到脉冲的线程)将获得该锁。
另外

        Wait 和 Pulse 方法必须写在 Monitor.Enter 和Moniter.Exit 之间

上面是MSDN的解释。不明白看代码:

 首先我们定义一个攻击类,

/// <summary>
    /// 怪物类
    /// </summary>
    internal class Monster
    {
        public int Blood { get; set; }
        public Monster(int blood)
        {
            this.Blood = blood;
            Console.WriteLine("我是怪物,我有{0}滴血",blood);
        }
    }

然后在定义一个攻击类

/// <summary>
    /// 攻击类
    /// </summary>
    internal class Play
    {
        /// <summary>
        /// 攻击者名字
        /// </summary>
        public string Name { get; set; } 
        /// <summary>
        /// 攻击力
        /// </summary>
        public int Power{ get; set; }
        /// <summary>
        /// 法术攻击
        /// </summary>
        public void magicExecute(object monster)
        {
            Monster m = monster as Monster;
            Monitor.Enter(monster);
            while (m.Blood>0)
            {
                Monitor.Wait(monster);
                Console.WriteLine("当前英雄:{0},正在使用法术攻击打击怪物", this.Name);
                if(m.Blood>= Power)
                {
                    m.Blood -= Power;
                }
                else
                {
                    m.Blood = 0;
                }
                Thread.Sleep(300);
                Console.WriteLine("怪物的血量还剩下{0}", m.Blood);
                Monitor.PulseAll(monster);
            }
            Monitor.Exit(monster);
        }
        /// <summary>
        /// 物理攻击
        /// </summary>
        /// <param name="monster"></param>
        public void physicsExecute(object monster)
        {
            Monster m = monster as Monster;
            Monitor.Enter(monster);
            while (m.Blood > 0)
            {
                Monitor.PulseAll(monster);
                if (Monitor.Wait(monster, 1000))     //非常关键的一句代码
                {
                    Console.WriteLine("当前英雄:{0},正在使用物理攻击打击怪物", this.Name);
                    if (m.Blood >= Power)
                    {
                        m.Blood -= Power;
                    }
                    else
                    {
                        m.Blood = 0;
                    }
                    Thread.Sleep(300);
                    Console.WriteLine("怪物的血量还剩下{0}", m.Blood);
                }
            }
            Monitor.Exit(monster);
        }
    }

执行代码:

    static void Main(string[] args)
        {
            //怪物类
            Monster monster = new Monster(1000);
            //物理攻击类
            Play play1 = new Play() { Name = "无敌剑圣", Power = 100 };
            //魔法攻击类
            Play play2 = new Play() { Name = "流浪法师", Power = 120 };
            Thread thread_first = new Thread(play1.physicsExecute);    //物理攻击线程
            Thread thread_second = new Thread(play2.magicExecute);     //魔法攻击线程
            thread_first.Start(monster);
            thread_second.Start(monster);
            Console.ReadKey();
        }

输出结果:

总结:

  第一种情况:

  1. thread_first首先获得同步对象的锁,当执行到 Monitor.Wait(monster);时,thread_first线程释放自己对同步对象的锁,流放自己到等待队列,直到自己再次获得锁,否则一直阻塞。

  2. 而thread_second线程一开始就竞争同步锁所以处于就绪队列中,这时候thread_second直接从就绪队列出来获得了monster对象锁,开始执行到Monitor.PulseAll(monster)时,发送了个Pulse信号。

  3. 这时候thread_first接收到信号进入到就绪状态。然后thread_second继续往下执行到 Monitor.Wait(monster, 1000)时,这是一句非常关键的代码,thread_second将自己流放到等待队列并释放自身对同步锁的独占,该等待设置了1S的超时值,当B线程在1S之内没有再次获取到锁自动添加到就绪队列。

  4. 这时thread_first从Monitor.Wait(monster)的阻塞结束,返回true。开始执行、打印。执行下一行的Monitor.Pulse(monster),这时候thread_second假如1S的时间还没过,thread_second接收到信号,于是将自己添加到就绪队列。

  5. thread_first的同步代码块结束以后,thread_second再次获得执行权, Monitor.Wait(m_smplQueue, 1000)返回true,于是继续从该代码处往下执行、打印。当再次执行到Monitor.Wait(monster, 1000),又开始了步骤3。

  6. 依次循环。。。。

   第二种情况thread_second首先获得同步锁对象,首先执行到Monitor.PulseAll(monster),因为程序中没有需要等待信号进入就绪状态的线程,所以这一句代码没有意义,当执行到 Monitor.Wait(monster, 1000),自动将自己流放到等待队列并在这里阻塞,1S 时间过后thread_second自动添加到就绪队列,线程thread_first获得monster对象锁,执行到Monitor.Wait(monster);时发生阻塞释放同步对象锁,线程thread_second执行,执行Monitor.PulseAll(monster)时通知thread_first。于是又开始第一种情况...

Monitor.Wait是让当前进程睡眠在临界资源上并释放独占锁,它只是等待,并不退出,当等待结束,就要继续执行剩下的代码。

(四) 使用Mutex类实现线程同步

        Mutex的突出特点是可以跨应用程序域边界对资源进行独占访问,即可以用于同步不同进程中的线程,这种功能当然这是以牺牲更多的系统资源为代价的。

  主要常用的两个方法:

public virtual bool WaitOne()   阻止当前线程,直到当前 System.Threading.WaitHandle 收到信号获取互斥锁。

 public void ReleaseMutex()     释放 System.Threading.Mutex 一次。

  使用实例:

    static void Main(string[] args)
        {
            Thread[] thread = new Thread[3];
            for (int i = 0; i < 3; i++)
            {
                thread[i] = new Thread(ThreadMethod1);
                thread[i].Name = i.ToString();
            }
            for (int i = 0; i < 3; i++)
            {
                thread[i].Start();
            }
            Console.ReadKey(); 
        } 

        public static void ThreadMethod1(object val)
        {
            mutet.WaitOne();    //获取锁
            for (int i = 0; i < 500; i++)
            {
                Console.Write(Thread.CurrentThread.Name); 
            } 
            mutet.ReleaseMutex();  //释放锁
        }

 四  如何使用.NET中的线程池?

  (1).NET中的线程池是神马

  我们都知道,线程的创建和销毁需要很大的性能开销,在Windows NT内核的操作系统中,每个进程都会包含一个线程池。而在.NET中呢,也有自己的线程池,它是由CLR负责管理的。

  线程池相当于一个缓存的概念,在该池中已经存在了一些没有被销毁的线程,而当应用程序需要一个新的线程时,就可以从线程池中直接获取一个已经存在的线程。相对应的,当一个线程被使用完毕后并不会立刻被销毁,而是放入线程池中等待下一次使用

  .NET中的线程池由CLR管理,管理的策略是灵活可变的,因此线程池中的线程数量也是可变的,使用者只需向线程池提交需求即可,下图则直观地展示了CLR是如何处理线程池需求的:

PS:线程池中运行的线程均为后台线程(即线程的 IsBackground 属性被设为true),所谓的后台线程是指这些线程的运行不会阻碍应用程序的结束。相反的,应用程序的结束则必须等待所有前台线程结束后才能退出。

  (2)在.NET中使用线程池

  在.NET中通过 System.Threading.ThreadPool 类型来提供关于线程池的操作,ThreadPool 类型提供了几个静态方法,来允许使用者插入一个工作线程的需求。常用的有以下三个静态方法:

  ① static bool QueueUserWorkItem(WaitCallback callback)

  ② static bool QueueUserWorkItem(WaitCallback callback, Object state)

  ③ static bool UnsafeQueueUserWorkItem(WaitCallback callback, Object state)

  有了这几个方法,我们只需要将线程要处理的方法作为参数传入上述方法即可,随后的工作都由CLR的线程池管理程序来完成。其中,WaitCallback 是一个委托类型,该委托方法接受一个Object类型的参数,并且没有返回值。下面的代码展示了如何使用线程池来编写多线程的程序:

    class Program
    {
        static void Main(string[] args)
        {
            string taskInfo = "运行10秒";
            // 插入一个新的请求到线程池
            bool result = ThreadPool.QueueUserWorkItem(DoWork, taskInfo);
            // 分配线程有可能会失败
            if (!result)
            {
                Console.WriteLine("分配线程失败");
            }
            else
            {
                Console.WriteLine("按回车键结束程序");
            }

            Console.ReadKey();
        }

        private static void DoWork(object state)
        {
            // 模拟做了一些操作,耗时10s
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("工作者线程的任务是:{0}", state);
                Thread.Sleep(1000);
            }
        }
    }

  上述代码执行后,如果不输入任何字符,那么会得到如下图所示的执行结果:

PS:事实上,UnsafeQueueWorkItem方法实现了完全相同的功能,二者的差别在于UnsafeQueueWorkItem方法不会将调用线程的堆栈传递给辅助线程,这就意味着主线程的权限限制不会传递给辅助线程。UnsafeQueueWorkItem由于不进行这样的传递,因此会得到更高的运行效率,但是潜在地提升了辅助线程的权限,也就有可能会成为一个潜在的安全漏洞。

五 如何查看和设置线程池的上下限?

  线程池的线程数是有限制的,通常情况下,我们无需修改默认的配置。但在一些场合,我们可能需要了解线程池的上下限和剩余的线程数。线程池作为一个缓冲池,有着其上下限。在通常情况下,当线程池中的线程数小于线程池设置的下限时,线程池会设法创建新的线程,而当线程池中的线程数大于线程池设置的上限时,线程池将销毁多余的线程

PS:在.NET Framework 4.0中,每个CPU默认的工作者线程数量最大值为250个,最小值为2个。而IO线程的默认最大值为1000个,最小值为2个。

  在.NET中,通过 ThreadPool 类型提供的5个静态方法可以获取和设置线程池的上限和下限,同时它还额外地提供了一个方法来让程序员获知当前可用的线程数量,下面是这五个方法的签名:

  ① static void GetMaxThreads(out int workerThreads, out int completionPortThreads)

  ② static void GetMinThreads(out int workerThreads, out int completionPortThreads)

  ③ static bool SetMaxThreads(int workerThreads, int completionPortThreads)

  ④ static bool SetMinThreads(int workerThreads, int completionPortThreads)

  ⑤ static void GetAvailableThreads(out int workerThreads, out int completionPortThreads)

  下面的代码示例演示了如何查询线程池的上下限阈值和可用线程数量:

    class Program
    {
        static void Main(string[] args)
        {
            // 打印阈值和可用数量
            GetLimitation();
            GetAvailable();

            // 使用掉其中三个线程
            Console.WriteLine("此处申请使用3个线程...");
            ThreadPool.QueueUserWorkItem(Work);
            ThreadPool.QueueUserWorkItem(Work);
            ThreadPool.QueueUserWorkItem(Work);

            Thread.Sleep(1000);

            // 打印阈值和可用数量
            GetLimitation();
            GetAvailable();
            // 设置最小值
            Console.WriteLine("此处修改了线程池的最小线程数量");
            ThreadPool.SetMinThreads(10, 10);
            // 打印阈值
            GetLimitation();

            Console.ReadKey();
        }


        // 运行10s的方法
        private static void Work(object o)
        {
            Thread.Sleep(10 * 1000);
        }

        // 打印线程池的上下限阈值
        private static void GetLimitation()
        {
            int maxWork, minWork, maxIO, minIO;
            // 得到阈值上限
            ThreadPool.GetMaxThreads(out maxWork, out maxIO);
            // 得到阈值下限
            ThreadPool.GetMinThreads(out minWork, out minIO);
            // 打印阈值上限
            Console.WriteLine("线程池最多有{0}个工作者线程,{1}个IO线程", maxWork.ToString(), maxIO.ToString());
            // 打印阈值下限
            Console.WriteLine("线程池最少有{0}个工作者线程,{1}个IO线程", minWork.ToString(), minIO.ToString());
            Console.WriteLine("------------------------------------");
        }

        // 打印可用线程数量
        private static void GetAvailable()
        {
            int remainWork, remainIO;
            // 得到当前可用线程数量
            ThreadPool.GetAvailableThreads(out remainWork, out remainIO);
            // 打印可用线程数量
            Console.WriteLine("线程池中当前有{0}个工作者线程可用,{1}个IO线程可用", remainWork.ToString(), remainIO.ToString());
            Console.WriteLine("------------------------------------");
        }
    }

  该实例的执行结果如下图所示:

PS:上面代码示例在不同的计算机上运行可能会得到不同的结果,线程池中的可用数码不会再初始时达到最大值,事实上CLR会尝试以一定的时间间隔来逐一地创建新线程,但这个时间间隔非常短。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!