最近试着做了几个.NET CORE的demo,看了些源码,感觉异步编程在Core里面已经成为主流,而对这块我还没有一个系统的总结,所以就出现了这篇文字,接下来几篇文章,我会总结下异步编程的思路,主要参考clr via c#及以前看过的优秀博文。第一篇文字,我们一起来就打牢基础,把线程基础知识梳理一遍。

  本文完全原创,如果转载请注明原文作者及链接。

一、线程基础

每个线程都有以下要素

线程内核对象(thread kernael object)

os为系统中创建的每个线程都分配并初始化这种数据结构,包含一组对线程进行描述的属性,还包含所谓的线程山下文(thread context)。上下文是包含cpu寄存器集合的内存块。对于x 86,x64和arm cpu架构,线程上下文分别使用700,1240和350字节的内存。

线程环境块(thread environment block,TEB)

TEB是在用户模式(应用程序代码能快速访问的地址空间)中分配和初始化的内存块。Teb耗用1个内存页。TEB包含线程的异常处理链首。线程进入的每个try块都在链首插入一个节点,线程退出try块时从链中删除该节点。此外,还包含线程的“线程本地存储”数据,以及由GDI(graphics device interface,图形设备接口)和opengl图形使用的一些数据结构

用户模式栈(user-mode stack)

用户模式栈存储传给方法的局部变量和实参。他还包含一个地址:指出当前的方法返回时,线程应该从什么地方接着执行。windows默认给每个线程的用户模式栈分配1mb内存。更具体地说,winows只是保留1mb地址空间,在线程实际需要时才会调拨物理内存。

内核模式栈(kernel-mode stack)

所谓的内核模式,主要是核心操作系统组件在内核模式下运行,很多驱动程序在内核模式下运行,内核模式效率更高,如果内核模式驱动程序损坏,则整个操作系统会损坏。

应用程序代码向操作系统中的内核模式函数传递实参时,还会使用内核模式栈。出于对按全额考虑,针对从用户模式的代码传递给内核的任何实参,windows都会把他们从线程的用户模式栈复制到线程的内核模式栈。一经赋值,内核就可以验证实参的值。由于应用程序代码不能访问内核模式栈,所以应用程序无法更改验证后的实参值。32位windows 内核栈大小12kb,64位windows是24kb。

DLL线程连接(attach)和线程分离(detach)通知

windows的一个策略是,任何时候在进程中创建线程,都会调用进程中加载的所有非托管dll的dllmain方法,并向该方法传递dll_thread_attach标志。类似地,任何时候线程终止,都会调用进程中的所有非托管dll的dllmain方法,并向方法传递dll_thread_detach标志。有的dll需要获取这些同志,才能为进程中创建/销毁的每个线程执行特殊的初始化或(资源)清理操作。

  1.1 windows系统线程切换

  cpu的单个核心同一时间只能进行一个线程的执行(不考虑intel超线程技术),在执行的线程可以运行一个“时间片”(quantum,也叫“量程”)。时间片时间片到期,windows进行线程切换所执行的操作:

1、 将cpu寄存器的值保存到当前正在运行的线程的内核对象内部的一个上下文结构中。

2、 从现有线程集合中选出一个线程进行执行。(如果该线程由另一个进程拥有,windows在开始执行任何代码之前,还必须切换虚拟地址空间到对应的进程)

3、 将所选执行线程上下文结构中的值加载到cpu·寄存器中

  虽然我们看到的是以上的三步,但是实际执行中,线程切换对性能的影响可能比以上三步的消耗更多。比如,cpu现在要执行一个不同的线程,而之前的线程的代码和数据还在cpu的告诉缓存(cache)中,这使cpu不必经常访问ram。而一旦进行上下文切换到新的线程,这个新的线程很大概率执行不同的代码,访问不同的数据,这些代码和数据并不在告诉缓存中,因此,cpu必须访问ram来填充他的高速缓存。

  1.2 线程调度的优先级

  windows之所以被称为抢占式多线程(preemptive multithreaded)操作系统,是因为线程可以在任何时间停止(被抢占)并调度另一个线程。程序员在这方面有一定的控制权,虽然不多。记住一点,你不能保证自己的线程一直在运行,你阻止不了其他线程的运行。

