C#:进程/线程/多线程/AppDomain详解


  • 好好理解

Task wait 是对单个task进行等待

Task waitall 是对多个task进行等待

Task waitany 是对任意一个task进行等待,执行哪一个就等哪一个

申明异步方法一般是返回类型Task Task Task void

要等task执行结束,才会执行await这个task下面的语句

Task任务,允许多个任务可以有多个线程(或在同一线程内进行)不用等待上一个执行结束,就可以直接进行下一个任务

Task是基于Thread的

异步是同时执行多个任务

多任务可同时进行,靠的是异步去实现

异步方法必须Task

异步可以让UI线程闲置下来

多线程只是实现异步的手段之一,异步与多线程的概念不要混淆了

Task.run会启动一个新的线程去执行Task,所包含的代码运行在另一个线程里,这才是消耗资源的

async await 不开启新的线程,只是一个多任务的处理模式
异步 就是在线程池里选择一个合适的线程去执行代码


线程池概念

每个进程由多个线程组成

前台进程:只有所有前台进程都关闭才完成程序的关闭

后台进程:只有所有前台线程结束,后台线程自动结束

多线程:就是为了让计算机做多件事情,节约时间,同时处理多件事情。

产生一个线程4个步骤

编写产生线程所要执行的方法

引用System.Threading命名空间

实例化Thread类,并传入一个线程要运行方法的委托 。(这时线程已经产生,但是没有运行)

调用Thread实例的Start方法,标记该线程可以被cpu执行了,具体时间由cpu决定

在.NET下,是不允许跨线程访问UI元素

多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。

多线程技术

多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。

假设一个服务器完成一项任务所需时间为:

T1 创建线程时间

T2 在线程中执行任务的时间

T3 销毁线程时间。

如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。

一个线程池包括以下四个基本组成部分:

1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;

2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;

