菜鸟之旅——学习线程(线程和线程池)

愉悦的绅士

菜鸟之旅——学习线程(线程和线程池)

  上一篇主要介绍了进程和线程的一些基本知识,现在回归正题,我们来学一下线程的使用,本篇主要是使用新建线程和线程池的方式。

线程  

  先来介绍简单的线程使用:使用new方法来创建线程,至于撤销线程,我们不必去管(我也不知道怎么去管XD),因为CLR已经替我们去管理了。 

 创建

  先来看一个简单的使用线程的例子:

        static void Main(string[] args)
        {
            Thread t1 = new Thread(Menthod1);
            Thread t2 = new Thread(Menthod2);
            t1.Start();
            t2.Start("线程2参数");

            Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("--------------------");
            Console.ReadLine();
        }

        static void Menthod1()
        {
            Thread.Sleep(2000);
            Console.WriteLine("线程1的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("--------------------");
        }

        static void Menthod2(object obj)
        {
            Thread.Sleep(1000);
            Console.WriteLine("线程2的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("obj:{0}", obj);
            Console.WriteLine("--------------------");
        }

  我们可以用过new的方式创建一个线程,然后使用Start()的方法来运行该线程,线程则会在其生命周期去执行Method1方法,执行方法肯定需要时间的,但是Method1的方法过于简单,我们使用Thread.Sleep的方法来进行停顿,这个方法可以暂时将当前的线程睡眠一段时间(毫秒为单位),因为主线程只是创建并运行t1子线程,运行任务的不是主线程,所以主线程可以继续往后执行程序。

  我们还可以向线程执行的方法传入一个参数,例如线程2,在t2执行Start方法时,传入想要传入的参数,然后就可以在运行的时候使用了;不过参数是有限制的,在子线程的方法只能接受object的类型的参数,则在使用的时候需要显式转换类型,还有就是只能接受一个参数,多个参数也不会支持。

 线程与Lambda表达式

  线程的new也支持Lambda表达式,若是执行方法比较简单,或者在某些场景下,我们可以将线程执行的代码使用Lambda内置到新建里面:

        static void Main(string[] args)
        {
            Thread t1 = new Thread(() =>
            {
                Thread.Sleep(2000);
                Console.WriteLine("线程1的ID:{0}", Thread.CurrentThread.ManagedThreadId);
                Console.WriteLine("--------------------");
            });
            t1.Start();

            Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("--------------------");
            Console.ReadLine();
        }

  这里这样子写还有一个好处,就是这里可以直接使用主方法里面的变量,当然,这也会产生线程安全的问题。

 线程同步

  一个进程中的多个线程都是可以访问其进程的其他资源,多线程若不加以控制也是并发执行的,若在多线程的执行方法中包含操作全局变量、者静态变量或是使用I/O设备的时候,很容易的就会产生线程安全的问题,从而导致不可预估的错误。这里就需要进行线程同步了,下面介绍一些线程同步的方式。

  Join:

  我们有时候开启了n各子线程来进行辅助计算,但是又想主线程等待所有子线程计算完毕在接着执行,或者线程之间的关系更复杂,其中涉及了线程的阻塞与激活,那么就可以使用Join()的方法来阻塞主线程,实现一种最简单的线程同步:

        static void Main(string[] args)
        {
            Thread t1 = new Thread(Menthod1);
            Thread t2 = new Thread(Menthod2);
            t1.Start();
            t1.Join();

            t2.Start("线程2参数");
            t2.Join();

            Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("--------------------");
            Console.ReadLine();
        }

        static void Menthod1()
        {
            Thread.Sleep(2000);
            Console.WriteLine("线程1的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("--------------------");
        }

        static void Menthod2(object obj)
        {
            Thread.Sleep(4000);
            Console.WriteLine("线程2的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("obj:{0}", obj);
            Console.WriteLine("--------------------");
        }

  上面的调用阻塞的过程:先是t1开始,阻塞2秒,再接着t2执行,阻塞4秒,共计阻塞6秒,貌似没有发挥出来多线程的优势,但是也有可能在t2运行之前必须运行完t1,所以,Join()的调用需要视情况而定,Join()就是阻塞当前线程到当前位置,直到阻塞线程结束后,当前线程继续运行。

  同步事件:

  除了Join()来实现线程间的阻塞与激活,还有同步事件来进行处理;同步事件有两种:AutoResetEvent和 ManualResetEvent。它们之间唯一不同的地方就是在激活线程之后,状态是否自动由终止变为非终止。AutoResetEvent自动变为非终止,就是说一个AutoResetEvent只能激活一个线程。而ManualResetEvent要等到它的Reset方法被调用,状态才变为非终止,在这之前,ManualResetEvent可以激活任意多个线程:先来看ManualResetEvent的使用:

        static ManualResetEvent muilReset = new ManualResetEvent(false);
        static void Main(string[] args)
        {

            Thread t1 = new Thread(Menthod1);
            t1.Start();
            Thread t2 = new Thread(Menthod2);
            t2.Start("params");
            Thread t3 = new Thread(Menthod3);
            t3.Start();
            muilReset.WaitOne();

            Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("--------------------");
            Console.ReadLine();
        }

        static void Menthod1()
        {
            muilReset.WaitOne();
            Console.WriteLine("线程1的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("--------------------");
        }

        static void Menthod2(object obj)
        {
            muilReset.WaitOne();
            Console.WriteLine("线程2的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("obj:{0}", obj);
            Console.WriteLine("--------------------");
        }

        static void Menthod3()
        {
            Thread.Sleep(3000);
            Console.WriteLine("线程3的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("激活线程...");
            Console.WriteLine("--------------------");
            muilReset.Set();
        }

  上面例子我们将主线程、线程1、线程2阻塞,使用线程3在3秒钟之后激活全部线程,显示成功

  线程3的ID:12
  激活线程...
  --------------------
  线程2的ID:11
  obj:params
  --------------------
  主线程的ID:9
  --------------------
  线程1的ID:10
  --------------------

  若是使用AutoResetEvent则只能激活主线程

  线程3的ID:12
  激活线程...
  --------------------
  主线程的ID:9
  --------------------

  注:ManualResetEvent会给所有引用的线程都发送一个信号(多个线程可以共用一个ManualResetEvent,当ManualResetEvent调用Set()时,所有线程将被唤醒),而AutoResetEvent只会随机给其中一个发送信号(只能唤醒一个)。

  这里的线程同步还可以使用委托与事件(推荐使用事件)来实现线程间的简单通讯,比如在某一线程执行到某一结点后,通过事件向另一个或者多个线程发送更多的信息。

  Monitor:

  上述的例子是各个子线程之间没有使用公共资源(公共变量、I/O设备等),它们只存在执行顺序上的先后;我们来找一个使用公共变量的例子试一试:

        static List<int> ids = new List<int>();
        static void Main(string[] args)
        {
            Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("--------------------");
            for (int i = 0; i < 100; i++)
            {
                Thread t = new Thread(Menthod);
                t.Start();
            }

            Console.ReadLine();
        }

        static void Menthod()
        {
            ids.Add(Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine(ids.Count);
            Console.WriteLine(ids[0]);
            Console.WriteLine("--------------------");
            ids.Clear();
        }

  这里新建100个子线程,然后使用一个静态公共变量List输出线程的Id,以上述方法运行时,有时会报出错误:索引超出范围。必须为非负值并小于集合大小!说明当前子线程输出Id时,该集合被Clear掉了,这就是一个很简单的线程安全问题,所以需要使用Monitor来进行锁住代码块,MSDN推荐定义一个私有的初始化不会再变的object变量作为一个排他锁,因为排他锁变了就没意义了,下面代码就可以变为:

        static readonly object locker = new object();
        static List<int> ids = new List<int>();
        static void Main(string[] args)
        {
            Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("--------------------");
            for (int i = 0; i < 100; i++)
            {
                Thread t = new Thread(Menthod);
                t.Start();
            }

            Console.ReadLine();
        }

        static void Menthod()
        {
            Monitor.Enter(locker);
            ids.Add(Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine(ids.Count);
            Console.WriteLine(ids[0]);
            Console.WriteLine("--------------------");
            ids.Clear();
            Monitor.Exit(locker);
        }

  当有一个线程进入锁住的代码块是,是在外面加锁,这样剩下的线程只能等待当前线程执行完毕后释放锁,这样的话就保证了List变量在取值时不会被其他线程清除掉;尽管List是一个线程安全类,就是多线程操作该类时只有一个线程操作的类,但是这里仍然避免不了线程安全的问题,因为仍然控制不了操作的顺序,在清除后读取肯定会报错。

  lock:

  调用Monitor执行只能有一个线程运行的代码块时,仍有可能会抛出异常,但是有时候又不能终止进程,使用try{}catch{}包起来是个解决方式,那干脆再封装一次Monitor的方法,于是lock便出现了,则上述的例子可以改写为:

        static void Menthod()
        {
            lock (locker)
            {
                ids.Add(Thread.CurrentThread.ManagedThreadId);
                Console.WriteLine(ids.Count);
                Console.WriteLine(ids[0]);
                Console.WriteLine("--------------------");
                ids.Clear();
            }
        }

  等价于:

        static void Menthod()
        {
            try
            {
                Monitor.Enter(locker);
                ids.Add(Thread.CurrentThread.ManagedThreadId);
                Console.WriteLine(ids.Count);
                Console.WriteLine(ids[0]);
                Console.WriteLine("--------------------");
                ids.Clear();
            }
            catch (Exception ex)
            {

            }
            finally
            {
                Monitor.Exit(locker);
            }
        }

  当线程进入lock代码块时,将会调用Monitor.Enter()方法,退出代码块会调用Monitor.Exit()方法。另外,Monitor还提供了三个静态方法Monitor.Pulse(),Monitor.PulseAll()和Monitor.Wait() ,用来实现一种唤醒机制的同步。关于这三个方法的用法,可以参考MSDN,我这里也在学习中,就先不讲述了。虽说lock没有Monitor功能强大,但是使用确实方便,这里取舍就看实际需求了。  

  补充

  线程同步的方式还有很多,比如Mutex。还有很多的方法,以后用到的时候在研究吧。
  Mutex:Mutex不具备Wait,Pulse,PulseAll的功能,因此,我们不能使用Mutex实现类似的唤醒的功能;不过Mutex有一个比较大的特点,Mutex是跨进程的,因此我们可以在同一台机器甚至远程的机器上的多个进程上使用同一个互斥体。

线程池

 目的

  上一篇内容提到,线程是由线程ID、程序计数器、寄存器集合和堆栈组成,是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一些在运行中必不可少的资源;这就意味着线程在进行创建与撤销的时候,都需要分配与清空一些资源,总归需要付出一定量的时空消耗;在一些大量使用线程(CPU密集、I/O密集)的进程里面,使用传统的new方法会频繁的创建、撤销线程,虽说线程的管理是由CLR来进行的,但是总归是影响性能,为了减少创建与撤销的时空消耗,便引入了线程池的概念:将线程实体池化,就是事先创建一定量的线程实体,然后放到一个容器中,做统一管理,没有任务时,线程处于空闲状态(差不多就是就绪状态),来任务后选择一个空闲线程来执行,执行完毕后自动关闭线程(没有被撤销,只是置为空闲状态)。

 CLR线程池

  CLR线程池是.NET框架中很重要的一部分,不光能被开发人员使用,自身的很多功能也是由线程池实现;我们在将任务委托给线程池的时候,是将该任务放到线程池的任务队列上,若线程池内存在空闲线程,则会将该任务委托给该线程,等待调度到CPU执行,若是没有空闲的线程且线程池所管理的线程数量还没有达到上限的时候,线程池便会创建新的Thread实体,否则,该任务会在队列中等待。

  数量上限:在CLR 2.0 SP1之前的版本中,线程池中 默认最大的线程数量 = 处理器数 * 25, CLR 2.0 SP1之后就变成了 默认最大线程数量 = 处理器数 * 250,线程上限可以改变,通过使用ThreadPool.GetMax+Threads和ThreadPool.SetMaxThreads方法,可以获取和设置线程池的最大线程数。

 使用

  线程池的使用更简单一些:

        static void Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem(new WaitCallback(Menthod1));
            ThreadPool.QueueUserWorkItem(new WaitCallback(Menthod2), "object");

            Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("--------------------");
            Console.ReadLine();
        }

        static void Menthod1(object obj)
        {
            Thread.Sleep(2000);
            Console.WriteLine("线程1的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("--------------------");
        }

        static void Menthod2(object obj)
        {
            Thread.Sleep(4000);
            Console.WriteLine("线程2的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("obj:{0}", obj);
            Console.WriteLine("--------------------");
        }

  这里QueueUserWorkItem方法需要传入一个QueueUserWorkItem委托(带object类型的参数,无返回值),所以我们需要线程执行的任务需要带一个object的参数,并且QueueUserWorkItem方法加入时存在一个重载,可以在这里传入一个参数。

  当然这里也可以使用Lambda表达式:

        ThreadPool.QueueUserWorkItem(new WaitCallback((object obj)=>{
            Thread.Sleep(2000);
            Console.WriteLine("线程3的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("obj:{0}", obj);
            Console.WriteLine("--------------------");
        }), "lambda");

 线程同步

  使用线程池并发执行任务同样会遇到线程安全的问题,一样需要进行同步,在涉及线程使用公共资源,Monitor、lock等方法与上述线程使用一样,同样能达到理想的效果,就不重复介绍了;但是对于控制执行顺序上,这个没有使用new线程来的自由。

  同步事件:

  在线程池中,没有Join方法,若想控制线程的执行顺序,我推荐使用主线程等待线程池任务执行完毕,阻塞主线程的方式,这里可以使用WaitHandle:

        static void Main(string[] args)
        {
            List<WaitHandle> handles = new List<WaitHandle>();

            AutoResetEvent autoReset1 = new AutoResetEvent(false);
            ThreadPool.QueueUserWorkItem(new WaitCallback(Menthod1), autoReset1);
            handles.Add(autoReset1);

            AutoResetEvent autoReset2 = new AutoResetEvent(false);
            ThreadPool.QueueUserWorkItem(new WaitCallback(Menthod2), autoReset2);
            handles.Add(autoReset2);

            WaitHandle.WaitAll(handles.ToArray());

            Console.WriteLine("主线程的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("--------------------");
            Console.ReadLine();
        }

        static void Menthod1(object obj)
        {
            Thread.Sleep(2000);
            Console.WriteLine("线程1的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("--------------------");
            AutoResetEvent handle = (AutoResetEvent)obj;
            handle.Set();
        }

        static void Menthod2(object obj)
        {
            Thread.Sleep(4000);
            Console.WriteLine("线程2的ID:{0}", Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine("--------------------");
            AutoResetEvent handle = (AutoResetEvent)obj;
            handle.Set();
        }

  在这里,给线程池每个相关的线程都创建一个AutoResetEvent,在执行完毕之后分别把属于自己的AutoResetEvent变为非终止,WaitHandle使用WaitAll方法阻塞主线程、等待所有的AutoResetEvent事件变为true,另外WaitHandle还有一个WaitAny方法阻塞,不过是只要其中一个线程结束,就会继续运行,不再阻塞。

  注:

  1、WaitHandle同样可以用于new创建线程的同步事件;

  2、WaitHandle等待方法(WaitAll、WaitAny)的数组长度的数目必须少于或等于 64 个,为了解决此限制,有网友封装了一个类,比较好用:

 MutipleThreadResetEvent

补充

  1、线程有前台线程和后台线程之分,使用new创建的线程默认为前台线程(可以使用IsBackground属性来进行更改),线程池里面都是后台线程

   前台线程:前台线程是不会被立即关闭的,它的关闭只会发生在自己执行完成时,不受外在因素的影响。假如应用程序退出,造成它的前台线程终止,此时CLR仍然保持活动并运行,使应用程序能继续运行,当它的的前台线程都终止后,整个进程才会被销毁。

   后台线程:后台线程是可以随时被CLR关闭而不引发异常的,也就是说当后台线程被关闭时,资源的回收是立即的,不等待的,也不考虑后台线程是否执行完成,就算是正在执行中也立即被终止。

  2、线程被系统调度到CPU执行时存在优先级:这里的优先级不是优先执行,而是被调度到CPU执行的概率高;使用new创建线程与线程池的优先级默认都是Normal,不过前者可以通过Priority属性来设置优先级。优先级有5个级别:Highest、AboveNormal、Normal、BelowNormal和Lowest。

  3、线程存在Suspend与Resume这两个过时的方法,但不是代表不能使用,只是微软不推荐你用,MSDN给出的原因是:请不要使用 Suspend 和 Resume 方法来同步线程活动。 没有办法知道当你暂停执行线程什么代码。 如果在安全权限评估期间持有锁,您挂起线程中的其他线程 AppDomain 可能被阻止。 如果执行类构造函数时,您挂起线程中的其他线程 AppDomain 中尝试使用类被阻止。 可以很容易发生死锁。你可以无视这个警告继续使用这两个方法进行线程同步,若觉得不怎么靠谱,那么可以在线程代码加入判断来保证执行正确性,或者使用控制同步事件(AutoResetEvent等)来实现线程同步。

  4、线程池的线程很珍贵,因为数量是有限的,所以不适合执行长时间的作业任务,适合执行短期并且频繁的作业任务,若想执行长时间的作业任务,建议使用new创建新线程的方式。毕竟线程池设计的初衷就是为了解决频繁创建与撤销线程而造成的资源浪费。

原文地址:https://www.cnblogs.com/cjm123/p/8624252.html

时间: 2024-08-07 02:48:32

菜鸟之旅——学习线程(线程和线程池)的相关文章

菜鸟之旅——学习线程(Task)

菜鸟之旅--学习线程(Task) 前面两篇回顾线程和线程池的使用方法,微软在.NET4.5推出了新的线程模型-Task.本篇将简单的介绍Task的使用方法. Task与线程 Task与线程或者说线程池关系紧密,可以说是基于线程池实现的,虽说任务最终还是要抛给线程去执行,但是Task仍然会比线程.线程池的开销要小,并且提供了可靠的API来控制线任务执行. 使用Task来执行的任务最终会交给线程池来执行,若该任务需要长时间执行,可以将其标记为LongRunning,这是便会单独去请求创建线程来执行该

菜鸟之旅——学习线程(基础)

愉悦的绅士 菜鸟之旅--学习线程(基础) 在现在的软件编程中,不可避免的会用到多线程或者其他方式来实现异步的目的,那么,线程是个什么东西,如何使用?这些都是需要去学习与摸索的东西.不过在学习线程之前,还是有一些知识需要掌握的,虽说都是书本上的东西,但是还是对线程的学习有一定的作用的. 进程 目的 现在的计算机存在很多的操作系统(OS),大部分操作系统都是实时操作系统,可以实时的响应用户的操作,它们往往都有共同的基本特征:并发.共享和虚拟,进程的产生于并发.共享有很大的联系. 操作系统可以"同时&

Android学习笔记之ExecutorService线程池的应用....

PS:转眼间就开学了...都不知道这个假期到底是怎么过去的.... 学习内容: ExecutorService线程池的应用... 1.如何创建线程池... 2.调用线程池的方法,获取线程执行完毕后的结果... 3.关闭线程...   首先我们先了解一下到底什么是线程池,只有了解了其中的道理,我们才能够进行应用...java.util.concurrent.ExecutorService表述了异步执行的机制   首先我们简单的举一个例子... package executor; import ja

linux学习之进程,线程和程序

                                                                                  程序.进程和线程的概念 1:程序和进程的差别 进程的出现最初是在UNIX下,用于表示多用户,多任务的操作系统环境下,应用程序在内存环境中基本执行单元的概念.进程是UNIX操作系统环境最基本的概念.是系统资源分配的最小单位.UNIX操作系统下的用户管理和资源分配等工作几乎都是操作系统通过对应用程序进程的控制实现的! 当使用c c++ j

[原]基础学习视频解码之视频线程

在上两篇文章[原]零基础学习视频解码之解码图像和[原]零基础学习视频解码之解码声音我们初步了解如何解码视频图像和视频声音.但是这些都是初步简单的解码出来而已,我们的主要功能是处理非常多:它是通过事件循环中运行,读取数据包,并在视频解码.所以,我们要做的就是拆分这些功能:我们将有一个线程,该线程将负责数据包进行解码;这些数据包将被添加到该队列中,并通过相应的音频和视频解码线程读取. 音频线:我们在上一篇已经建立了我们想要的方式; 视频线:视频线会相对比较麻烦一些,因为我们要自己显示自己的视频画面.

学习pthreads,管理线程的栈

进程的地址空间分成代码段,静态数据段,堆和栈段.线程栈的位置和大小是从它所属的进程的栈中切分出来的.每个栈必须足够大,以容纳所有对等线程的函数的执行以及它们将会调用的例程链.或许你会问为什么要进行线程栈的管理?因为栈的管理由系统自动管理.但是针对具体问题,有可能系统自动管理的栈不能满足运行的要求,这时对线程的栈的管理是必要的.本文分为三个部分,第一部分给出管理线程栈的代码示例,第二部分对代码进行讲解,第三部分给出运行结果. 一 代码示例 本例程利用线程的属性对象,获取栈的大小,并改变栈的大小.

Android(java)学习笔记65:线程的生命周期

我们学习线程本质就是学习:如何开始线程和终止线程.下面这个关于线程的生命周期图,要牢记:

Linux程序设计学习笔记----多线程编程线程同步机制之互斥量(锁)与读写锁

互斥锁通信机制 基本原理 互斥锁以排他方式防止共享数据被并发访问,互斥锁是一个二元变量,状态为开(0)和关(1),将某个共享资源与某个互斥锁逻辑上绑定之后,对该资源的访问操作如下: (1)在访问该资源之前需要首先申请互斥锁,如果锁处于开状态,则申请得到锁并立即上锁(关),防止其他进程访问资源,如果锁处于关,则默认阻塞等待. (2)只有锁定该互斥锁的进程才能释放该互斥锁. 互斥量类型声明为pthread_mutex_t数据类型,在<bits/pthreadtypes.h>中有具体的定义. 互斥量

Java并发学习之十八——线程同步工具之CyclicBarrier

本文是学习网络上的文章时的总结,感谢大家无私的分享. CyclicBarrier 类有一个整数初始值,此值表示将在同一点同步的线程数量.当其中一个线程到达确定点,它会调用await() 方法来等待其他线程.当线程调用这个方法,CyclicBarrier阻塞线程进入休眠直到其他线程到达.当最后一个线程调用CyclicBarrier 类的await() 方法,它唤醒所有等待的线程并继续执行它们的任务. 注意比较CountDownLatch和CyclicBarrier: 1.CountDownLatc