在windows中,每个线程都分配了从0(最低)到31(最高的优先级),系统决定为cpu分配哪个线程时,会以一种轮流的方式调度他。

线程的优先级是进程优先级和线程本身优先级叠加后计算出来的,如下图。

二、异步编程

  2.1 clr线程池

  创建和销毁线程是一个昂贵的操作,为了改善这个情况,clr包含了代码来管理自己的线程池(thread pool)。每个clr一个线程池:这个线程池由clr控制的所有AppDomain共享。

线程池具体维护多少的线程根据程序的请求频次有关,这个clr有内部的算法,我们这里不进行深入讨论。

  2.2 异步操作的取消

异步操作的取消可以使用CancellationTokenSource类

简单的代码实例如下

internal static class CancellationDemo
{
public static void Go()
{
CancellationTokenSource cts = new CancellationTokenSource(); ThreadPool.QueueUserWorkItem(o => Count(cts.Token, )); Console.WriteLine("press <enter> to cancel the operation"); Console.ReadLine();
cts.Cancel();//如果count方法已经返回,cancel没有任何效果 Console.ReadLine();
}
private static void Count(CancellationToken token, int countTo)
{
for (int count = ; count < countTo; count++)
{
if (token.IsCancellationRequested)
{
Console.WriteLine("Count is cancelled");
break;
}
Console.WriteLine(count);
Thread.Sleep();
}
Console.WriteLine("Count is done");
}
}

  2.3 Task

  QueueUserWorkItem没有内建机制让你知道操作在什么时候完成,也没有机制在操作完成时获取返回值。为了克服这些限制(并解决其他一些问题),microsoft引入了任务的概念。

调用方式如下:

ThreadPool.QueueUserWorkItem(o => Count(cts.Token, ));

Task.Run(() => Count(cts.Token, ));

Task.Run(() => { Console.WriteLine(); }, cts.Token);

  2.3.1 任务内部解密

  每个task对象都有一组字段,这些字段构成了任务的状态。其中包括一个Int32 Id(只读)、代表task执行状态的一个int32、对父任务的引用、对task创建时指定的taskScheduler的引用、对回调方法的引用、对要传给回调方法的对象的引用、对ExecutionContext的引用以及对ManualResetEventSlim对象的引用。另外每个task对象都有根据需要创建补充状态的引用。补充状态包含CancellationToken、一个ContinueWithTask对象集合、未抛出未处理异常的子任务而准备的一个task对象集合等。以上这么多,让我们意识到task虽然有用,但是并不是没有代价,如果不需要task的附加功能,那么使用threadpool.QueueUserWorkItem能获得更好的资源利用率。

在一个task对象的存在期间,课查询task的只读status属性了解它在其生存期的什么位置。该属性返回一个taskStatus值,如下

首次构造task对象时,他的状态是created。以后,当任务启动时,他的状态变成waitingToRun。task实际在一个线程上运行时,他的状态变成running。任务停止运行,并等待他的任何子任务时,状态变成waitingForChildrenToComplete。任务完成时进入一下状态之一:RanToCompoletion(运行完成),Canceled(取消)或Faulted(出错)。如果运行完成,可通过task<TResult>的Result属性来查询任务结果。出错时,可查询task的exception属性来获取任务抛出的未处理异常,该属性总是返回一个aggregateException对象,对象的innerException集合包含了所有未处理的异常。

为了简化编码,task提供了几个只读Boolean属性,包括IsCanceled,IsFaulted和IsCompleted。

调用continueWith等方法创建的task对象处于waitingForActivation状态。该状态意味着task的调度由任务基础结构控制,自动启动。

  2.3.2 任务工厂

  有时候需要创建一组共享相同配置的task对象。为避免机械的赋值,我们可以创建一个任务工厂来封装通用配置,TaskFactory和TaskFactory<TResult>。创建工厂类,需要向构造器传递需要具有的默认值,如CancellationToken、TaskScheduler、TaskCreationOptions及TaskContinuationOptions等。

