多线程技术的历史发展与简单使用

2022-08-25 00:00:00 执行 操作 线程 队列 方法

进程与线程

进程是应用的执行实例,可狭义理解为一个应用程序就是一个进程。启用一个应用程序时就是启动了一个进程。该应用运行所需的所有地址空间,代码,数据及系统资源都属于此进程。进程所使用的所有资源会在进程终止时被释放或关闭。

线程是进程内部的一个执行单元。启动进程的同时就会启动该进程的主线程。一个进程可以包含很多线程。

线程分类

线程有很多种分类

从系统回收的角度来说

可分为前台线程和后台线程

  1. 前台线程

    前台线程不会受外在原因影响,只会在自己执行完成时关闭。假设一个应用程序启动了一个前台线程写文件,随后关闭应用程序,应用程序的前台线程终止,但CLR依旧保持活动并运行,使应用程序还会继续运行,只有写文件的这个前台线程完成,终止后,整个进程才会被销毁,线程才被回收。

  2. 后台线程

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

从创建方式来说

初期使用Threadnew创建的线程都是专用线程(默认为前台线程),从推出程池ThreadPool后,基于ThreadPool的线程都称为线程池线程(默认为后台线程)。

从线程池线程的功能来说

可分为工作线程与I/O线程

  1. 工作线程:执行普通操作
  2. I/O线程:专用于异步I/O操作,如文件读写,网络请求

注意:

  1. 进程(应用程序)会等待所有的前台线程完成后再结束本工作;但是如果只剩下后台线程,则会直接结束本工作,不会等待后台线程完成后再结束本工作。
  2. 在任何时候我们都可以通过线程的IsBackground属性改变线程的前后台属性
  3. 应用程序的主线程以及使用Thread构造的线程都默认为前台线程
  4. 基于线程池ThreadPool功能创建的线程都默认为后台线程
  5. 不涉及一些专用的,长时间保持运行的功能,都建议使用后台线程。
  6. I/O 异步线程数,这个线程数限制的是执行异步委托的线程数量

异步编程

底层技术发展历程简述

Thread < ThreadPool < Task < Async/Await

性能越来越好

异步编程模式发展简述

  1. 应用Thread
  2. 应用TheadPool
  3. APM(Asynchronous Programming Model):异步编程模型
  4. EAP(Event-based Asynchornous Pattern):基于事件的异步编程模式
  5. TAP(Task-based Asynchronous Pattern):基于任务的异步编程模式
  6. 应用Async/Await

ThreadPool

解决问题:

解决频繁创建,销毁线程十分耗时的问题。

创建和销毁线程是十分消耗CPU资源的操作,也就是十分耗时的操作。频繁创建、销毁线程会影响应用程序性能。所以引入缓存来解决这个问题。创建一些线程后不销毁,而是保存在一些地方,需要使用线程时,调用这些已有线程就可以。节省了创建、销毁线程的时间。

APM(Asynchronous Programming Model)

异步编程模型

基于IAsyncResult接口实现

解决的问题:

解决ThreadPool中没有反应异步操作状态的机制,无法获取异步操作返回值的问题。

特征:

  1. 实现IAsyncResult接口
  2. Beginxxx方法,启动异步操作
  3. Endxxx方法,结束异步操作

异步方法相比于同步方法多2个参数(不过也都可以为null)

  1. AsyncCallback callback

    AsyncCallback是一个委托delegate void AsyncCallback(IAsyncResult ar),此委托绑定异步操作完成时要调用的方法

  2. Object object

    一个用户可以自定义的对象,此对象可用来向异步操作完成时为AsyncCallback委托方法传递应用程序特定的状态信息,也可通过此对象在委托中访问Endxxx方法。

