一、线程的基础知识

2019-12-16  本文已影响0人  pingwazi

回到主目录

1、使用场景

正如“不识庐山真面目,只缘身在此山中。”所描述的场景一样,实际上我们日常使用的很多软件、工具大多都是多线程的(比如web服务器IIS、数据库等),只是你身处其中,没有察觉到罢了。那么我们能够将多线程应用于什么场景呢?
实际上我们最有可能使用的场景就是并行编程,这种编程方式能够使计算机资源得到充分利用。其次是算法预测分析,当需要分析解决一个问题的不同算法时,可以使用多线程同时执行不同的算法,谁先执行完成的算法就是最优的。如果你是在作界面开发,那么也有可能使用到多线程技术,因为将UI线程和工作线程分开后,可以保证界面不卡顿。

2、线程的创建方式

2.2 Thread类

直接在实例化Thread的时候,指定一个委托即可

    class Program
    {
        static int _number = 0;
        static void Main(string[] args)
        {
            Thread thread = new Thread(() => { Thread.Sleep(1000 * 2); Console.WriteLine("工作线程"); });
            thread.Start();
            Console.WriteLine("主线程");
            Console.ReadKey();
        }
    }

2.2、通过任务并行库(TPL)使用线程池

通过创建任务的方式来间接使用线程池中的线程执行传入的委托是比较常见的一种方式

    class Program
    {
        static void Main(string[] args)
        {
            Task.Factory.StartNew(() => { Thread.Sleep(1000 * 2); Console.WriteLine("工作线程执行了"); });
            Console.WriteLine("主线程执行了");
            Console.ReadKey();
        } 
    }

2.3、直接使用线程池

通过ThreadPool使用线程池中的线程执行传入的委托

    class Program
    {
        static void Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem((stateObj) => { Thread.Sleep(1000 * 2); Console.WriteLine("工作线程执行了"); });
            Console.WriteLine("主线程执行了");
            Console.ReadKey();
        } 
    }

2.4、异步委托

异步委托是借助委托实现的异步功能,当调用委托的BeginInvoke()方法的时,可以指定一个当执行完成后的异步回调。它可以避免委托调用后造成当前线程阻塞的问题。\color{red}{(注:这种方式目前在.NetCore平台上不支持)}

class Program
    {
        delegate int WorkDelegate(int number, out int tempNumber);
        static void Main(string[] args)
        {
            WorkDelegate work = new WorkDelegate(Done);
            IAsyncResult result = work.BeginInvoke(8, out int tempNumber, CallBack, work);
            result.AsyncWaitHandle.WaitOne();//会阻塞当前线程直到Done运行结束
            Console.WriteLine("按任意键退出");
            Console.ReadKey();

        }
        static int Done(int number, out int tempNumbew)
        {
            Thread.Sleep(1000 * 2);//休眠两秒
            tempNumbew = (int)Math.Pow(number, 2);
            return tempNumbew - 1;
        }
        static void CallBack(IAsyncResult asyncResult)
        {
            Thread.Sleep(1000 * 2);//休眠两秒
            WorkDelegate target = (WorkDelegate)asyncResult.AsyncState;//AsyncState的值是BeginInvoke的最后一个参数
            int result = target.EndInvoke(out int tempNumber, asyncResult);
            Console.WriteLine($"完成回调result={result},tempNumber={tempNumber}");
        }
    }

3、线程控制

3.1 休眠当前线程

注意调用Thread.Sleep()是休眠当前线程,也就说在哪个线程中调用的,那么它就会休眠哪个线程。

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("开始运行");
            Thread.Sleep(3000);//程序会休眠3秒后再运行
            Console.WriteLine("运行结束");
            Console.ReadKey();
        }
    }

3.2 将一个线程加入到当前线程中

在一个线程中等待另外一个线程,直到所等待的线程运行结束。

class Program
    {
        static void Main(string[] args)
        {
         Thread  thread=new  Thread(Work);
         thread.Start();
         thread.Join();
         Console.WriteLine("按任意键退出...");
         Console.Read();
        }
        static void Work()
        {
            Console.WriteLine("开始运行");
            Thread.Sleep(1000*3);
            Console.WriteLine("运行结束");
        }
    }