实例代码如下

public static void Go()
{
Task parent = new Task(() =>
{
var cts = new CancellationTokenSource();
var tf = new TaskFactory<Int32>(cts.Token, TaskCreationOptions.AttachedToParent, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
var childTasks = new[] {
tf.StartNew(()=> Sum(cts.Token,)),
tf.StartNew(()=> Sum(cts.Token,)),
tf.StartNew(()=> Sum(cts.Token,Int32.MaxValue))//这里执行会抛错
};
//任何子任务抛出异常,就取消其余子任务
for (int task = ; task < childTasks.Length; task++)
{
childTasks[task].ContinueWith(t => cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted);
}
//所有子任务完成后,从未出错/未取消的任务获取返回的最大值,
//然后将最大值传给另一个任务来显示最大结果
tf.ContinueWhenAll(childTasks, completedTasks => completedTasks.Where(t => !t.IsFaulted && !t.IsCanceled).Max(t => t.Result), CancellationToken.None).ContinueWith(t => Console.WriteLine("The maximum is :" + t.Result), TaskContinuationOptions.ExecuteSynchronously);
});
//子任务完成后,显示任何未处理的异常
parent.ContinueWith(p =>
{ //这里先用stringbuilder收集输入,然后调用一次Console.WriteLine()
StringBuilder sb = new StringBuilder("the following exception(s) occurred:" + Environment.NewLine);
foreach (var e in p.Exception.Flatten().InnerExceptions)
{
sb.Append(" " + e.GetType().ToString());
}
Console.WriteLine(sb.ToString());
}, TaskContinuationOptions.OnlyOnFaulted);
parent.Start();
parent.Wait();
}
private static Int32 Sum(CancellationToken ct, Int32 n)
{
Int32 sum = ;
for (; n>; n--)
{
ct.ThrowIfCancellationRequested();
checked
{
sum += n;
}
}
return sum;
}

任务工厂代码

  2.3.3 任务调度

  任务基础结构非常灵活,TaskScheduler对象功不可没。TaskScheduler赋值执行任务的调度,同时向visual studio调试器公开任务信息。fcl提供了两个派生自TaskScheduler的类型:线程池任务调度器(thread pool task scheduler),和同步上下文任务调度器(synchronization context task scheduler)。默认情况下都是使用线程池任务调度器。同步上下文任务调度器适合提供了图形用户界面的应用程序,如wpf,uwp等。

参考资料:

《CLR via C#(第四版)》

MSDN

Stephen Cleary相关异步文章

第一篇文章,所以先把基础的东西写出来,后续会深入讨论异步编程的实践。

C#异步编程(一)线程及异步编程基础的更多相关文章

  1. C#:异步编程和线程的使用(.NET 4.5 )

    摘自:http://www.codeproject.com/Articles/996857/Asynchronous-programming-and-Threading-in-Csharp-N(葡萄城 ...

  2. 异步编程和线程的使用(.NET 4.5 )

    C#:异步编程和线程的使用(.NET 4.5 )   异步编程和线程处理是并发或并行编程非常重要的功能特征.为了实现异步编程,可使用线程也可以不用.将异步与线程同时讲,将有助于我们更好的理解它们的特征 ...

  3. C#:异步编程和线程的使用(.NET 4.5 ),异步方法改为同步执行

    摘自:http://www.codeproject.com/Articles/996857/Asynchronous-programming-and-Threading-in-Csharp-N(葡萄城 ...

  4. Python并发编程06 /阻塞、异步调用/同步调用、异步回调函数、线程queue、事件event、协程

    Python并发编程06 /阻塞.异步调用/同步调用.异步回调函数.线程queue.事件event.协程 目录 Python并发编程06 /阻塞.异步调用/同步调用.异步回调函数.线程queue.事件 ...

  5. 多线程编程学习笔记——使用异步IO(一)

    接上文 多线程编程学习笔记——使用并发集合(一) 接上文 多线程编程学习笔记——使用并发集合(二) 接上文 多线程编程学习笔记——使用并发集合(三) 假设以下场景,如果在客户端运行程序,最的事情之一是 ...

  6. 多线程编程学习笔记——使用异步IO

    接上文 多线程编程学习笔记——使用并发集合(一) 接上文 多线程编程学习笔记——使用并发集合(二) 接上文 多线程编程学习笔记——使用并发集合(三) 假设以下场景,如果在客户端运行程序,最的事情之一是 ...

  7. C#多线程和异步(三)——一些异步编程模式

    一.任务并行库 任务并行库(Task Parallel Library)是BCL中的一个类库,极大地简化了并行编程,Parallel常用的方法有For/ForEach/Invoke三个静态方法.在C# ...

  8. socket编程的同步、异步与阻塞、非阻塞示例详解

     socket编程的同步.异步与阻塞.非阻塞示例详解之一  分类: 架构设计与优化 简介图 1. 基本 Linux I/O 模型的简单矩阵 每个 I/O 模型都有自己的使用模式,它们对于特定的应用程序 ...

  9. 第十一章:Python高级编程-协程和异步IO

    第十一章:Python高级编程-协程和异步IO Python3高级核心技术97讲 笔记 目录 第十一章:Python高级编程-协程和异步IO 11.1 并发.并行.同步.异步.阻塞.非阻塞 11.2 ...

  10. 【读书笔记】C#高级编程 第十三章 异步编程

    (一)异步编程的重要性 使用异步编程,方法调用是在后台运行(通常在线程或任务的帮助下),并不会阻塞调用线程.有3中不同的异步编程模式:异步模式.基于事件的异步模式和新增加的基于任务的异步模式(TAP, ...

随机推荐

  1. ZOJ 3959 Problem Preparation 【水】

    题目链接 http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=3959 AC代码 #include <cstdio> ...

  2. SVN 过滤文件

    SVN新手最容易犯的一个错误: 就是把所有文件一股脑地全提交上去了. 这样很不好,因为这当中包含很多编译器自动生成的文件,还有中间文件. 这些文件可能每次编译都会不同,所以编译一次就冲突一次. 很显然 ...

  3. vue 数据传递的方法

    组件(Component)是 Vue.js 最强大的功能.组件可以封装可重用的代码,通过传入对象的不同,实现组件的复用,但组件传值就成为一个需要解决的问题. 1.父组件向子组件传值 组件实例的作用域是 ...

  4. Linux文件系统管理 挂载命令mount

    概述 mount命令用来挂载Linux系统外的文件. Linux 中所有的存储设备都必须挂载之后才能使用,包括硬盘.U 盘和光盘(swap 分区是系统直接调用的,所以不需要挂载).不过,硬盘分区在安装 ...

  5. 【HackerRank】Insertion Sort Advanced Analysis(归并排序求数列逆序数对)

    Insertion Sort is a simple sorting technique which was covered in previous challenges. Sometimes, ar ...

  6. 【鸟哥的Linux私房菜】笔记1

    Linux是什么 从操作系统与cpu架构关系到linux  Richard Mathew Stallman GPL 关于GNU计划 Linux的发展 Linux的核心版本 Linux的特色 Linux ...

  7. Vue.js学习笔记 第六篇 内置属性

    computed属性 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> &l ...

  8. 斯坦福机器学习视频笔记 Week6 关于机器学习的建议 Advice for Applying Machine Learning

    我们将学习如何系统地提升机器学习算法,告诉你学习算法何时做得不好,并描述如何'调试'你的学习算法和提高其性能的“最佳实践”.要优化机器学习算法,需要先了解可以在哪里做最大的改进. 我们将讨论如何理解具 ...

  9. Cisco学习笔记

    目录 1. 路由 1.1 静态路由 1.2 动态路由 2. 访问控制列表 2.1 标准访问控制列表 2.2 扩展访问控制列表 2.3 命名访问控制列表 3. VLAN 3.1 基础知识 3.2 配置实 ...

  10. java基础学习总结——java环境变量配置(转)

    只为成功找方法,不为失败找借口! 永不放弃,一切皆有可能!!! java基础学习总结——java环境变量配置 前言 学习java的第一步就要搭建java的学习环境,首先是要安装 JDK,JDK安装好之 ...