注意:

  1. 其中Beginxxx方法返回IAsyncResultEndxxx方法返回与同步方法相同的返回值。

  2. Beginxxx方法启动异步操作在另一个线程执行时,若想要获取其异步操作的返回值,需调用Endxxx方法来获取。

  3. 那如果我们的异步操作不需要返回值就可以在Beginxxx方法启动异步操作后,不调用Endxxx方法来终止异步操作吗?

    答案是不行Beginxxx方法后必须调用Endxxx方法来终止。原因有2。

    Beginxxx方法启动异步操作后,会被分配一些资源,这些资料会一直保持到调用Endxxx方法才会释放。

    第二,即使我们的异步操作没有返回值,我们也需要知道我们的异步操作是否执行完毕,是否出错,出了什么错等等信息,这些信息都需要我们通过调用Endxxx方法老获取。

  4. APM中,我们想要在异步完成时执行一些操作怎么办?

    可以通过在Beginxxx方法的AsyncCallback callback参数中传递回调方法来做异步后的其他处理。

使用委托进行异步编程

C#中的委托自动为我们提供了同步调用方法Invoke与异步调用方法BeginInvokeEndInvoke

异步委托是快速构建异步调用的方式之一,它就是基于IAsyncResult实现的,通过BeginInvoke返回IAsyncResult对象,通过EndInvoke获取结果。

大的缺陷:

没有提供进度通知等功能及多线程间控件的访问

特别声明

.NET Core以后不再支持异步委托(可狭义理解为不再支持APM那种形式),只能在.NET Framework中使用。在.NET Core中使用后会报错:System.PlatformNotSupportedException:“Operation is not supported on this platform.”

原因:

Async delegates are not in .NET Core for several reasons:

*Async delegates use deprecated IAsyncResult-based async pattern. This pattern is generally not supported throughout .NET Core base libraries, e.g. System.IO.Stream does not have IAsyncResult-based overloads for Read/Write methods in .NET Core.
Async delegates depend on remoting (System.Runtime.Remoting) under the hood. Remoting is not in .NET Core - implementation of async delegates without remoting would be challenging.

异步委托不再应用于.NET Core的原因:

异步委托使用已弃用的基于IAsyncResult的异步模式(也就是APM),这种模式不再受.NET Core基础库的支持。例如,在.NET CoreSystem.IO.Stream已经没有了基于IAsyncResult的重载方法。

异步委托依赖于Remoting (System.Runtime.Remoting).NET Core中已经没有了Remoting。在没有Remoting的情况下实现异步委托是一个挑战。

个人补充:反正就是不支持了,这种旧代码能看懂就基本可以了。我们使用的话肯定是用新不用旧。另.NET的官方文档其实是有异步委托的相关示例的,这里猜测可能是服务于APM转TAP的情况吧。

EAP(Event-based Asynchronous Pattern)

基于事件的异步编程模式

关键的基础设施:

  1. 事件
  2. AsyncOperation类
  3. AsyncOperationManager类

基于事件的异步编程模式的主要功能:

  1. 异步执行耗时的操作
  2. 获取进度报告和增量结果
  3. 支持异步耗时任务的取消
  4. 可以获取异步耗时任务的结果数据或异常信息
  5. 支持同时执行多个异步操作,及获取他们的进度报告,增量结果,取消操作,返回结果或异常信息
  6. 对于简单的多线程应用,提供BackgroundWorker组件可以快速搭建简单的解决方案。

优点:

  1. 与Visual Studio UI设计器有很好的集成
  2. 通过内部的SynchronizationContext类,可以很方便的跨线程操作控件。

特征:

  1. 简单情况:一个xxxAsync方法对应一个xxxCompleted事件,以及其同步版本
  2. 复杂情况:多个xxxAsync方法对应其各自的xxxCompleted事件,及其同步版本
  3. 更复杂的情况:异步方法支持取消(CancelAsync()方法),支持进度报告(ReportProgress() 方法),支持增量结果(ProgressChanged事件)
  4. 如果不想支持多个并发调用,可考虑公开IsBusy属性
  5. 如要异步操作的同步版本中有 Out 和 Ref 参数,它们应做为对应 xxxCompletedEventArgs的一部分