3.3 出让当前线程的时间片
出让当前线程的时间片,并且不是让线程停止执行,而是让线程退出执行状态,等待操作系统重新调用。在介绍自旋锁的时候会再次提及这个功能。

    class Program
    {
        static void Main(string[] args)
        {
         Thread  thread=new  Thread(Work);
         thread.Start();
         thread.Join();
         Console.WriteLine("按任意键退出...");
         Console.Read();
        }
        static void Work()
        {
            Console.WriteLine("开始运行");
            Thread.Yield();//出让当前正在使用的CPU资源(比如:时间片),等待操作系统重新调用
            Console.WriteLine("运行结束");
        }
    }
}

4、前台线程与后台线程

前台线程与后台线最主要的区别就是当所有前台线程结束的时候,整个应用程序都会退出,不论后台线程是否运行结束。
通过Thread类创建的线程,默认都是前台线程;而通过线程池创建的线程都是后台线程。

5、线程优先级

可以通过Thread的ThreadPriority属性来设置一个线程相对于操作系统中其他线程的优先级,也就是指定ThreadPriority中的一个枚举值,ThreadPriority是一个枚举。但是设置线程优先级的时候最好考虑一下是否真的需要设置它,如果设置的过高,其他线程可以可能会资源饥饿(不能配分足够的CPU时间);如果设置得过低,可能会导致自己资源饥饿。
另外线程的优先级还依赖与所属进程的优先级。进程的优先级可以通过Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.Normal;方式来设置

6、线程中异常的正确处理方式

假设线程在A中创建了线程B,那么处理线程B中的异常的正确方式,应该是在线程B内部处理,而不是在线程A中处理。

错误的处理方式(线程B中的异常任然是未处理异常):

class Program
    {
        static void Main(string[] args)
        {
            //这种方式是没有办法正确处理线程中发生的异常的
            try
            {

                Thread thread = new Thread(Work);
                thread.Start();
                thread.Join();
            }
            catch (Exception ex)
            {
                Console.WriteLine($"线程中发生异常{ex.Message}");
            }
            Console.WriteLine("按任意键退出...");
            Console.Read();
        }
        static void Work()
        {
            int i = 0;
            int j = 1 / i;
        }
    }

正确的处理方式:

class Program
    {
        static void Main(string[] args)
        {
            Thread thread = new Thread(Work);
            thread.Start();
            thread.Join();
            Console.WriteLine("按任意键退出...");
            Console.Read();
        }
        static void Work()
        {
            try
            {
                int i = 0;
                int j = 1 / i;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"线程中发生异常{ex.Message}");
            }
        }
    }

7、线程池

线程池中的线程是共享的!!!相比于通过Thread手动创建线程,线程池能够消耗更少的资源来创建线程,并且线程池会对池中的线程进行统一管理,保证同一时刻不会有太多的线程在运行,因为太多的活动线程会增加操作系统的负担,降低CPU的缓存效果。
另外,一个线程在存活期间会占用大约1MB的内存资源,并且在创建一个线程的时候也需要花费一定的时间去准备资源(比如去开辟线程的栈空间)。如果手动同时创建了成百上千个线程,那么这将是将是非常恐怖的操作。然后而线程池线程则不会出现这种情况,因为池中的线程是共享的,如果你向线程池发出了需要一千个线程来执行一个批量任务,线程池并不会创建一千个线程供你使用,而是在已有线程数量的基础上(可能会适量增加一些线程)去执行你的任务,没有执行的任务会通过排队到线程池中拿线程去运行。当没有任务需要执行时,线程池会回收多余的线程以避免资源浪费。

7.1 最小线程数

线程池中有一个不得不了解的优化方案,那就设置最小线程数。默认情况下,线程池创建线程之间会有一定的间隔时间,比如说,你向线程池请求四个线程来执行四个任务,但线程池可并不会立即给你四个线程,而是先给你一个线程让你执行其中一个任务,在一段时间后(同一个线程可能会相继执行第二个、第三个…),线程池发现你的任务还没有执行完,那么它会再创建一个线程给你去执行任务,再过一段时间后,线程池发现你的任务执行完了,那线程池就不会再创建第三、第四个线程给你。
这是线程池默认的机制,但我们可以通过设置改变这种机制,在向线程池请求线程的时候,可以明确告诉线程立即给我创建四个线程,中间不能有间隔。那么怎么设置的呢?就是通过设置线程池的最小线程数。ThreadPool.SetMinThreads(10, 10);//第二个参数用于设置I/O完成线程的,因为I/O操作可能会阻塞很长时间,所以线程池是将共享线程和I/O线程给分开了的。

上一篇 下一篇

猜你喜欢

热点阅读