线程池和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);
                    }
                }
            }
        }
    }

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

原文地址:https://www.cnblogs.com/caozhengze/p/10067352.html