BackgroundWorker组件

它是System.ComponentModel命名空间为我们提供的一个简单的多线程应用解决方案,它允许在单独的线程上运行耗时操作而不会导致用户界面阻塞。但是注意,它同一时刻只能运行一个异步耗时操作(使用IsBusy属性判定),并且不能夸AppDomain边界进行封送处理(也就是不能在多个AppDomain中执行多线程操作)

BackgroundWorker bgWorker;
private void Bt15_Click(object sender, RoutedEventArgs e) 
{
    bgWorker = new BackgroundWorker();
    // 注册异步执行事件
    bgWorker.DoWork += BgWorker_DoWork;
    // 注册完成事件
    bgWorker.RunWorkerCompleted += BgWorker_RunWorkerCompleted;
    // 使能获取进度的功能
    bgWorker.WorkerReportsProgress = true;
    // 注册获取进度事件
    bgWorker.ProgressChanged += BgWorker_ProgressChanged;

    // 启动异步执行
    bgWorker.RunWorkerAsync(5);
}

private void BgWorker_DoWork(object sender, DoWorkEventArgs e){/* Dosomething */}
private void BgWorker_ProgressChanged(object sender, ProgressChangedEventArgs e){/* Dosomething */}
private void BgWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e){/* Dosomething */}

private void Bt16_Click(object sender, RoutedEventArgs e)
{
    // 使能终止任务的功能
    bgWorker.WorkerSupportsCancellation = true;
    // 终止任务
    bgWorker.CancelAsync();
}

注意:

  1. 如何向DoWork中传递参数?

    bgWorker.RunWorkerAsync(object);可传递object变量,在DoWork中用e.Argument获取。

  2. 如何获取进度信息?

    使能标识bgWorker.WorkerReportsProgress = true

    注册事件ProgressChanged

    然后通过bgWorker.ReportProgress(i, message);函数启动

    个参数类型为int,表示执行进度。

    第二个参数为object,可以传递我们的自定义信息,在ProgressChanged中通过e.UserState获取

  3. Dowork中怎么向RunWorkerCompleted传递参数?

    DoWork中设置e.Result,在RunWorkerCompletede.Result中就可以获取到。

  4. 如何停止任务?

    使能标识bgWorker.WorkerSupportsCancellation = true;

    通过函数bgWorker.CancelAsync();停止

    注意:我们要自己在异步代码中编写捕获停止标识的代码,来控制异步代码的停止与退出。不是执行了CancelAsync();任务就会停止退出。

TAP(Task-based Asynchronous Pattern)

TPL(Task Parallel library)

TAP:基于任务的异步编程模型

TPL:任务并行库

好多文章都会把这两个混着说,实在是让人非常迷茫。查询了很多说法,其中我比较能认同。来自于【转】Difference between TPL and TAP_20201201

I want to clarify the difference between two these abbreviations: TPL(a task parallel library) and TAP (task async pattern).

AFAIU, TPL - is a task parallel library and the main part of this library is Task and all related staff. So, it's like a technology which was implemented by Microsoft.

TAP - it's a pattern which underlies to async/await syntax sugar. And which is based on callback function + state machine + SynchronizationContext logic.

Is there something to add or correct?

A:

TPL is a part of the BCL. It includes Task as well as several other parallelism-related higher-level abstractions including Parallel and Parallel LINQ. The focus of TPL was parallel processing, and using tasks as futures - while supported - was a relatively unused feature.

TAP is a pattern. It's called "Task-based" because it reused the Task type from the TPL as a generic Future type. Task (and related *) were enhanced to include more primitives to support TAP and asynchronous programming (e.g., GetAwaiter(), Task.WhenAll, etc). These days, TAP also works with "tasklikes" including ValueTask. TAP is focused on asynchronous programming as opposed to parallel processing.