3、任务接口 (Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;

4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。完后的收尾工作,任务的执行状态等;

同步异步是对方法执行的描述

同步:完成一行计算以后,再进入下一行

异步:不会等待方法完成,会直接进入下一行


线程是一个可执行路径,它可以独立于其他线程执行

每个线程都在操作系统内执行,而操作系统进程提供了程序命运独立环境

单线程应用,在进程的独立环境只跑一个线程,所以线程拥有独占权

多线程应用,单个进程中跑多个线程,他们会共享执行环境(尤其是内存)

比如wpf应用,在读取数据时,ui线程就不响应了,所以应该开辟一个新线程

线程被抢占

他的执行与另一个线程上的代码交织的那一点

关于Process/Thread/ThreadPool/Task

这三者都是为了处理耗时任务,都是异步的,windows是抢占式操作系统

1、进程(process)

当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。

而一个进程又是由多个线程所组成的。

进程(Process)是Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。一个正在运行的应用程序在操作系统中被视为一个进程,进程可以包括一个或多个线程。线程是操作系统分配处理器时间的基本单元,在进程中可以有多个线程同时执行代码。进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。是应用程序的一个运行例程,是应用程序的一次动态执行过程。
2、线程(thread)

线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的。

多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。 线程的生命周期包括:创建线程 、挂起线程、线程等待、终止线程
3、同步(sync)

发出一个功能调用时,在没有得到结果之前,该调用就不返回。
4、异步(async)

与同步相对,调用在发出之后,这个调用就直接返回了,所以没有返回结果。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。

通知调用者的三种方式:

状态:即监听被调用者的状态(轮询),调用者需要每隔一定时间检查一次,效率会很低。

通知:当被调用者执行完成后,发出通知告知调用者,无需消耗太多性能。

回调:与通知类似,当被调用者执行完成后,会调用调用者提供的回调函数。
5、阻塞(block)

阻塞调用是指调用结果返回(或者收到通知)之前,当前线程会被挂起,即不继续执行后续操作。

简单来说,等前一件做完了才能做下一件事。
6、非阻塞(non-block)

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

[0] 进程与线程 Process& Thread

进程是指一个程序在计算机上运行时,全部的计算资源的合集;

线程是程序的最小执行单位,独立调度和分派的基本单位,包含计算资源,任何一个操作的响应都是线程来完成的;每条线程可以并行执行不同的任务

  • 多线程是指多个线程并发执行;
  • 多线程虽然能够提升程序的运行效率,但是消耗的资源更多,所以线程并不是越多越好。

每个进程由多个线程组成

多线程:就是为了让计算机做多件事情,节约时间,同时处理多件事情。

**进程(Process)**是Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。进程之间是相对独立的,一个进程无法直接访问另一个进程的数据 (除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行 ,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界

**线程(thread)**是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是行程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并行多个线程,每条线程并行执行不同的任务。

形象理解

进程(Porcess)

进程 就像厨师,是系统进行资源分配和调度的一个独立单位

每个厨师都有自己的工作空间,比如自己的灶台,砧板和厨具,这些都独立不共享,类似于进程有自己的内存空间

厨师之间互不干扰,可以同时工作,但是一个厨师忙碌时不会影响到另一个厨师,类似于进程之间相互独立

线程(Thread)

线程 就像厨师的手,是进程中一个实际运作的单位

厨师的手可以同时进行多个动作,比如一只手在调味,另一只手在翻炒,就像一个进程中的多个线程可以并行处理多个任务

手的动作都是在厨师的控制下进行的,可以共享厨具和食材,就像线程之间可以共享进程的资源。

进程与线程的区别

独立性:进程之间相互独立,就像不同的厨师有各自独立的工作环境,线程之间可以共享进程的资源,就像厨师的手,共享他的工具和食材。

资源消耗:启动一个新的厨师**(Porcess),需要更多的资源**,比如更多的食材和厨具,而从单手到双手的**(多线程)则资源消耗相对比较少**。

通讯

厨师之间通讯可以通过对讲机或者走过去说话,比较麻烦,类似于进程之间通讯比较复杂

而同一个厨师两只手之间协作更加直接高效,类似于线程之间通讯写作更加简单


同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文档描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈,自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)

一个进程可以有很多线程,每条线程并行执行不同的任务。

在多核或多CPU,或支持Hyper-threading的CPU上使用多线程进程设计的好处是显而易见,即提高了进程的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责IO处理、人机交互而常备阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了进程的执行效率。

进程的一些属性和方法

1.1 Process 的属性与方法

在 System.Diagnostics 命名空间当中存在Process类,专门用于管理进程的开始、结束,访问进程中的模块,获取进程中的线程,设定进程的优先级别等。

表1.0 显示了Process类的常用属性

属性 说明
BasePriority 获取关联进程的基本优先级。
ExitCode 获取关联进程终止时指定的值。
ExitTime 获取关联进程退出的时间。
Handle 返回关联进程的本机句柄。
HandleCount 获取由进程打开的句柄数。
HasExited 获取指示关联进程是否已终止的值。
Id 获取关联进程的唯一标识符。
MachineName 获取关联进程正在其上运行的计算机的名称。
MainModule 获取关联进程的主模块。
Modules 获取已由关联进程加载的模块。
PriorityClass 获取或设置关联进程的总体优先级类别。
ProcessName 获取该进程的名称。
StartInfo 获取或设置要传递给Process的Start方法的属性。
StartTime 获取关联进程启动的时间。
SynchronizingObject 获取或设置用于封送由于进程退出事件而发出的事件处理程序调用的对象。
Threads 获取在关联进程中运行的一组线程

除了上述属性,Process类也定义了下列表1.0经常使用的方法:

方法 说明
GetProcessById 创建新的 Process 组件,并将其与您指定的现有进程资源关联。
GetProcessByName 创建多个新的 Process 组件,并将其与您指定的现有进程资源关联。
GetCurrentProcess 获取新的 Process 组件并将其与当前活动的进程关联。
GetProcesses 获取本地计算机上正在运行的每一个进程列表。
Start 启动一个进程。
Kill 立即停止关联的进程。
Close 释放与此组件关联的所有资源。
WaitForExit 指示 Process 组件无限期地等待关联进程退出。

微软官方例子:以下示例使用 类的 Process 实例来启动进程。

csharp 复制代码
using System;
using System.Diagnostics;
using System.ComponentModel;

namespace MyProcessSample
{
    class MyProcess
    {
        public static void Main()
        {
            try
            {
                using (Process myProcess = new Process())
                {
                    myProcess.StartInfo.UseShellExecute = false;
                    // You can start any process, HelloWorld is a do-nothing example.
                    myProcess.StartInfo.FileName = "C:\\HelloWorld.exe";
                    myProcess.StartInfo.CreateNoWindow = true;
                    myProcess.Start();
                    // This code assumes the process you are starting will terminate itself.
                    // Given that it is started without a window so you cannot terminate it
                    // on the desktop, it must terminate itself or you can do it programmatically
                    // from this application using the Kill method.
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }
    }
}

微软官方例子:以下示例使用 Process 类本身和静态 Start 方法启动进程。

csharp 复制代码
using System;
using System.Diagnostics;
using System.ComponentModel;

namespace MyProcessSample
{
    class MyProcess
    {
        // Opens the Internet Explorer application.
        void OpenApplication(string myFavoritesPath)
        {
            // Start Internet Explorer. Defaults to the home page.
            Process.Start("IExplore.exe");

            // Display the contents of the favorites folder in the browser.
            Process.Start(myFavoritesPath);
        }

        // Opens urls and .html documents using Internet Explorer.
        void OpenWithArguments()
        {
            // url's are not considered documents. They can only be opened
            // by passing them as arguments.
            Process.Start("IExplore.exe", "www.northwindtraders.com");

            // Start a Web page using a browser associated with .html and .asp files.
            Process.Start("IExplore.exe", "C:\\myPath\\myFile.htm");
            Process.Start("IExplore.exe", "C:\\myPath\\myFile.asp");
        }

        // Uses the ProcessStartInfo class to start new processes,
        // both in a minimized mode.
        void OpenWithStartInfo()
        {
            ProcessStartInfo startInfo = new ProcessStartInfo("IExplore.exe");
            startInfo.WindowStyle = ProcessWindowStyle.Minimized;

            Process.Start(startInfo);

            startInfo.Arguments = "www.northwindtraders.com";

            Process.Start(startInfo);
        }

        static void Main()
        {
            // Get the path that stores favorite links.
            string myFavoritesPath =
                Environment.GetFolderPath(Environment.SpecialFolder.Favorites);

            MyProcess myProcess = new MyProcess();

            myProcess.OpenApplication(myFavoritesPath);
            myProcess.OpenWithArguments();
            myProcess.OpenWithStartInfo();
        }
    }
}

Process组件提供对计算机上正在运行的进程的访问权限。 用最简单的术语来说,进程是一个正在运行的应用。 线程是操作系统分配处理器时间的基本单元。 线程可以执行进程代码的任何部分,包括当前由另一个线程执行的部分。

组件 Process 是用于启动、停止、控制和监视应用的有用工具。 可以使用 Process 组件获取正在运行的进程的列表,也可以启动新进程。 组件 Process 用于访问系统进程。 Process组件初始化后,它可用于获取有关正在运行的进程的信息。 此类信息包括线程集、加载的模块 (.dll 和.exe文件) ,以及进程正在使用的内存量等性能信息。

此类型实现 IDisposable 接口。 在使用完类型后,您应直接或间接释放类型。 若要直接释放类型,请在 try/finally 块中调用其 Dispose 方法。

进程的建立与销毁

csharp 复制代码
static void Main(string[] args)
{
    Process process = Process.Start("notepad.exe","File.txt");
    Thread.Sleep(2000);
    process.Kill();
}

列举本机的进程

csharp 复制代码
static void Main(string[] args)
{
  var processList = Process.GetProcesses()
      .OrderBy(x=>x.Id)
      .Take(10);
  foreach (var process in processList)
  Console.WriteLine(string.Format("ProcessId is:{0} \t ProcessName is:{1}",process.Id, process.ProcessName));
  Console.ReadKey();
}

获取进程中模块的信息

csharp 复制代码
static void Main(string[] args)
{
    var moduleList = Process.GetCurrentProcess().Modules;
    foreach (System.Diagnostics.ProcessModule module in moduleList)
        Console.WriteLine(string.Format("{0}\n  URL:{1}\n  Version:{2}",
        module.ModuleName,module.FileName,module.FileVersionInfo.FileVersion));
    Console.ReadKey();
}

[1] 什么是Thread 线程

一个线程是一组执行指令,当我们提及多线程的时候会想到thread和threadpool,这都是异步操作,Threadpool其实就是Thread的集合 ,具有很多优势,不过在任务多的时候全局队列会存在竞争而消耗资源。Thread默认为前台线程,主程序必须等线程跑完才会关闭 ,而ThreadPool相反,ThreadPool属于后台进程

总结:Threadpool确实比Thread性能优,但是两者都没有很好的api区控制,如果线程执行无响应就只能等待结束,从而诞生了Task任务。


操作系统能够优先访问CPU,并能够调整不同程序访问CPU的优先级,避免某一个程序独占CPU的情况发生。

可以认为线程是一个虚拟进程,用于独立运行一个特定的程序。

线程会消耗大量的操作系统资源,多个线程共享一个物理处理器将导致操作系统忙于管理这些线程,而无法运行程序

在单核cpu上并行执行计算任务是没有意义的。

在多核cpu上可以使用多线程有效的利用多个cpu的计算能力。这需要组织多个线程间的通信和相互同步。

理解线程?

1、举个例子:

我们打游戏时,经常会按Tab键,显示计分板来查看双方的战绩,对方出了什么装备,进行针对性的出装。这个计分板就是一个线程,在不断地更新各自的信息。这个计分板并不影响我们打游戏。

2、再举个例子,当我打开Word输入文字的时候,左下角会显示我们输入了多少字。其中计算输入了多少文字就是一个线程,在后台运行。我们可以不用管它,继续编辑文档

理解前台线程和后台线程?

例子:

在游戏中,我们通过鼠标右击来使英雄移动。我是ad,想去拿个红Buff,我右击小地图红Buff所在的位置,英雄便自动地行走,这时,我可以查看计分板,这个计分板就是个后台线程 ,它不影响我的英雄移动,不影响我现在的操作

如果,计分板一直打开,等待关闭后,英雄才开始往红Buff所在的位置移动,那这个计分板是个前台线程,它阻塞了其他线程的运行

2、再举个例子:比如,网页中有某个按钮(这个按钮的功能是点击后,会在页面上显示额外的信息),如果我们点击了这个按钮,要等半天 ,并且不能进行其他操作 ,比如点击其他的按钮,这个就是前台线程;如果我们点击这个按钮,虽然要等半天,但我们仍可以进行其他操作,看看这个页面的其他部分,这个就是后台线程。

总结:前台线程不能同时触发多个操作,其他事件;

后台线程仍可以进行其他操作

前台线程和后台线程。这两者的区别就是 :应用程序必须运行完所有的前台线程才可以退出 ;而对于后台线程,应用程序则可以 不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。

为什么有些设计不够完美的WinForm程序,在某种特定的情况下,即使所有的窗口都关闭了,但是在任务管理器的管理列表里仍然可以找到该程序的进程,仍然在消耗着CPU和内存资源.因此,在WinForm程序中,关闭所有窗口前,应该停止所有前台线程,千万不要遗忘了某个前台线程.应用程序进程的存亡由前台线程决定而于后台线程无关.这就是它们的区别.

前台线程死了,后台线程也就结束了

.NET环境使用Thread建立的线程默认情况下是前台线程即线程属性IsBackground=false ,在进程中,只要有一个前台线程未退出,进程就不会终止。主线程就是一个前台线程。而后台线程不管线程是否结束,只要所有的前台线程都退出(包括正常退出和异常退出)后,进程就会自动终止。一般后台线程用于处理时间较短的任务,如在一个Web服务器中可以利用后台线程来处理客户端发过来的请求信息。而前台线程一般用于处理需要长时间等待的任务,如在Web服务器中的监听客户端请求的程序,或是定时对某些系统资源进行扫描的程序。下面的代码演示了前台和后台线程的区别。

csharp 复制代码
public static void myStaticThreadMethod()
{
    Thread.Sleep(3000);
}
Thread thread = new Thread(myStaticThreadMethod);
// thread.IsBackground = true;  是否置为后台线程
thread.Start()

如果运行上面的代码,程序会等待3秒后退出,如果将注释去掉,将thread设成后台线程,则程序会立即退出。要注意的是,必须在调用Start方法之前设置线程的类型,否则一但线程运行,将无法改变其类型

通过BeginXXX方法运行的线程都是后台线程,启动了多个线程的程序在关闭的时候却出现了问题,如果程序退出的时候不关闭线程,那么线程就会一直的存在,但是大多启动的线程都是局部变量,不能一一的关闭,如果调用Thread.CurrentThread.Abort()方法关闭主线程的话,就会出现ThreadAbortException 异常,因此这样不行。后来找到了这个办法: Thread.IsBackground设置线程为后台线程。


构造函数:

public Thread (System.Threading.ThreadStart start); 无参数
public Thread (System.Threading.ParameterizedThreadStart start); 有参数

属性:

IsBackground 获取或设置线程是否为后台线程
Priority 获取或设置优先级
ManagedThreadId 获取当前线程的唯一标识符

方法:

Abort() 终止线程
Join() 让线程依次运行(这个方法经常用到)
csharp 复制代码
//无参数的线程
Thread thread=new Thread(new ThreadStart(方法名));//实例化线程
thread.Start();//启动线程

//有参数的线程
Thread threadParam = new Thread(new ParameterizedThreadStart(方法名));//有参数
//这里有个非常重要的知识
方法里面的形参必须是object类型的
threadParam.Start(DateTime.Now);//有参数的线程启动方法
创建一个新线程.Start
csharp 复制代码
        /// <summary>
        /// 线程启动函数
        /// </summary>
        static void PrintNums()
        {
            Console.WriteLine("starting ...........");
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine(i);
            }
            Console.WriteLine("end......");
        }
        /// 创建一个线程
        public static void Main()
        {
            Thread thread = new Thread(PrintNums);
            thread.Start();
            PrintNums();//方法名,主线程调用
        }
创建前台线程和后台线程
csharp 复制代码
static void Main(string[] args)
{
    //当前主线程是个前台线程,且不能修改为后台线程
    Console.WriteLine(Thread.CurrentThread.IsBackground);
    //Thread创建的线程是前台线程
    Thread th = new Thread(delegate() { Console.WriteLine("start a new thread"); });
    Console.WriteLine(th.IsBackground);
    //Task使用程序池创建线程,默认为后台线程
    Task task = new Task(() => Console.WriteLine("start a new task"));
    Console.Read();
}
修改前台线程为后台线程
csharp 复制代码
static void Main(string[] args)
{
    //修改前台线程为后台线程
    Thread th = new Thread(delegate() { Console.WriteLine("start a new thread"); });
    Console.WriteLine(th.IsBackground);
    th.Start();
    th.IsBackground = true;
    Console.WriteLine(th.IsBackground);

    Console.Read();
}

在C#中,前台线程可以修改为后台线程,这是由HostProtectionAttribute属性的SelfAffectingThreading字段决定的,如果可以变成后台线程,则值为true.

前台线程阻止进程的关闭
csharp 复制代码
static void Main(string[] args)
 {
     //前台线程阻止了主线程的关闭
     Thread th = new Thread(delegate()
     {
         Thread.Sleep(6000);
         Console.WriteLine("start a new thread");

     });
     th.Start();

     Console.WriteLine("main thread end");
 }

输出结果:这里主线程马上执行完成,并不马上关闭,前台线程等待6秒再执行输出

后台线程不阻止进程的关闭
csharp 复制代码
static void Main(string[] args)
{
    //后台线程不阻止主线程的关闭
    Thread th = new Thread(delegate()
    {
        Thread.Sleep(6000);
        Console.WriteLine("start a new thread");

    });
    th.IsBackground = true;
    th.Start();

    Console.WriteLine("main thread end");
}

结果:不等线程执行完成,主线程执行完毕后自动退出。

暂停当前线程.Sleep

通过在线程函数中调用Thread.Sleep()暂停当前线程,使线程进入休眠状态。此时线程会占用尽可能少的CPU时间。

Thread.Sleep 函数来使线程挂起一段时间.

Thread.Sleep(0) 表示挂起0毫秒,你可能觉得没作用,你要写Thread.Sleep(1000) 就有感觉了。似乎毫无意义。

MSDN的说明:指定零 (0) 以指示应挂起此线程以使其他等待线程能够执行。

Thread.Sleep(0) 并非是真的要线程挂起0毫秒,意义在于这次调用Thread.Sleep(0)的当前线程确实的被冻结了一下,让其他线程有机会优先执行 。Thread.Sleep(0) 是你的线程暂时放弃cpu ,也就是释放一些未用的时间片给其他线程或进程使用,就相当于一个让位动作

csharp 复制代码
//暂停线程
static void PrintNumsWithDelay()
{
    Console.WriteLine("starting ...........");
    //Log("当前线程状态 :"  + Thread.CurrentThread.ThreadState.ToString());
    for (int i = 0; i < 10; i++)
    {
        Thread.Sleep(TimeSpan.FromSeconds(1));  //每次暂停一秒
        Console.WriteLine(i);
    }
    Console.WriteLine("end......");
}

//创建一个线程并暂停
public static void Test1()
{
    Thread t = new Thread(PrintNumsWithDelay);
    t.Start();
    PrintNums();//主线程调用
}
是否为后台线程.IsBackground

C#中,Thread类有一个IsBackground 的属性.MSDN上对它的解释是:获取或设置一个值,该值指示某个线程是否为后台线程。个人感觉这样的解释等于没有解释.

.Net中的线程,可以分为后台线程和前台线程。后台线程与前台线程并没有本质的区别,它们之间唯一的区别就是:后台线程不会防止应用程序的进程被终止掉。

其实,说白了就是当前台线程都结束了的时候,整个程序也就结束了,即使还有后台线程正在运行,此时,所有剩余的后台线程都会被停止且不会完成.但是,只要还有一个前台线程没有结束,那么它将阻止程序结束.这就是为什么有些设计不够完美的WinForm程序,在某种特定的情况下,即使所有的窗口都关闭了,但是在任务管理器的管理列表里仍然可以找到该程序的进程,仍然在消耗着CPU和内存资源.因此,在WinForm程序中,关闭所有窗口前,应该停止所有前台线程,千万不要遗忘了某个前台线程.应用程序进程的存亡由前台线程决定而于后台线程无关.这就是它们的区别.

知道了前后台线程的区别,就应该知道如何声明IsBackgroud属性的值了.

值得说明的一点是:改变线程从前台到后台不会以任何方式改变它在CPU协调程序中的优先级和状态。因为前台后线程与程序进程的优先级无关.

结束前摘录MSDN上一段示例码,以帮助大家便好的理解这一区别:

下面的代码示例对比了前台线程与后台线程的行为。创建一个前台线程和一个后台线程。前台线程使进程保持运行,直到它完成它的 while 循环。前台线程完成后,进程在后台线程完成它的 while 循环之前终止。

终止线程.Abort

通过调用Thread.Abort()方法强制终止线程。这会给线程注入ThreadAbortExeception方法,导致线程被终结。这是一个非常危险的操作, 任何时刻发生并可能彻底摧毁应用程序。另外,使用该技术也不一定总能终止线程。目标线程可以通过处理该异常并调用Thread.ResetAbort方法来拒绝被终止。因此并不推荐使用,Abort方法来关闭线程 。

csharp 复制代码
/// <summary>
/// 终止线程  非常危险,不推荐使用,也不一定能够终止线程
/// </summary>
public static void Main()
{
    Thread t = new Thread(PrintNumsWithDelay);
    t.Start();
    //5s之后终止线程t
    Thread.Sleep(5000);
    t.Abort();
    Console.WriteLine("Thread t has been Abort");
}
获取线程状态.CurrentThread

线程状态位于Thread对象的ThreadState属性中。ThreadState属性是一个C#枚举对象。刚开始线程状态为ThreadState.Unstarted,然后我们启动线程,线程状态会从ThreadState.Running变为ThreadState. WaitSleepJoin。 其中:请注意始终可以通过Thread.CurrentThread静态属性获得当前Thread对象。

csharp 复制代码
/// <summary>
/// 线程状态
/// </summary>
public static void Test5()
{
    Thread t1 = new Thread(PrintNumsWithDelay);
    Log("t1线程状态 :"  + t1.ThreadState.ToString());
    t1.Start();
    Log("t1线程状态 :"  + t1.ThreadState.ToString());
    t1.Join(); //等待线程t1执行完成,程序会在这里阻塞
    Log("t1线程状态 :"  + t1.ThreadState.ToString());
    Console.WriteLine("Thread t1 finished");
    Log("t1线程状态 :"  + t1.ThreadState.ToString());
}
线程优先级.Priority

通过设置Thread.Priority属性给线程对象设置优先级 ThreadPriority.Highest (最高优先级)、 ThreadPriority.Lowest(最低优先级)。通常优先级更高的线程将获取到更多cpu时间。

csharp 复制代码
/// <summary>
/// 线程优先级
/// </summary>
class ThreadSample
{
    private bool isStop = false;

    public void Stop()
    {
        isStop = true;
    }

    public void CountNums()
    {
        long counter = 0;
        while (!isStop)
        {
            counter++;
        }
        Console.WriteLine("{0} with {1,11} priority has a count = {2,13}"
            ,Thread.CurrentThread.Name,Thread.CurrentThread.Priority,
            counter.ToString());
    }
}
static void RunThreads()
{
    //启动两个线程t1 t2
    var sample = new ThreadSample();
    Thread t1 = new Thread(sample.CountNums);
    t1.Name = "Thread1";
    Thread t2 = new Thread(sample.CountNums);
    t2.Name = "Thread2";
    //设置线程的优先级
    t1.Priority = ThreadPriority.Highest;  //t1为最高优先级
    t2.Priority = ThreadPriority.Lowest;  //t2为最低优先级
    //启动
    t1.Start();
    t2.Start();

    //主线程阻塞2s
    Thread.Sleep(TimeSpan.FromSeconds(2));
    sample.Stop();  //停止计数
    //等待按键按下
    Console.ReadKey();
}
/// <summary>
/// 线程优先级,决定该线程可以占用多少cpu时间
/// </summary>
public static void Test6()
{
    Log("当前线程的优先级 priority = " + Thread.CurrentThread.Priority.ToString());
    Log("在所有核上运行");

    RunThreads();

    Thread.Sleep(TimeSpan.FromSeconds(2));
    Log("在单个核上运行");
    //在该进程下的线程只能在一个核上运行
    Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1);
    //再次执行
    RunThreads();

    /*
     结果:多核时高优先级的线程通常会比低优先级的线程多执行次数 但是大体接近
        单核的时候竞争就更激烈了,用有高优先级的线程会占用更多的cpu时间,而留给低优先级的线程的
        cpu时间就更少了。
     * 在所有核上运行
        Thread1 with     Highest priority has a count =     583771892
        Thread2 with      Lowest priority has a count =     444097830
        在单个核上运行
        Thread2 with      Lowest priority has a count =      32457242
        Thread1 with     Highest priority has a count =    6534967709
     *
     */
}

总结

一.主线程、前台线程与后台线程

相信前几年,大家都用过迅雷,用来下载文件是非常方便的,更重要的是速度快。那么,它的速度非常之快,全速下载的时候明显地拉慢了整个系统的响应时间,说明他占用了大量的系统资源。那它为什么这么快?

知乎上的yskin用户解释说

『一个下载任务进来,迅雷把文件平分成10份,然后开10个线程分别下载。这时主界面是一个单独的线程 ,并不会因为下载文件而卡死。而且主线程可以控制下属线程,比如某个线程下载缓慢甚至停止,主线程可以把它强行关掉并重启另外一个线程。 』这么多线程同时工作,正常情况下,下载速度会有质的提升。

那么,问题来了,当我暂停这个下载任务时,后面10个线程会关掉,但界面线程关闭掉这10个线程的时候,我们并没有察觉 ,它们在后台悄默声地就关掉了。这10个线程就是我们说的后台线程

现在,我们把迅雷软件退出了,那么所有的下载任务都关掉了,下载任务背后的线程自然也会关掉,这个迅雷软件运行的线程 就是我们说的主线程 ,它是一个前台线程

但当我们再在网页中重新找到可下载的内容时,迅雷的资源嗅探又可以检测到这些内容,交提示我们是否要下载。它不随着前面迅雷软件的退出而退出。

如果你觉得这个例子不好理解,《C#高级编程》中也有一个,当你使用word来编辑文档时,它会实时提供一些拼写检查,当你需要打印文档时,可以选择后台打印,在打印机打印文档的同时,你可以继续编辑当前文档。同时word文档关闭时,这个打印任务可继续执行,直到打印出来,但拼写检查任务不会再执行。

这就是主线程、前台线程与后台线程的一些类比。下面,我们来说结论。

当一个程序启动时 ,就有一个进程被操作系统(OS)创建 ,与此同时一个线程也立刻运行 ,该线程通常叫做程序的主线程 (Main Thread),因为它是程序开始时就执行的,如果你需要再创建线程,那么创建的线程就是这个主线程的子线程,它是前台线程

新建的子线程可以是前台线程或者后台线程,前台线程必须全部执行完,即使主线程关闭掉,这时进程仍然存活。后台线程在未执行完成时,如果前台线程关掉,则后台线程也会停掉,且不抛出异常。也就是说,**前台线程与后台线程唯一的区别是后台线程不会阻止进程终止。**可以在任何时候将前台线程修改为后台线程。


[2] 什么是Task

多用于异步操作

允许多个任务可以有多个线程(或在同一线程内)进行不必等上一个 任务执行完才开始 执行下一个任务

Task简单地看就是任务,那和thread有什么区别呢?Task的背后的实现也是使用了线程池ThreadPool线程 ,但它的性能优于ThreadPool,因为它使用的不是线程池的全局队列,而是使用的本地队列,使线程之间的资源竞争减少。同时Task提供了丰富的API来管理线程、控制。但是相对前面的两种耗内存,Task依赖于CPU对于多核的CPU性能远超前两者,单核的CPU三者的性能没什么差别。Task是要依赖Thread来执行的。

Task其实就是在ThreadPool的基础上进行一层封装,ThreadPool启动的线程不好判断线程的执行情况,但Task可以,很好地解决了这个问题。

Thread与Task的区别

Thread 类主要用于实现线程的创建以及执行。

Task 类表示以异步方式执行的单个操作。

1、Task 是基于 Thread 的,是比较高层级的封装,Task 最终还是需要 Thread 来执行

2、Task 默认使用后台线程执行,Thread 默认使用前台线程


csharp 复制代码
static void Main(string[] args)
{
   new Thread(Test) { IsBackground = false }.Start(); //.Net 在1.0的时候,就已经提供最基本的API.
   ThreadPool.QueueUserWorkItem(o => Test());  //线程池中取空闲线程执行委托(方法)
   Task.Run((Action)Test); //.Net 4.0以上可用
   Console.WriteLine("Main Thread");
   Console.ReadLine();
}

static void Test()
{
    Thread.Sleep(1000);
    Console.WriteLine("Hello World");
}

本质关系

其实不管是Task,ThreadPool,本质最终都是Thread。只不过微软帮我们在简化线程控制的复杂度。

线程池是CLR中事先定义好的一些线程。Task取的线程池,只不过在语法上,可以非常方便取返回值。

复制代码
构造函数:
public Task (Action action);	无参数无返回值
public Task(Action action, object state);	有参数无返回值
public Task(Func<object, TResult> function, object state);	有参数有返回值
属性:
CurrentId	正在执行的Task的id
IsCompleted	是否完成
IsCompleted	是否出现异常
方法:
Start()	启动Task
Wait()	等待Task执行完成

创建Task有两种方法

csharp 复制代码
//法一
Task t = Task.Factory.StartNew(() => {
         Console.WriteLine("任务已启动....");});
使用factory创建会直接执行,使用new创建不会执行,必须等到start启动之后才执行

//法二
Task t2 = new Task(() => {
                Console.WriteLine("开启一个新任务");
            });
            t2.Start();//任务已启动...
第一种方法不需要调用start 初始化后任务就开始了

csharp 复制代码
Task task_NoParam = new Task(无参数无返回值的方法);
task_NoParam.Start();

Task task_WithParam = new Task(有参数无返回值的方法, 传给方法的参数);
task_WithParam.Start();

Task<string> task_WithParam_WithReturn = new Task<string>(有参数有返回值的方法, 传给方法的参数);
task_WithParam_WithReturn.Start();
string Result=task_WithParam_WithReturn.Result;//返回的结果

csharp 复制代码
class CommonClass
    {
        public void TestMethod()
        {
            Console.WriteLine("没有参数的方法");
            for (int i = 0; i < 3; i++)
            {
                Console.WriteLine("无参数的方法" + i + "");
            }
        }

        public void TestMethod(object obj)//这个形参必须是object类型的---这很重要
        {
            Console.WriteLine("有参数的方法,参数为" + obj.ToString() + "");
            for (int i = 0; i < 3; i++)
            {
                Console.WriteLine("有参数的方法" + i + "");
            }
        }
        public void TestMethod_ThreadPool(object obj)
        {
            if (obj != null)
            {
                Console.WriteLine("ThreadPool-有参数的方法,参数为" + obj.ToString() + "");
                for (int i = 0; i < 3; i++)
                {
                    Console.WriteLine("ThreadPool-有参数的方法" + i + "");
                }
            }
            else
            {
                Console.WriteLine("ThreadPool-没有参数的方法");
                for (int i = 0; i < 3; i++)
                {
                    Console.WriteLine("ThreadPool-无参数的方法" + i + "");
                }
            }

        }
        public void TestMethod_Task_NoParam()
        {
            Console.WriteLine("Task-无参数");
        }
        public void TestMethod_Task_WithParam(object obj)
        {
            Console.WriteLine($"Task-有参数,参数为:{obj.ToString()}");
        }
        public string TestMethod_Task_WithParam_WithReturn(object obj)
        {
            Console.WriteLine($"Task-有参数,参数为:{obj.ToString()}");
            return obj.ToString();
        }
    }

csharp 复制代码
class Program
{
   static void Main()
   {
       for (int i = 0; i < 3; i++)
       {
           Console.WriteLine("主线程" + i + "");
       }
       CommonClass commonClass = new CommonClass();
            //Thread的使用
            Thread thread = new Thread(new ThreadStart(commonClass.TestMethod));//没有参数
            //thread.IsBackground = false;//设置前台线程还是后台线程,在线程启动前设置
            thread.Start();//没有参数的线程启动方法

            Thread threadParam = new Thread(new ParameterizedThreadStart(commonClass.TestMethod));//有参数
            threadParam.Start(DateTime.Now);//有参数的线程启动方法

            //等待上面两个线程执行完后
            thread.Join();
            threadParam.Join();

            Console.WriteLine("\n下面是ThreadPool的使用");
            //ThreadPool的使用
            ThreadPool.QueueUserWorkItem(commonClass.TestMethod_ThreadPool);
            ThreadPool.QueueUserWorkItem(commonClass.TestMethod_ThreadPool, DateTime.Now);
            //注意:使用ThreadPool不好判断线程什么时候完成

            Thread.Sleep(1000);
            Console.WriteLine("\n下面是Task的使用");
            //Task的使用
            Task task_NoParam = new Task(commonClass.TestMethod_Task_NoParam);//无参数无返回值的方法
            task_NoParam.Start();
            Task.WaitAll(task_NoParam);//等task_NoParam这个Task执行完执行下面的
            //这就是使用Task的好处,便于控制,知道Task什么时候执行完
            //不像TreadPool,让他启动后台线程,然后就没有然后了。任务完成后自动销毁。
            Task task_WithParam = new Task(commonClass.TestMethod_Task_WithParam, "sdf");//有参数无返回值的方法
            task_WithParam.Start();
            Task<string> task_WithParam_WithReturn = new Task<string>(commonClass.TestMethod_Task_WithParam_WithReturn, "sfdgsdfgasdf");//有参数有返回值
            task_WithParam_WithReturn.Start();
            Console.WriteLine("有参数有返回值的Task执行结果:" + task_WithParam_WithReturn.Result + "");

            Console.ReadKey();
        }
    }

Task方法

Task.ConfigureAwait()

ConfigureAwait 是 C# 中 async/await 机制一个非常重要的概念,尤其是在涉及到 UI 应用程序或某些特定服务器端框架(如 ASP.NET Classic)时。它决定了 await 关键字后面的代码(称为"延续")在异步操作完成后,是否要尝试回到原始的执行上下文。

1. 什么是"上下文" (SynchronizationContext)?

在深入 ConfigureAwait 之前,我们需要理解"同步上下文" (SynchronizationContext)。

同步上下文 :你可以把它理解为一个特定的线程或线程调度器,它能够保证后续操作在特定线程上执行。 不同的应用程序模型有不同的同步上下文:

  • UI 应用程序 (WinForms, WPF):有一个 UI 线程。SynchronizationContext 会将延续代码调度回 UI 线程,以确保你可以安全地更新 UI 元素(因为 UI 元素通常不是线程安全的)。
  • ASP.NET (旧版,如 .NET Framework 的 ASP.NET):有一个 AspNetSynchronizationContext。它会将延续代码调度回处理 HTTP 请求的原始线程,这对于 HttpContext 等请求特定数据的访问非常重要。
  • ASP.NET Core / Console Apps / Libraries:默认情况下,这些应用程序没有特定的 SynchronizationContext。或者说,它们的 SynchronizationContext 是一个"自由线程"的上下文,它会将延续代码调度到线程池中的任何可用线程上。

2. await 的默认行为

当你在一个 async 方法中使用 await someTask; 时,await 默认会执行以下步骤:

  1. 检查当前同步上下文:它会捕获 await 发生时的当前 SynchronizationContext (如果存在)。
  2. 启动异步操作:someTask 开始执行。
  3. 如果 someTask 未完成:控制权返回给调用者,当前方法暂停。

someTask 完成后:

  1. await 会尝试将 someTask 后面的延续代码调度回之前捕获到的同步上下文。
  2. 如果捕获到了 UI 线程的上下文,延续就会在 UI 线程上执行。
  3. 如果捕获到了 ASP.NET 请求的上下文,延续就会在那个请求线程上执行。
  4. 如果没有捕获到特定的上下文(例如在控制台应用或 ASP.NET Core 中),延续就会在线程池中的任意线程上执行。

3. Task.ConfigureAwait(bool continueOnCapturedContext)

ConfigureAwait 是 Task 上的一个方法,它接受一个布尔参数:

ConfigureAwait(true) (默认行为):

  • 这是 await 的默认行为。
  • 它会捕获当前的 SynchronizationContext(如果存在),并在异步操作完成后,尝试将延续代码调度回这个捕获到的上下文。
  • 优点:在需要访问上下文特定状态(如 UI 元素)的情况下,保证了线程安全和正确性。
  • 缺点/风险
    • 潜在的死锁:这是最主要的问题。如果你的代码在 UI 线程上调用了一个同步阻塞的方法 (.Wait() 或 .Result),而这个同步阻塞方法内部又 await 了一个没有 ConfigureAwait(false) 的任务,并且这个任务的延续需要回到 UI 线程,那么就会发生死锁。因为 UI 线程被阻塞了,无法处理延续,而延续又在等待 UI 线程。
    • 性能开销:在不需要上下文的场景下,捕获上下文和将延续调度回特定线程会带来额外的开销(虽然通常很小)。

ConfigureAwait(false) (不捕获上下文):

  • 它告诉 await不要捕获当前的 SynchronizationContext。
  • 当异步操作完成后,延续代码将会在线程池中的任意线程上恢复执行(而不是尝试回到原始线程)。
  • 优点
    • 避免死锁:在库代码和非 UI 代码中,这是避免死锁的有效方法。
    • 提高性能:消除了上下文捕获和调度的开销,可以在任何可用的线程上继续执行,提高吞吐量。
    • 更好的可伸缩性:避免将线程绑定到特定的上下文,允许线程池更高效地利用。
  • 缺点
    • 不能访问上下文特定状态:如果你在 ConfigureAwait(false) 之后尝试访问 UI 元素(如 TextBox.Text)或 HttpContext,将会导致运行时错误,因为你不再在正确的线程上。
    • 需要注意线程安全:由于延续可能在任何线程上运行,你需要确保后续代码是线程安全的。

4. 什么时候使用 ConfigureAwait(true) 和 ConfigureAwait(false)?

经验法则 (General Rule of Thumb):

在 UI 应用程序 (WinForms, WPF) 和旧版 ASP.NET 应用程序中,如果你需要访问 UI 元素或 HttpContext 等上下文相关的数据,并且这些代码位于用户界面层(UI layer)或框架的入口点(例如按钮点击事件处理程序),则通常省略 ConfigureAwait 或使用 ConfigureAwait(true) (它们是等价的)。 这样可以确保你的代码在正确地线程上执行,以更新 UI 或访问请求数据。

csharp 复制代码
// WinForms/WPF 按钮点击事件
private async void Button_Click(object sender, EventArgs e)
{
    // 这里的代码在 UI 线程上
    Debug.WriteLine($"Before await - Thread: {Thread.CurrentThread.ManagedThreadId}");
    string data = await GetDataAsync(); // 默认 await GetDataAsync().ConfigureAwait(true);
    // 延续回到 UI 线程,可以安全更新UI
    Debug.WriteLine($"After await - Thread: {Thread.CurrentThread.ManagedThreadId}");
    textBox1.Text = data; // 安全地更新UI
}

// 内部实现可以 ConfigureAwait(false)
private async Task<string> GetDataAsync()
{
    await Task.Delay(1000).ConfigureAwait(false); // 避免死锁,提高性能
    return "Loaded Data";
}

在编写库代码、ASP.NET Core 应用程序、控制台应用程序或其他不依赖特定 SynchronizationContext 的通用代码时,应尽可能使用 ConfigureAwait(false)。 这样做可以提高性能,避免死锁,并使你的库更通用和可复用。

csharp 复制代码
// 库方法
public class DataService
{
    public async Task<List<string>> FetchItemsAsync()
    {
        // 在这里,我们不关心延续在哪个线程上运行,
        // 只要能高效地完成任务即可。
        // 使用 ConfigureAwait(false) 避免不必要的上下文捕获和调度,
        // 从而提高性能并避免潜在的死锁(如果库方法被UI线程同步阻塞调用)。
        var result = await _httpClient.GetStringAsync("https://api.example.com/items").ConfigureAwait(false);
        // ... 处理结果 ...
        return JsonConvert.DeserializeObject<List<string>>(result);
    }
}

// ASP.NET Core Controller
public class ProductsController : ControllerBase
{
    private readonly DataService _dataService;

    public ProductsController(DataService dataService)
    {
        _dataService = dataService;
    }

    [HttpGet]
    public async Task<ActionResult<List<string>>> GetProducts()
    {
        // 在 ASP.NET Core 中,通常没有 SynchronizationContext,
        // 所以 ConfigureAwait(false) 的影响不如在 UI 应用中那么显著,
        // 但仍然是最佳实践,以防万一有自定义上下文或为了代码风格一致性。
        var products = await _dataService.FetchItemsAsync().ConfigureAwait(false);
        return products;
    }
}

5. 死锁的典型场景 (UI 应用程序)

这是一个经典的死锁例子,通常发生在 UI 线程上:

csharp 复制代码
// 假设这是在 WinForms 或 WPF 按钮点击事件中
private void DeadlockButton_Click(object sender, EventArgs e)
{
    // 这行代码在 UI 线程上运行
    Task<string> dataTask = GetDataFromNetworkAsync(); // 启动异步操作
    string result = dataTask.Result; // !!! 危险 !!! 这里会阻塞 UI 线程,直到 dataTask 完成
    // ... UI 线程被阻塞,无法处理消息 ...
    textBox1.Text = result;
}

private async Task<string> GetDataFromNetworkAsync()
{
    // 默认行为是 await Task.Delay(2000).ConfigureAwait(true);
    await Task.Delay(2000); // 模拟网络延迟
    // 这里的延续(返回结果给调用者)需要回到原始线程(UI 线程)
    return "Network Data";
}

// 发生什么了?
// 1. DeadlockButton_Click 在 UI 线程上调用 GetDataFromNetworkAsync。
// 2. GetDataFromNetworkAsync 遇到 await Task.Delay(2000);
// 3. await 捕获 UI 线程上下文,然后释放 UI 线程,让 GetDataFromNetworkAsync 暂停。
// 4. DeadlockButton_Click 继续执行到 dataTask.Result;
// 5. dataTask.Result 会阻塞 UI 线程,等待 GetDataFromNetworkAsync 完成。
// 6. 2秒后,Task.Delay 完成,GetDataFromNetworkAsync 的延续需要回到 UI 线程。
// 7. 但是 UI 线程被 dataTask.Result 阻塞了,无法处理这个延续。
// 8. 于是,死锁发生:UI 线程在等待延续,延续在等待 UI 线程。

如何避免死锁?

GetDataFromNetworkAsync 中使用 ConfigureAwait(false)

csharp 复制代码
private async Task<string> GetDataFromNetworkAsync()
{
    await Task.Delay(2000).ConfigureAwait(false); // <--- 关键在这里
    // 这里的延续不再需要回到原始线程(UI 线程),它可以在线程池上完成。
    return "Network Data";
}
// 此时,GetDataFromNetworkAsync 内部的 await 不会阻塞 UI 线程,
// DeadlockButton_Click 仍然会阻塞 UI 线程,但 GetDataFromNetworkAsync 不会反向依赖 UI 线程,
// 因此不会发生死锁。然而,dataTask.Result 仍然会阻塞 UI 线程,所以仍然应该避免。
// 最好的做法是:
private async void SafeButton_Click(object sender, EventArgs e)
{
    // 这里的代码在 UI 线程上
    string result = await GetDataFromNetworkAsync(); // 使用 await 而不是 .Result
    // 延续回到 UI 线程,可以安全更新UI
    textBox1.Text = result; // 安全地更新UI
}

上下文捕获意味着在 await 之后,代码将恢复在原来的同步上下文上继续执行,这在某些情况下可能并不必要,尤其是在后台任务中。这个行为会导致不必要的上下文切换,增加执行开销,甚至可能导致死锁。

csharp 复制代码
public async Task LoadDataAsync()
{
    // 异步操作,可能需要较长时间
    var data = await GetDataAsync();

    // await之后,代码在UI线程上继续执行
    UpdateUI(data);
}

在上述代码中,await GetDataAsync() 捕获了UI线程的上下文,异步操作完成后,代码返回到UI线程执行 UpdateUI。在这种情况下,这种捕获是必要的,因为UI的更新需要在主线程进行。

然而,假如我们的操作不涉及UI更新,或者代码运行在后台线程上,这样的上下文捕获实际上是不必要的。这时候可以通过 ConfigureAwait(false) 来避免上下文捕获。

通过在 await 语句后面调用 ConfigureAwait(false),可以告诉编译器在恢复执行时,不需要返回到原来的同步上下文。这样做不仅可以减少不必要的上下文切换,还可以提高性能

csharp 复制代码
public async Task LoadDataAsync()
{
    // 不需要返回UI线程的异步操作
    var data = await GetDataAsync().ConfigureAwait(false);

    // 因为ConfigureAwait(false),下面的代码将不在UI线程上运行
    ProcessData(data);
}
await GetDataAsync() 后的代码不会返回UI线程执行,而是继续在后台线程上运行。由于 ProcessData 并不依赖UI线程,因此这种优化是合理的。

ConfigureAwait(false)的适用场景

  • **后台任务:**对于不需要访问UI或同步上下文的任务,ConfigureAwait(false) 可以提高性能。
  • **ASP.NET Core:**在ASP.NET Core中,由于默认情况下不使用同步上下文,ConfigureAwait(false)的性能收益较小,但仍是一个良好的实践,确保代码不会无意中依赖特定的上下文。
  • **避免死锁:**在某些情况下,特别是Windows Forms应用中,await 会尝试在UI线程恢复执行,如果UI线程被阻塞,会导致死锁。使用 ConfigureAwait(false) 可以避免此类问题。

虽然 ConfigureAwait(false) 可以优化性能,但并非所有场景都适合使用。特别是涉及到UI更新时,必须确保代码在正确的上下文中执行。如果 ConfigureAwait(false) 使得代码在错误的线程上执行,将会导致崩溃或异常。

以下是一个UI线程场景中的例子:

csharp 复制代码
public async Task LoadAndDisplayDataAsync()
{
    // 获取数据时不需要UI线程
    var data = await GetDataAsync().ConfigureAwait(false);

    // 回到UI线程更新UI
    await Dispatcher.InvokeAsync(() => UpdateUI(data));
}

在这个例子中,我们在 await GetDataAsync() 时使用了 ConfigureAwait(false) 来避免上下文切换,然后在数据获取完成后,通过 Dispatcher.InvokeAsync() 确保 UpdateUI 在UI线程上执行。


总结

  • ConfigureAwait(true) (默认) 是"回到原点":它会尝试在异步操作完成后,将后续代码调度回 await 发生时的原始线程/上下文。
  • ConfigureAwait(false) 是"自由运行":它允许后续代码在线程池中的任意线程上继续执行,不关心原始上下文。
  • 对于编写库或不涉及 UI/特定上下文的后端代码,请使用 ConfigureAwait(false) 来避免死锁和提高性能。
  • 对于需要在 UI 线程上更新 UI 元素的代码,或者需要在特定框架上下文(如旧版 ASP.NET HttpContext)中运行的代码,可以省略 ConfigureAwait 或使用 ConfigureAwait(true) 但同时要避免在这些方法中使用同步阻塞的 Wait()Result
  • 在现代 .NET 应用程序(如 ASP.NET Core、控制台应用)中,默认没有 SynchronizationContext,所以 ConfigureAwait(false) 的实际运行时行为可能与默认行为相同,但养成在库代码中使用的习惯仍然是好的实践。

[3]什么是ThreadPool

提供一个线程池,该线程池可用于执行任务、发送工作项、处理异步 I/O、代表其他线程等待以及处理计时器

线程池 是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程 。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。

线程池可以看做容纳线程的容器;一个应用程序最多只能有一个线程池;ThreadPool静态类通过QueueUserWorkItem()方法将工作函数排入线程池; 每排入一个工作函数,就相当于请求创建一个线程;

核心就是不需要自己每次都新建一个线程,比如某个线程执行完毕后,不会自行销毁,而是以挂起的状态返回线程池里,等待新任务

ThreadPool优点

1、使用起来简单,只需使用QueueUserWorkItem来调用方法即可

2、减少了线程的开销(不需要自己手动新建与销毁线程),如果通过Thread新建一个线程的话,开销就差不多要消耗1M左右的内存

ThreadPool缺点

1、它其中的一个优点,不需要自己手动新建与销毁线程,有时候也成了缺点,不好控制各个线程的执行,不好判断线程什么时候执行完了,那怎么办呢,这时,Task便出现了。

方法

QueueUserWorkItem(WaitCallback) 将方法排入队列以便执行

QueueUserWorkItem(WaitCallback, Object) 将方法排入队列以便执行,Object需要传递的参数

csharp 复制代码
ThreadPool.QueueUserWorkItem(方法名);//这个方法必须要有个参数object

csharp 复制代码
        // 参数:
        // workerThreads:
        // 要由线程池根据需要创建的新的最小工作程序线程数。
        // completionPortThreads:
        // 要由线程池根据需要创建的新的最小空闲异步 I/O 线程数。
        // 返回结果:如果更改成功,则为 true;否则为 false。
ublic static bool SetMinThreads(int workerThreads, int completionPortThreads);
        // 参数:
        // workerThreads:
        // 线程池中辅助线程的最大数目。
         // completionPortThreads:
        // 线程池中异步 I/O 线程的最大数目。
        // 返回结果:如果更改成功,则为 true;否则为 false。
        [SecuritySafeCritical]
public static bool SetMaxThreads(int workerThreads, int completionPortThreads);

关于AppDomain

应用程序域(AppDomain)

在.Net中,应用程序有了一个新的边界:应用程序域(以下简称域) 。它是一个用于隔离应用程序的虚拟边界。为了禁止不应交互的代码进行交互,这种隔离是必要的。.Net的应用程序在域层次上进行隔离一个域中的应用程序不能直接访问另一个域中的代码和数据。这种隔离使得在一个应用程序范围内创建的所有对象都在一个域内创建,确保在同一进程中一个域内运行的代码不会影响其他域内的应用程序,大大提高了运行的安全。

.Net结构中,由于公共语言运行库能够验证代码是否为类型安全的代码,所以它可以提供与进程边界一样大的隔离级别,其性能开销也要低得多。你可以在单个进程中运行几个域,而不会造成进程间调用或切换等方面的额外开销。这种方法是把任何一个进程分解到多个域中,允许多个应用程序在同一进程中运行,每个域大致对应一个应用程序,运行的每个线程都在一个特殊的域中。如果不同的可执行文件都运行在同一个进程空间中,它们就能轻松地共享数据或直接访问彼此的数据。这种代码同运行同一个进程但域不同的类型安全代码一起运行时是安全的。在一个进程内运行多个应用程序的能力显著增强了服务器的可伸缩性。

使用.NET建立的可执行程序 .exe,并没有直接承载到进程当中,而是承载到应用程序域(AppDomain)当中。应用程序域是.NET引入的一个新概念,它比进程所占用的资源要少,可以被看作是一个 轻量级的进程*。

在一个进程中可以包含多个应用程序域,但一个AppDomain只能属于一个进程,一个应用程序域可以装载一个可执行程序(.exe)或者多个程序集(.dll) 。这样可以使应用程序域之间实现深度隔离即使进程中的某个应用程序域出现错误,也不会影响其他应用程序域的正常运作

当一个程序集同时被多个应用程序域调用时,会出现两种情况:

第一种情况:CLR分别为不同的应用程序域加载此程序集。

第二种情况:CLR把此程序集加载到所有的应用程序域之外,并实现程序集共享,此情况比较特殊,被称作为Domain Neutral。

域与线程的关系

在.Net中,线程是公共语言运行库用来执行代码的操作系统构造。在运行时,所有托管代码均加载到一个域中,由特定的操作系统线程来运行。然而,域和线程之间并不具有一一对应关系。在任意给定时间,单个域中可以执行不止一个线程,而且特定线程也并不局限在单个域内。也就是说,线程可以跨越域边界不为每个域创建新线程当然,在指定时刻,每一线程都只能在一个域中执行。运行库会跟踪所有域中有哪些线程正在运行。通过调用.Net类库的 Thread.GetDomain() 方法,你还可以确定正在执行的线程所在的域

AppDomain与进程/线程/Assembly关系

AppDomain是CLR的运行单元,它可以加载Assembly、创建对象以及执行程序。

AppDomain是CLR实现代码隔离的基本机制

每一个AppDomain可以单独运行、停止;每个AppDomain有自己默认的异常处理;

一个AppDomain的运行失败不会影响到其他的AppDomain。

CLR在被CLR Host(windows shell or InternetExplorer or SQL Server)加载后,要创建一个默认的AppDomain,程序的入口点(Main方法)就是在这个默认的AppDomain中执行

1.AppDomain vs 进程

AppDomain被创建在进程中,一个进程内可以有多个 AppDomain。一个 AppDomain只能属于一个进程。

2.AppDomain vs 线程

其实两者本来没什么好对比的。AppDomain是个静态概念,只是限定了对象的边界;线程是个动态概念,它可以运行在不同的AppDomain。

一个AppDomain内可以创建多个线程,但是不能限定这些线程只能在本 AppDomain内执行代码。

CLR中的 System.Threading.Thread对象其实是个soft thread,它并不能被操作系统识别;操作系统能识别的是hard thread。

一个soft thread只属于一个 AppDomain,穿越 AppDomain的是hard thread。当hard thread访问到某个 AppDomain时,一个 AppDomain就会为之产生一个soft thread。

hard thread有thread local storage(TLS) ,这个存储区被CLR用来存储这个hard thread当前对应的AppDomain引用以及soft thread引用。当一个hard thread穿越到另外一个AppDomain时,TLS中的这些引用也会改变。

3.AppDomain vs Assembly

Assembly是.Net程序的基本部署单元,它可以为CLR提供用于识别类型的元数据等等。Assembly不能单独执行,它必须被加载到 AppDomain中,然后由AppDomain创建程序集中的对象。

一个Assembly可以被多个AppDomain加载,一个AppDomain可以加载多个Assembly。

每个AppDomain引用到某个类型的时候需要把相应的assembly在各自的AppDomain中初始化。因此,每个AppDomain会单独保持一

个类的静态变量。

4.AppDomain vs 对象

任何对象只能属于一个AppDomain。AppDomain用来隔离对象,不同AppDomain之间的对象必须通过Proxy(reference type)或者 Clone(value type)通信。

引用类型需要继承System.MarshalByRefObject才能被Marshal/UnMarshal(Proxy)。

值类型需要设置Serializable属性才能被Marshal/UnMarshal(Clone)。

5.AppDomain vs Assembly Code

AppDomain和程序集的源代码,汇编代码是什么关系呢?每个程序集的代码会分别装载到各个AppDomain中?

首先我们要把程序集分3类

1.mscorlib,这是每个.net程序都要引用到的程序集。

2.GAC,这个是强命名的公用程序集,可以被所有的.net程序引用。

3.Assembly not in GAC,这是普通的assembly,可以不是强命名,不放到GAC中。

启动CLR,进入entry point时可以设置LoaderOptimization属性:

LoaderOptimization(LoaderOptimization.MultiDomain

static void Main()

{...}

LoaderOptimization属性可以设置三个不同的枚举值,来设置针对前面说的三种程序集的代码存放以及访问方式。

1.SingleDomain,由于只启动一个AppDomain,那么code就被直接装载到了AppDomain中,访问静态变量更快捷。

2.MultiDomain,所有的Assembly代码是进程级别的,因此所有的AppDomain只访问一份代码。这大大减少了程序占用的内存,但

是由于程序集的静态变量仍然在各个AppDomain中,因此代码访问静态变量需要先得到AppDomain的引用再进行转换,速度会受到

影响。

3.MultiDomainHost,只有GAC代码是共享的,非GAC的Assembly依然会加载到被使用的AppDomain中,这样提高了静态变量的访问

速度,当然也增加了程序占用的内存。

不管是哪种方式,mscorlib始终是process级别的,即只有一份mscorlib代码在内存中。

[注意]

  1. 要想让一个对象能够穿过AppDomain边界,必须要继承MarshalByRefObject类,否则无法被其他AppDomain使用。

  2. 每个线程都有一个默认的AppDomain,可以通过Thread.GetDomain()来得到

AppDomain的属性与方法

在System命名空间当中就存在AppDomain类,用管理应用程序域。下面是AppDomain类的常用属性:表2.0

属性 说明
ActivationContext 获取当前应用程序域的激活上下文。
ApplicationIdentity 获得应用程序域中的应用程序标识。
BaseDirectory 获取基目录。
CurrentDomain 获取当前 Thread 的当前应用程序域。
Id 获得一个整数,该整数唯一标识进程中的应用程序域。
RelativeSearchPath 获取相对于基目录的路径,在此程序集冲突解决程序应探测专用程序集。
SetupInformation 获取此实例的应用程序域配置信息。

AppDomain类中有多个方法,可以用于创建一个新的应用程序域,或者执行应用程序域中的应用程序。表2.1

方法 说明
CreateDomain 创建新的应用程序域。
CreateInstance 创建在指定程序集中定义的指定类型的新实例。
CreateInstanceFrom 创建在指定程序集文件中定义的指定类型的新实例。
DoCallBack 在另一个应用程序域中执行代码,该应用程序域由指定的委托标识。
ExecuteAssembly 执行指定文件中包含的程序集。
ExecuteAssemblyByName 执行程序集。
GetAssemblies 获取已加载到此应用程序域的执行上下文中的程序集。
GetCurrentThreadId 获取当前线程标识符。
GetData 为指定名称获取存储在当前应用程序域中的值。
IsDefaultAppDomain 返回一个值,指示应用程序域是否是进程的默认应用程序域。
SetData 为应用程序域属性分配值。
Load 将 Assembly 加载到此应用程序域中。
Unload 卸载指定的应用程序域。

AppDomain类中有多个事件,用于管理应用程序域生命周期中的不同部分。表2.2

事件 说明
AssemblyLoad 在加载程序集时发生。
AssemblyResolve 在对程序集的解析失败时发生。
DomainUnload 在即将卸载 AppDomain 时发生。
ProcessExit 当默认应用程序域的父进程存在时发生。
ReflectionOnlyAssemblyResolve 当程序集的解析在只反射上下文中失败时发生。
ResourceResolve 当资源解析因资源不是程序集中的有效链接资源或嵌入资源而失败时发生。
TypeResolve 在对类型的解析失败时发生。
UnhandledException 当某个异常未被捕获时出现。

下面将举例详细介绍一下AppDomain的使用方式

在AppDomain中加载程序集

由表2.1中可以看到,通过CreateDomain方法可以建立一个新的应用程序域。

下面的例子将使用CreateDomain建立一个应用程序域,并使用Load方法加载程序集Model.dll。最后使用GetAssemblies方法,列举此应用程序域中的所有程序集。

csharp 复制代码
static void Main(string[] args)
{
    var appDomain = AppDomain.CreateDomain("NewAppDomain");
    appDomain.Load("Model");
    foreach (var assembly in appDomain.GetAssemblies())
    Console.WriteLine(string.Format("{0}\n----------------------------",
        assembly.FullName));
    Console.ReadKey();
}

注意:当加载程序集后,就无法把它从AppDomain中卸载,只能把整个AppDomain卸载。

当需要在AppDomain加载可执行程序时,可以使用ExecuteAssembly(执行程序集)方法。

AppDomain.ExecuteAssembly("Example.exe");

卸载AppDomain

通过Unload可以卸载AppDomain,在AppDomain卸载时将会触发DomainUnload事件。

下面的例子中,将会使用CreateDomain建立一个名为NewAppDomain的应用程序域。然后建立AssemblyLoad的事件处理方法,在程序集加载时显示程序集的信息。最后建立DomainUnload事件处理方法,在AppDomain卸载时显示卸载信息。

csharp 复制代码
static void Main(string[] args)
 {
     //新建名为NewAppDomain的应用程序域
     AppDomain newAppDomain = AppDomain.CreateDomain("NewAppDomain");
     //建立AssemblyLoad事件处理方法
     newAppDomain.AssemblyLoad +=
         (obj, e) =>
         {
             Console.WriteLine(string.Format("{0} is loading!", e.LoadedAssembly.GetName()));
        };
    //建立DomainUnload事件处理方法
    newAppDomain.DomainUnload +=
        (obj, e) =>
        {
            Console.WriteLine("NewAppDomain Unload!");
        };
    //加载程序集
    newAppDomain.Load("Model");
    //模拟操作
    for (int n = 0; n < 5; n++)
        Console.WriteLine("  Do Work.......!");
     //卸载AppDomain
    AppDomain.Unload(newAppDomain);
    Console.ReadKey();
}

在AppDomain中建立程序集中指定类的对象

使用CreateInstance方法,能建立程序集中指定类的对像。但使用此方法将返回一个ObjectHandle对象,若要将此值转化为原类型,可调用Unwrap方法。

下面例子会建立Model.dll程序集中的Model.Person对象。

csharp 复制代码
public class Program
{
     static void Main(string[] args)
     {
         var person=(Person)AppDomain.CurrentDomain
                      .CreateInstance("Model","Model.Person").Unwrap();
         person.ID = 1;
         person.Name = "Leslie";
         person.Age = 29;
         Console.WriteLine(string.Format("{0}'s age is {1}!",person.Name,person.Age));
         Console.ReadKey();
     }
}

namespace Model
{
    public class Person
    {
          public int ID{get;set;}
          public string Name{get;set;}
          public int Age{get;set;}
     }
}

[附]为什么需要协程?

我们都知道多线程,当需要同时执行多项任务的时候,就会采用多线程并发执行。拿手机支付举例子,当收到付款信息的时候,需要查询数据库来判断余额是否充足,然后再进行付款。

假设最开始我们只有可怜的10个用户,收到10条付款消息之后,我们开启启动10个线程去查询数据库,由于用户量很少,结果马上就返回了。第2天用户增加到了100人,你选择增加100个线程去查询数据库,等到第三天,你们加大了优惠力度,这时候有1000人同时在线付款,你按照之前的方法,继续采用1000个线程去查询数据库,并且隐隐觉察到有什么不对。

[附]硬件线程与软件线程

1.硬件线程

核处理器带有一个以上的物理内核--物理内核是真正的独立处理单元,多个物理内核使得多条指令能够同时并行运行。硬件线程也称为逻辑内核,一个物理内核可以使用超线程技术提供多个硬件线程。所以一个硬件线程并不代表一个物理内核;

2.软件线程

系统中每个运行的程序都是一个进程,每一个进程都会创建并运行一个或多个线程,这些线程称为软件线程。硬件线程就像是一条泳道,而软件线程就是在其中游泳的人。

并发与并行

并发与并行通过多线程执行的话,会基于CPU的资源进行执行。资源多的时候每个线程都可以分配到CPU资源,便可以实现并行;资源少的时候,每个CPU需要处理多个线程,便是并发。

对于CPU资源的调配,我们是无权参与的。

多线程是多个线程并发执行

每个线程真正意义上的同时执行,无需竞争CPU资源。

一个进程至少有一个线程,称为主线程。进程和线程是1:n的关系。

硬件线程,也称之为逻辑内核或逻辑处理器 ,windows将每一个硬件线程识别为一个可调度的逻辑处理器每一个逻辑处理器可以运行软件线程的代码,windows调度器可以决定将一个软件线程赋给一个硬件线程,通过这种方式均衡每一个硬件线程的工作负载,以达到并行优化的作用。

区别

打个比方,如果把硬件线程 看作是泳道,那么软件线程就是在泳道中游泳的人。

负载均衡,就是将软件线程的任务分发在多个硬件线程上的操作,通过负载均衡,工作负载(任务)可以公平的分配在各个硬件线程之间,然而,是否能够完美的实现负载均衡取决于应用程序的并行程度、工作负载、软件线程数、可用的硬件线程以及负载均衡策略。

[附]并发与并行

1.并发

并发上限代表事件上限

每个线程并不是同一时间执行,而是通过竞争资源后执行。

多个几乎同时到达的请求看起来像同一时间处理

线程竞争CPU资源,从而得到执行资源。同一时间,一个CPU的资源只能分配给一个线程

2.并行

并行代表同时处理的事件数

相关推荐
华仔啊2 小时前
RabbitMQ 如何保证消息不丢失和不重复消费?掌握这 4 个关键点就够了
java·后端·rabbitmq
学历真的很重要2 小时前
PyTorch 机器学习工作流程基础 - 完整教程
人工智能·pytorch·后端·python·深度学习·机器学习·面试
曹牧2 小时前
在C#中,string和String
开发语言·c#
执笔诉情殇〆3 小时前
使用AES加密方法,对Springboot+Vue项目进行前后端数据加密
vue.js·spring boot·后端
码事漫谈3 小时前
单链表与双链表专题详解
后端
Lear4 小时前
【JavaSE】NIO技术与应用:高并发网络编程的利器
后端
expect7g4 小时前
Paimon源码解读 -- Compaction-3.MergeSorter
大数据·后端·flink
码事漫谈4 小时前
C++链表环检测算法完全解析
后端