zoukankan      html  css  js  c++  java
  • 线程池和Thread

    1、线程池
    创建线程需要时间。如果有不同的短任务要完成,就可以事先创建许多线程,在应完成这些任务时发出请求。这个线程数最好在需要更多线程时增加,在需要释放资源时减少。不需要自己创建这样一个列表。该列表有ThreadPool类托管。这个类会在需要时增减池中线程的线程数,直到最大的线程数。池中的最大线程数是可配置的。在四核CPU中,默认设置为1023个工作线程和1000个I/O线程。也可以指定在创建线程池时应立即启动的最小线程数,以及线程池中可用的最大线程数。如果有更多的作业要处理,线程池中线程的个数也到了极限,最新的作业就要排队,且必须等待线程完成其任务。
    下面的示例应用程序首先要读取工作线程和I/O线程的最大线程数,把这些信息写入控制台中,接着在for循环中,调用ThreadPool.QueueUserWorkItem()方法,传递一个WaitCallback类型的委托,来调用该方法。如果线程池还没有运行,就会创建一个线程池,并启动第一个线程。如果线程池已经在运行,且有一个空闲线程来完成该任务,就把该任务传递给这个线程。

           int nWorkerThreads;
            int nCompletionPortThreads;
            ThreadPool.GetMaxThreads(out nWorkerThreads,out nCompletionPortThreads);
            Console.WriteLine("Max worker threads:{0}  ,I/O completion threads:{1}",nWorkerThreads,nCompletionPortThreads);
    
            for (int i = 0; i < 5; i++)
            {
                ThreadPool.QueueUserWorkItem(JobForAThread);
            }
    
       static void JobForAThread(object state)
        {
            for (int i = 0; i < 3; i++)
            {
                Console.WriteLine("loop {0}, running inside pooled thread {1}",i,Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(50);
            }
        }
    


    运行应用程序时,可以看到1023个工作线程的当前设置。5个任务只由4个线程池中的线程处理(这是一个四核系统)。

    线程池的一些限制:
    a、线程池中的线程都是后台线程。如果进程的所有前台线程都结束了,所有的后台线程就会停止。不能把入池的线程改为前台线程。
    b、不能给入池的线程设置优先级或名称。
    c、对于COM对象,入池的所有线程都是多线程单元(MTA)线程许多COM对象都需要单线程单元(STA)线程。
    d、入池的线程只能用于时间较短的任务。如果线程要一直运行就应使用Thread类创建一个线程(或者在创建Task时使用LongRunning选项)。

    2、Thread类
    使用Thread类可以创建和控制线程。下面的结合Lambda表达式示例,结果不能保证哪一个先输出。

           var t1 = new Thread(() => Console.WriteLine("running in a thread,id:{0}",Thread.CurrentThread.ManagedThreadId));
            t1.Start();
            Console.WriteLine("This is the main ,id:{0}",Thread.CurrentThread.ManagedThreadId);
    


    2.1、给线程传递参数
    给线程传递一些数据可以采用两种方式。一种方式是使用带ParameterizedThreadStart委托参数的Thread构造函数,另一种是创建一个自定义类,把线程的方法定义为实例方法,这样就可以初始化实例的数据,之后启动线程。
    第一种:

           var d = new Data { message="this is a thread"};
            var t1 = new Thread(ThreadMainWithParameters);
            t1.Start();
        static void ThreadMainWithParameters(object o)
        {
            Data d = (Data)o;
            Console.WriteLine("Running in a thread,receiver {0}");
        }
        public class Data
        {
            public string message;
        }
    

    第二种:

        public class MyThread
        {
            private string data;
    
            public MyThread(string data)
            {
                this.data = data;
            }
            public void ThreadMain()
            {
                Console.WriteLine("Running in a thread,data:{0}",data);
            }
           }
        var obj = new MyThread("info");
            var t3 = new Thread(obj.ThreadMain);
            t3.Start();
    

    2.2、后台线程
    只要有一个前台线程在运行,应用程序的进程就在运行。如果多个前台线程在运行,而Main()方法结束了,应用程序的进程就仍然是激活的,知道所有前台线程完成其任务为止。在默认情况下,用Tread类创建的线程是前台线程。线程池中的线程总是后台线程。在用Thread类创建线程时,可以设置IsBackground属性,以确定该线程是前台线程还是后台线程。
    2.3、线程的优先级
    前面提到,线程由操作系统调度。给线程指定优先级,就可以影响调度顺序。在改变优先级之前,必须理解线程调度器。操作系统根据优先级来调度线程。调度优先级最高的线程以在CPU上运行。线程如果在等待资源,它就会停止运行,并释放CPU。
    线程必须等待时有几个原因,例如,响应睡眠指令、等待磁盘I/O的完成,等待网络包的到达等。如果线程不是主动释放CPU,线程调度器就会抢占该线程。线程有一个时间量,这意味着它可以持续使用CPU,知道这个时间到达(这是指没有更高优先级的线程时)。如果优先级相同的多个线程等待使用CPU,线程调度器就会使用一个循环调度规则,将CPU逐个交给线程使用。如果线程被其他线程抢占,它就会排在队列的最后。
    只有优先级相同的多个线程在运行,才用的上时间量和循环规则。优先级是动态的。如果线程是CPU密集型的(一直需要CPU,且不等待资源),其优先级就降低为该线程定义的基本优先级。如果线程在等待资源,它的优先级会提高。由于优先级的提高,线程很有可能在下次等待结束时获得CPU。
    在Thread类中,可以设置Priority属性,以影响线程的基本优先级。Priority属性需要ThreadPriority枚举定义的一个值。定义的级别有Highest、AboveNormal、Normal、BelowNormal和Lowest。(在给线程指定较高级的优先级的时候要小心,因为这可能降低其他线程的运行概率,根据需要、可以短暂地改变优先级)
    2.4、控制线程
    调用Thread对象的Start()方法,可以创建线程。但是,在调用Start()方法后,新线程仍不是出于Running状态,而是处于Unstarted状态。只要操作系统的线程调度器选择了要运行的线程,线程就会改变Running状态,读取Thread.ThreadState属性,就可以获得线程的当前状态。
    使用Thread.Sleep()方法,会使线程处于WaitSleepJoin状态,在等待Sleep()方法定义的时间段后,线程就会再次被唤起。
    要停止另一个线程,可以调用Thread.Abort()方法调用这个方法时,会在接到终止命令的线程中抛出一个ThreadAbortException类型的异常。用一个处理程序捕获这个异常,线程可以在结束前就完成一些清理工作。如果调用了Thread.ResetAbort,线程还有机会接收到ThreadAbortException异常后继续运行,如果线程,没有重置终止,接收到终止请求的线程的状态就从AbortRequest改为Aborted。
    如果需要等待线程的结束,就可以调用Thread.join()方法。Thread.join()方法会停止当前线程,并把它设置为WaitSleepJoin状态,直到加入的线程完成为止。
    3、线程问题
    使用多个线程编程并不容易。在启动访问相同数据的多个线程时,会间歇性地遇到难以发现的问题。如果使用任务,并行LINQ或Parallel类,也会遇到这些问题。为了避免这些问题,必须特别注意同步问题和多个线程可能发生的其他问题。
    3.1、争用条件
    如果两个或多个线程访问相同的对象,并且对共享状态的访问没有同步,就会出现争用条件。为了说明争用条件下面定义一个StateObject类,它包含一个int字段和一个ChangeState()方法。在ChangeState()方法的实现代码中,验证状态是否包含5,如果包含,就递增其值。下一条语句是Trace.Assert,它立刻验证State现在是否包含6.。

      public class StateObject
        {
            private int state = 5;
    
            public void ChangeState(int loop)
            {
                if(state==5)
                {
                    state++;
                    Trace.Assert(state==6,"Race condition occurred after"+loop+"loops");
                }
                state = 5;
            }
        }
    

    下面通过给任务顶一个方法来验证这一点

      public class SampleTask
        {
            public  void RaceCondition(object o)
            {
                Trace.Assert(o is StateObject,"o must be of type StateObject");
                StateObject state = o as StateObject;
    
                int i = 0;
                while(true)
                {
                    state.ChangeState(i++);
                }
            }
        }
    

    在Main()方法中新建StateObject对象,它由所有任务共享。通过使用传递给Task的Run方法的lambda表达式调用RaceCondition方法来创建Task对象。然后,主线程等待用户输入。但是,可能出现争用,所以程序很有可能在读取用户输入前就挂了。

          var state = new StateObject();
            for (int i = 0; i < 2; i++)
            {
                Task.Run(()=>new SampleTask().RaceCondition(state));
            }
    


    要避免该问题,可以锁定共享的对象。这可以在线程中完成,用下面的Lock语句锁定在线程中共享的state变量。只有一个线程能在锁定的块中处理共享的state对象。由于这个对象在所有的线程之间共享,因此如果一个线程锁定了state。另一个线程就必须等待该锁定的解除。一旦接受锁定,线程就拥有该锁定,直到该锁定块的末尾才能解除锁定,如果改变state变量引用的对象的每个线程都使用一个锁定,就不会出现争用条件。

     public class SampleTask
        {
            public  void RaceCondition(object o)
            {
                Trace.Assert(o is StateObject,"o must be of type StateObject");
                StateObject state = o as StateObject;
    
                int i = 0;
                while(true)
                {
                    lock(state)//no race condition with this lock
                    {
                        state.ChangeState(i++);
                    }
                   
                }
            }
        }
    

    在使用共享对象时,除了进行锁定之外,还可以将共享对象设置为线程安全的对象。在下面的代码中,ChangeState方法中包含一条lock语句,由于不能锁定state本身(只有引用类型才能用于锁定)

     public class StateObject
        {
            private int state = 5;
            private object _lock = new object();
            public void ChangeState(int loop)
            {
                lock(_lock)
                {
                    if (state == 5)
                    {
                        state++;
                        Trace.Assert(state == 6, "Race condition occurred after" + loop + "loops");
                    }
                    state = 5;
                }                
            }
        }
    

    3.2、死锁
    过多的锁定也会有麻烦。在死锁中,至少有两个线程被挂起,并等待对方解除锁定。由于两个线程都在等待对方,就出现了死锁,线程将无限等待下去。为了说明死锁,下面实例化StateObject类型的两个对象,并把它们传递给SampleTask类的构造函数,创建两个任务,其中一个任务运行Deadlock1()方法,另一个任务运行Deadlock2()方法。

           var state1 = new StateObject();
            var state2 = new StateObject();
            new Task(new Sample(state1,state2).DeadLock1).Start();
            new Task(new Sample(state1, state2).DeadLock2).Start();
      public class Sample
        {
            public Sample(StateObject s1, StateObject s2)
            {
                this.s1 = s1;
                this.s2 = s2;
            }
            private StateObject s1;
            private StateObject s2;
    
            public void DeadLock1()
            {
                int i = 0;
                while(true)
                {
                    lock(s1)
                    {
                        lock(s2)
                        {
                            s1.ChangeState(i);
                            s2.ChangeState(i++);
                            Console.WriteLine("still running , {0}",i);
                        }
                    }
                }
            }
    
            public void DeadLock2()
            {
                int i = 0;
                while (true)
                {
                    lock (s2)
                    {
                        lock (s1)
                        {
                            s1.ChangeState(i);
                            s2.ChangeState(i++);
                            Console.WriteLine("still running , {0}", i);
                        }
                    }
                }
            }
        }
    

    在本例中只需要改变锁定顺序,这两个线程就会以相同的顺序进行锁定。但是,锁定可能隐藏在方法的深处。为了避免这个问题,可以在应用程序的体系架构中,从一开始就设计好锁定顺序,也可以为锁定定义超时时间。

  • 相关阅读:
    【Linux系列汇总】小白博主的嵌入式Linux实战快速进阶之路(持续更新)
    【matlab系列汇总】小白博主的matlab学习实战快速进阶之路(持续更新)
    【FreeRTOS学习01】CubeIDE快速整合FreeRTOS创建第一个任务
    STM32F767ZI NUCLEO144 基于CubeIDE快速开发入门指南
    【matlab 基础篇 01】快速开始第一个程序(详细图文+文末资源)
    Linux 通过终端命令行切换系统语言
    ubuntu 1604升级到ubuntu 1804无法忽视的细节问题(亲测有效)
    假如用王者荣耀的方式学习webpack
    小程序第三方框架对比 ( wepy / mpvue / taro )
    我所理解的前端
  • 原文地址:https://www.cnblogs.com/caozhengze/p/10067352.html
Copyright © 2011-2022 走看看