我想说清这两个缩写之间的区别:TPL(Task Parallel library)和TAP(Task-based Asynchronous Pattern)。

据我所知(AFAIU:as far as I understand),TPL-是一个任务并行库,主要包含Task与所有其相关构成,它更偏向于是微软架设的一种底层技术。

TAP-是async/await语法糖的基础模式。是一种基于回调函数,状态机,与同步上下文逻辑(SynchronizationContext)的一种模式。

TPL是BCL的一部分。它包括Task以及其他许多包括Parallel and Parallel LINQ在内的与并行性相关的抽象。TPL专注于解决并行处理。在TPL中使用了tasks作为futures,是一直受支持的,但相对来说tasks是不怎么被使用的功能。

TAP是一种模式,它被成为“基于Task的”,因为它复用重用了TPL中的Task作为一个通用的Funture类型。Task(和其相关类型)都被增强了,以包含更过支持TAP和异步编程的原语(如,GetAwaiter()、Task.WhenAll 等)。如今,TAP还与包括ValueTask的“类tasks”型一起工作。TAP专注于处理异步编程问题,而不是并行处理。

个人理解:

TAP是基于TPL的。TPL其实与异步编程不是一个赛道的。好不要混着说。

Future type:感觉像是各种语言默认的与并行还是异步相关的一个类型,具体翻译成啥也说不好。

async/await

async/await关键字,主要用于我们使用顺序结构(而不是使用回调)来实现异步编程。极大增强异步编程的可读性。

下述异步方法即为:asyncawait关键字修饰的方法

注意:

  1. 异步方法的参数:不能使用“ref”参数和“out”参数,但是在异步方法内部可以调用含有这些参数的方法

  2. 异步方法的返回类型:返回类型有且只有3种,TaskTask<TResult>void。其中

    Task代表异步方法没有返回值

    Task<TResult>代表异步方法有返回值,且返回值类型为TResult

    void主要用于事件处理程序(不能被等待,无法捕获异常),也可以说只是为了兼容一些旧版本代码。

  3. asyncawait关键字不会导致其他线程的创建,只有当await等待任务运行时,异步方法才会将控制权转移给异步方法外部,让其不受阻塞的执行。待await等待的任务执行完毕再将控制权转移给await处,继续执行异步方法后续的代码。

    补充上一句,上一句的“只有当await等待任务运行时,异步方法才会将控制权转移给异步方法外部”会让人感觉是await关键字创建了新线程,但其实不是。await修饰的方法中依旧会存在“同步”的代码,真正创建新线程的方法还是方法中的Task.Run()或其他Task相关的代码。但那句话也不是不对,因为await修饰的代码必须返回TaskTask<TResult>,否则就会报错无法执行。

  4. 被“async”关键字标记的方法不会被转换为异步方式。

Task

Task.Run(~)

Para:(Action action),Return:Task

Queues the specified work to run on the thread pool and returns a System.Threading.Tasks.Task object that represents that work.

将指定工作排入线程池的工作队列,并返回一个Task代表这个工作。

Task.ContinueWith(~)

Creates a continuation that executes asynchronously when the target System.Threading.Tasks.Task completes.

创建一个伴随程序,当异步Task执行完毕的时候执行。

Para:(Action continuationAction),Return:Task

An action to run when the System.Threading.Tasks.Task completes. When run, the delegate will be passed the completed task as an argument.

只有一个参数 continuationAction时,它代表Task完成时所要运行的操作。该操作运行时,将会把已完成的任务作为参数传入委托。

Task.WaitAll(~)

Waits for all of the provided System.Threading.Tasks.Task objects to complete execution.

等待所有提供的Task执行完成。

就只单纯的等,相当于到这就停住,该方法包含的所有Task执行完毕后,才可以执行后续处理。

Q&A

什么是线程上下文

当系统从一个线程切换到另一个线程时,它将保存被抢先的线程的线程上下文,并重新加载线程队列中下一个线程的已保存的线程上下文。

