最近试着做了几个.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. linux环境变量 【转】

    Linux 的变量可分为两类:环境变量和本地变量 环境变量,或者称为全局变量,存在与所有的shell 中,在你登陆系统的时候就已经有了相应的系统定义的环境变量了.Linux 的环境变量具有继承性,即子 ...

  2. 如何在IAR中配置CRC参数(转)

    源:如何在IAR中配置CRC参数 前言 STM32全系列产品都具有CRC外设,对CRC的计算提供硬件支持,为应用程序节省了代码空间.CRC校验值可以用于数据传输中的数据正确性的验证,也可用于数据存储时 ...

  3. HTTP协议—常见的HTTP响应状态码解析

    常见的HTTP响应状态码解析 1XX Informational(信息性状态码) 2XX Success(成功状态码) 3XX Redirection(重定向状态码) 4XX Client Error ...

  4. 2017最全的php面试题目及答案总结

    最近在网上看到很多的小伙伴们都在询问如何应对php面试,这个对于有工作经验和实战项目的小伙伴来说是没什么问题的,但是对于刚刚学习完php的小伙伴们.php面试却是一个很重要的一步,那么今天php中文网 ...

  5. 20145210姚思羽《网络对抗》MSF基础应用实验

    20145210姚思羽<网络对抗>MSF基础应用实验 实验后回答问题 1.用自己的话解释什么是exploit,payload,encode. exploit就是进行攻击的那一步 paylo ...

  6. JSP DAO(Model)

    示例代码: 1. Users类 package com.po; public class Users { private String username; private String passwor ...

  7. sql 数据库中只靠一个数据,查询到所在表和列名

    有时候我们想通过一个值知道这个值来自数据库的哪个表以及哪个字段,在网上搜了一下,找到一个比较好的方法,通过一个存储过程实现的.只需要传入一个想要查找的值,即可查询出这个值所在的表和字段名. 前提是要将 ...

  8. Hive数据类型总结

    转载自:http://blog.csdn.net/chenxingzhen001/article/details/20901045 Hive的内置数据类型可以分为两大类:(1).基础数据类型:(2). ...

  9. 使用 grep 查找所有包含指定文本的文件

    目标:本文提供一些关于如何搜索出指定目录或整个文件系统中那些包含指定单词或字符串的文件. 难度:容易 约定: # - 需要使用 root 权限来执行指定命令,可以直接使用 root 用户来执行也可以使 ...

  10. 泛型学习第三天——C#读取数据库返回泛型集合 把DataSet类型转换为List<T>泛型集合

    定义一个类: public class UserInfo    {        public System.Guid ID { get; set; } public string LoginName ...