zoukankan      html  css  js  c++  java
  • C# 多线程学习系列二

    一、关于前台线程和后台线程

    1、简介

    CLR中线程分为两种类型,一种是前台线程、另一种是后台线程.

    前台线程:应用程序的主线程、Thread构造的线程都默认为前台线程

    后台线程:线程池线程都为后台线程

    2、区别

    前台线程:前台线程一般执行重要性很高的任务,至于什么是重要性很高,这个需要结合业务综合考虑,哪些操作是当前应用程序必须执行的.

    后台线程:这里需要注意,当一个进程的所有前台线程关闭时,也就是当应用程序推出的时候,无论后台线程有没有执行完它的任务,它都会被强制关闭.但是,当应用程序开启时,它又会重新启动.后台线程一般执行不重要、耗时很短的任务,就算进程(应用程序)关闭了,导致它强制关闭,也不会造成影响的任务.比如系统清理程序等.

    注意:一般进程会在所有的前台线程执行完毕时关闭.

    3、代码演示区别

    前台线程:

            static void Main(string[] args)
            {
                var thread = new Thread(DoWork);
                thread.Start();
            }
    
            private static void DoWork()
            {
                Thread.Sleep(2000);
                Console.WriteLine("子线程处理完工作,已结束");
            }

    子线程停留两秒后,控制台消失.

    后台线程:

            static void Main(string[] args)
            {
                var thread = new Thread(DoWork);
                thread.IsBackground = true;
                thread.Start();
            }
    
            private static void DoWork()
            {
                Thread.Sleep(2000);
                Console.WriteLine("子线程处理完工作,已结束");
            }

    控制台不等子线程处理完,直接消失.

    效果很直观,前台线程会执行完才结束进程,但是后台线程则不会.

    二、使用Thread构建异步操作(受限制)

    1、Thread简介和使用场景

    关于使用Thread类构建线程执行异步操作有以下几点需要注意的:

    (1)、Thread本身微软并不建议使用,应为它其中的很多Api并不靠谱(如Start、Join、IsBackground等)

    (2)、由于(1)的问题,所以微软将整个Thread类都不开放给Windows Store应用

    虽然Thread有很多不好的缺点,但是它还是有它的用武之处,只要满足以下条件:

    (1)、如果执行的代码处于一种特定的状态,这种状态对于线程池来说时非同寻常的.

    (2)、线程需要以非普通优先级运行.所有线程池线程都以普通优先级运行,虽然可以修改,但是在不同线程池之间,这种优先级无法持续.

    (3)、需要线程变现为一个前台线程(什么是前台线程,上面有解释),防止应用程序在线程结束前就关闭了.这个后台线程无法做到(也就是线程池线程)

    (4)、线程需要执行长时间的计算任务.

    (5)、线程可能存在终止的情况,程序池线程不存在这种情况(它会一直执行).Thread类的Abort方法.

    2、代码演示

            static void Main(string[] args)
            {
                Console.WriteLine("主线程开始执行,并调用了一个不带参子线程执行其他的任务");
                Thread.Sleep(500);
                var thread = new Thread(ExecuteOtherWork);//构造一个线程对象,实际没有开启线程
                thread.Start();//开启子线程,做其他的任务
                Console.WriteLine("主线程继续做它的事情");
                thread.Join();//等待不带参子线程做完它的任务
                Console.WriteLine("主线程开始执行,并调用了一个带参的子线程执行其他的任务");
                var thread1 = new Thread(ExecuteWorkWithParameter);
                thread1.Start(666);
                Console.WriteLine("主线程继续做它的事情");
                thread1.Join();
                Console.WriteLine("3个线程完美的完成了它们的任务");
                Console.Read();
            }
    
            /// <summary>
            /// 开启一个不传递参数的子线程,并执行它的任务
            /// </summary>
            static void ExecuteOtherWork()
            {
                Thread.Sleep(500);
                Console.WriteLine("不带参的子线程开始做它的事情,处理这件事情需要花费点时间");
                Thread.Sleep(2000);//子线程休息5秒
                Console.WriteLine("不带参的子线程完美的完成了它的任务");
                Thread.Sleep(500);
            }
    
            /// <summary>
            /// 开启一个带传递参数的子线程,并执行它的任务
            /// </summary>
            /// <param name="parameter"></param>
            static void ExecuteWorkWithParameter(object parameter)
            {
                Thread.Sleep(500);
                Console.WriteLine("带参的子线程开始做它的事情,处理这件事情需要花费点时间,主线程传递给它的参数是:{0}", parameter.ToString());
                Thread.Sleep(2000);//子线程休息5秒
                Console.WriteLine("带参的子线程完美的完成了它的任务");
                Thread.Sleep(500);
            }

    三、CLR线程池

    1、进程和CLR的关系
    一个进程可以只包含一个CLR,也可以包含多个CLR
    2、CLR和AppDomain的关系
    一个CLR可以包含多个AppDomain
    3、CLR和线程池的关系
    一个CLR只包含一个线程池
    所以得出一个CLR下的多个AppDomain共享一个线程池和一个进程下的多个CLR拥有多个线程池的结论.注:多个线程池间的线程池相互不产生影响.

    4、CLR和线程池和操作请求队列的关系
    (1)、CLR第一次初始化时,线程池并没有线程,当应用程序调用异步代码执行一个方法时,会将该请求记录项加入到操作请求队列中,线程池的代码从这个队列中获取记录项,并派发给线程池线程,接着
    线程池会创建线程,当然这里会有性能开销,但是当该线程执行完毕之后,线程池会回收这个线程,这里注意:线程池不会直接销毁这个线程,而是让它处于闲置状态.这样就不会产生额外的性能开销.
    但是如果该线程如果长时间处于闲置状态,那么线程池会销毁它,关于这个时间的计算很复杂,各个CLR对它的定义各不相同.
    (2)、当应用程序向线程池发起了多个请求,线程池会尝试用一个线程来处理你所有的请求,但是如果这个线程处理压力过大,那么它会开启一个新的线程来给它分担压力.以此类推.
    (3)、线程池之包含了少量线程,因为如果线程太多,会增加性能开销,当然如果你升级了你电脑的cpu,线程池则会创建更多的线程.这个过程线程池会自动的去读取你得cpu核数信息,自动的去分配合适的线程数
    合理地分配CPU资源.当应用程序的压力减轻,那么它会销毁不用的线程.

    (4)、代码演示

            static void Main(string[] args)
            {
                Console.WriteLine("主线程开始执行,调用一个带参数的线程池子线程");
                //主线程调用ThreadPool.QueueUserWorkItem方法向线程池的操作队列添加一个记录项
                //线程池会遍历这个操作队列的所有记录项,然后将记录项中派发给一个线程池线程
                //接着线程池的线程就开始执行ExecuteOtherWork方法(同时接受了主线程传递给它的参数)
                ThreadPool.QueueUserWorkItem(ExecuteOtherWork,666);
                Console.WriteLine("主线程继续执行");
                Console.WriteLine("两个线程全部执行完毕");
                Console.Read();//这行代码必须加,因为线程池是后台线程,当进程关闭,该进程所有的后台线程都会被关闭,不管是否执行完毕.
            }
    
            /// <summary>
            /// 线程池子线程调用的方法
            /// </summary>
            /// <param name="state"></param>
            static void ExecuteOtherWork(object state)
            {
                Console.WriteLine("线程池子线程开始执行,主线程传递给它的参数是:{0}", state);
                Console.WriteLine("线程池子线程执行完毕");
            }

    注:这里的输出顺序不确定,因为在多核机器下,可能线程调度器会同时执行主线程和子线程.

    四、关于线程池线程的执行上下文

    (1)、什么是执行上下文

    执行上下文是初始线程的环境描述的数据结构,该结构包含以下东西:

    i、安全设置(压缩栈、Thread的Principal属性( 获取或设置线程的当前负责人(对基于角色的安全性而言))和Windows身份)

    ii、宿主设置 详情参见HostExecutionContext、HostExecutionContextManager类,通过该类可以设置宿主上下文的状态、以及创建当前宿主上下文的副本.代码,并设置子线程的上下文为主线程的上下文:

            static void Main(string[] args)
            {
                Console.WriteLine("主线程开始执行,调用一个带参数的线程池子线程");
                var mainThreadContext = new HostExecutionContext("0");
                mainThreadContext=mainThreadContext.CreateCopy();
                var thread = new Thread(ExecuteOtherWork);
                thread.Start(mainThreadContext);
                Console.Read();
            }
    
            /// <summary>
            /// 线程池子线程调用的方法
            /// </summary>
            /// <param name="state"></param>
            static void ExecuteOtherWork(object state)
            {
                var manager = new HostExecutionContextManager();
                manager.SetHostExecutionContext(state as HostExecutionContext);
                Console.WriteLine("子线程执行完毕");
            }

    凑合着看,暂时还没有发现这么做的实际意义.可能只有微软知道。哈哈!CLR默认造成初始线程的上下文流向任何子线程。

    注:关于上下文复制的这种机制,很清楚,肯定会造成性能上的开销,每开启一个新的线程就会复制原有线程的上下文给新的线程.

    但是考虑到性能问题,MS提供了ExecutionContext

            private static string shareKey = "线程之间共享的数据槽值键";
            static void Main(string[] args)
            {
                
                Console.WriteLine("主线程开始执行,调用一个带参数的线程池子线程");
                CallContext.LogicalSetData(shareKey, "666");
                ExecutionContext.SuppressFlow();
                var thread = new Thread(ExecuteOtherWork);
                thread.Start();
                Console.Read();
            }
    
            /// <summary>
            /// 线程池子线程调用的方法
            /// </summary>
            /// <param name="state"></param>
            static void ExecuteOtherWork(object state)
            {
                Console.WriteLine("线程之间共享的数据槽值:{0}", CallContext.LogicalGetData(shareKey));
                Console.WriteLine("子线程执行完毕");
            }

    关于CallContext.LogicalSetData参考下面的例子

    iii、逻辑调用上下文数据结构CallContext类,关于它的用法,如下:

            private static string notShareContextKey = "线程内唯一的对象,无法共享到其他线程";
    
            private static string shareContextKey = "线程之间共享的对象,可以传播到其他线程";
    
            static void Main(string[] args)
            {
                Console.WriteLine("主线程开始执行,调用一个带参数的线程池子线程");
                CallContext.SetData(notShareContextKey, "111");
                CallContext.LogicalSetData(shareContextKey, "666");
                var thread = new Thread(ExecuteOtherWork);
                thread.Start();
                Console.WriteLine("看看主线程能不能通过CallContext.SetData方法拿到这个数据:{0}", CallContext.GetData(notShareContextKey) ??"没有拿到");
                Console.WriteLine("看看主线程能不能通过CallContext.LogicalSetData方法拿到主线程的逻辑上下文对象里面设置的数据:{0}", CallContext.LogicalGetData(shareContextKey) ?? "没有拿到");
                Console.Read();
            }
    
            /// <summary>
            /// 线程池子线程调用的方法
            /// </summary>
            /// <param name="state"></param>
            static void ExecuteOtherWork(object state)
            {
                var obj=CallContext.GetData("线程内唯一的对象,无法共享到其他线程");
                Console.WriteLine("看看子线程能不能通过CallContext.SetData方法拿到主线程的逻辑上下文对象里面设置的数据:{0}", CallContext.GetData(notShareContextKey) ?? "没有拿到");
                Console.WriteLine("看看子线程能不能通过CallContext.LogicalSetData方法拿到主线程的逻辑上下文对象里面设置的数据:{0}", CallContext.LogicalGetData(shareContextKey) ?? "没有拿到");
                Console.WriteLine("");
                Console.WriteLine("子线程执行完毕");
            }

    CallContext.SetData设置的数据线程内唯一,不能跨线程调用,但是CallContext.LogicalSetData可以跨线程调用.后者类似于HttpContext的Session机制,用于保存用户信息,不受多线程的影响,如果你希望你的数据随着线程的消失而消失可以使用前者来做,其实HttpContext上下文的本质就是使用CallContext,我推测的,没有检验,效果是一样的,本身用户的请求就相当于一个线程.所以,可以通过对这两者的理解,可以封装一个对象,该对象维持一个应用程序上下文,同时能满足Web应用,可其他基于线程池的应用.即使在多线程环境下,也能很好的维护一些应用全局共享的关键数据.

  • 相关阅读:
    Oracle创建自增字段方法-ORACLE SEQUENCE的简介
    iOS项目开发实战——使用Xcode6设计自己定义控件与图形
    准备开源用javascript写Tomcat下的WebApp的项目
    Codeforces Round #256 (Div. 2) B. Suffix Structures
    静默方式安装10g数据库软件+升级patch+手工建库
    oracle 数据库开发面试题
    待机异常篇
    HTTP状态码(HTTP Status Code)
    POJ3126——Prime Path
    RHEL7 -- 通过gerp使用正则表达式
  • 原文地址:https://www.cnblogs.com/GreenLeaves/p/9955781.html
Copyright © 2011-2022 走看看