个人理解就是线程需要保存的数据和资源。一般英文文档中的xxxContext都会被翻译为“xxx上下文”,个人认为是挺隔路。隔路的点在于,英文文档中的xxxContext都是表示该对象的内容,但汉语语境中,“xxx上下文”,通常会理解为除该对象以外的内容。

前台线程与后台线程的区别

这个根据要表达的重点不同会有很多表述。其核心功能可狭义理解为前台线程不受外在因素影响,启动后必须执行完才停止。而后台线程受其他因素控制,执行过程中也可立即停止。

一个显著的例子就是若应用程序启动了一个前台线程,退出应用程序后,前台线程还会继续执行(也就是应用程序其实并没有真正“退出”,资源也没有释放)。若应用程序启动的是后台线程,退出应用程序后,后台线程也会停止执行并释放。

所以使用前台线程时要注意避免遗留为停止的前台线程,会导致应用程序无法停止。

低优先级的线程会等待高优先级的线程执行完再执行吗?

不会,低优先级的线程不会被阻塞。低优先级的线程相比于高优先级的线程,只是在相同时间间隔内,被CPU调度的次数相对少而已。

线程池出现的原因

创建和销毁线程是十分消耗CPU资源的操作,也就是十分耗时的操作。频繁创建、销毁线程会影响应用程序性能。所以引入缓存来解决这个问题。创建一些线程后不销毁,而是保存在一些地方,需要使用线程时,调用这些已有线程就可以。节省了创建、销毁线程的时间。

系统,程序中的池是什么

我们编程过程中或多或少都接触过各种“池”,比如,数据库连接池,线程池,socket连接池等等。这些池的主要用途都是一个:把系统需要频繁使用的对象保存起来,供系统调用,节省对象重复创建与销毁多耗费的时间。是一种“空间换时间”的处理机制。

当然把对象保存起来并不能解决问题,我们还需要解决缓存的大小问题、排队执行任务、调度空闲线程、按需创建新线程及销毁多余空闲线程……等等问题。而微软的团队已经都为我们解决好了这些问题,也就是ThreadPool类,我们只需要调用类中的方法就可以了。这样我就就可以专注于程序业务功能而不是线程管理。

并行与并发的区别

并行:多个处理核心同一时刻同时处理多个不同的任务。

并发:一个处理核心在同一时间段处理多个不同任务,各个任务快速交替执行。即同一时刻,其实只有一个任务在执行。

什么是任务的全局队列与局部队列

在主线程或其他并没有分配给某个特定任务的线程的上下文中创建并启动的任务,这些任务将会在全局队列中竞争工作线程。这些任务被称为顶层任务

如果是在其他任务的上下文中创建的任务(子任务或嵌套任务),这些任务将被分配在线程的局部队列中。

全局队列的调用顺序是FIFO

局部队列的调用顺序通常是LIFO

为什么会出现任务的局部队列这种机制

线程的全局队列是共享资源,所以内部会实现一个锁机制。当一个任务内部会创建很多子任务时,并且这些子任务完成得非常快,就会造成频繁的进入全局队列和移出全局队列,从而降低应用程序的性能。

为了避免这种情况,线程池引擎为每个线程引入了局部队列。

局部队列有2个性能优势:任务内联化和工作窃取

什么是任务内联化

仅当线程等待时出现

是线程的局部队列带来的性能优化方法。是利用阻塞的顶层任务的线程去执行局部队列中的任务,减少了额外线程的开销。

如一个顶层任务需要等待3个嵌套任务执行完毕再执行,其中一个嵌套任务就可以运行在正在等待的顶层任务的线程中,这样就减少了一个额外线程的开销。

什么是工作窃取

就是让空闲的工作线程,来进入局部队列执行局部队列中正在等待的任务。

async会创建新线程还是await会创建新线程

都不会,async/await可以理解为一种异步的结构同步化语法糖,具体的新线程还是通过Task.Run()等代码创建。

在await的代码中不返回Task,返回void不行吗

不行,await后面跟着的必须是一个等待表达式,如TaskTask<TResult>。返回void,或其他参数会报错。"CS4008:无法等待void"或“CS1061:bool未包含GetAwaiter的定义,并且找不到可接受个bool类型参数的可访问扩展方法GetAwaiter(是否缺少 using 指令或程序集引用?)”

以前的异步编程怎么实现顺序执行

在异步代码内连续委托,回调。

异步编程模式的逐步发展主要为了什么

除去基础设施的完善。异步编程的发展主要为了编码人员能够更加简单的编写出异步程序。由初的Thread发展至目前常用的async\await关键字。逐步解决了线程频繁创建的问题,线程管理的问题,APM或EAP模式需要手写大量代码,又因为委托、回调导致代码可读性很差,控制流混乱的问题。终可以让我们以一种类似于同步的结构来编写异步代码,极大的减少了编写难度,增强了可读性。

异步编程本质是为了什么

这个一定是有很多的用处,但目前就我个人来说,大的用处就是

使用异步处理一些耗时操作,保证UI线程的线程能力,提高用户体验。

Thread.sleep()究竟是让那个线程停止。

解析一个场景

假设一个需求:我们需要从数据库中查询一个数据,并将查询结果显示到页面中。

假设查询数据库的方法为GetResult(),其至少需要5s。界面上显示的控件为TBResult

我们会有几种解决方式。

1,同步方式

string result = GetResult();
TBResult.Text = result;

大的弊病:

查询数据库的这至少5秒时间,整个应用是阻塞的,也就是不能操作的,简单来说就是卡死的。

2.Thread异步

Thread t = new Thread(() =>
{
    string result = GetResult();
    
    this.Dispatcher.Invoke(() =>
	{
		TBResult.Text = str;;
	});
});
t.Start();

一种方法是直接在线程中操作

Thread t = new Thread(() =>
{
    Action<string> callback = new Action<string>(ThreadCallBackAction);
    string result = GetResult();
    callback(result);
});
t.Start();

private void ThreadCallBackAction(string str)
{
	this.Dispatcher.Invoke(() =>
	{
		TBResult.Text = str;;
	});
}

另一种方法是利用委托来实现

弊病:

  1. 我们无法在外部正常获取Thread的返回值,也无法知道Thread什么时候执行完毕,已经获取到了值。
  2. Thread创建的线程是前台线程,很可能会造成线程问题,我们需要自己进行管理。

3.ThreadPool异步

ThreadPool.QueueUserWorkItem(new WaitCallback((s) =>
{
    Action<string> callback = new Action<string>(ThreadPoolCallBackAction);
    string result = GetResult();
    callback(result);
});

private void ThreadPoolCallBackAction(string str)
{
	this.Dispatcher.Invoke(() =>
	{
		TBResult.Text = str;;
	});
}

其实操作方式与Thread几乎一样,但是使用线程池我们就不用自己管理线程了。CLR引擎会替我们解决线程管理的问题

4.Task异步

var ResultTask = Task.Run(() => {
    string result = GetResult();
    return result;
});

ResultTask.ContinueWith((ResultTask)=> 
{
    this.Dispatcher.Invoke(() =>
	{
		TxtRes.Text = ResultTask.Result;
	});
});

使用Task,我们终于摆脱了复杂的回调,使用Task的ContinueWith方法就可以在指定任务执行结束后在执行其他任务。

5.Asyn/Await异步

private async void Button_Click(object sender, RoutedEventArgs e)
{
    string res = await Task.Run(() =>
    {
        string result = GetResult();
    	return result;
    });

    TxtRes.Text = res;
}

省去了复杂的Task方法调用过程,也省去了UI线程的委托过程。虽然是异步,但代码像同步一样逻辑简洁。

相关文章