C#异步编程(一)线程及异步编程基础
最近试着做了几个.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
第一篇文章,所以先把基础的东西写出来,后续会深入讨论异步编程的实践。
C#异步编程(一)线程及异步编程基础的更多相关文章
- C#:异步编程和线程的使用(.NET 4.5 )
摘自:http://www.codeproject.com/Articles/996857/Asynchronous-programming-and-Threading-in-Csharp-N(葡萄城 ...
- 异步编程和线程的使用(.NET 4.5 )
C#:异步编程和线程的使用(.NET 4.5 ) 异步编程和线程处理是并发或并行编程非常重要的功能特征.为了实现异步编程,可使用线程也可以不用.将异步与线程同时讲,将有助于我们更好的理解它们的特征 ...
- C#:异步编程和线程的使用(.NET 4.5 ),异步方法改为同步执行
摘自:http://www.codeproject.com/Articles/996857/Asynchronous-programming-and-Threading-in-Csharp-N(葡萄城 ...
- Python并发编程06 /阻塞、异步调用/同步调用、异步回调函数、线程queue、事件event、协程
Python并发编程06 /阻塞.异步调用/同步调用.异步回调函数.线程queue.事件event.协程 目录 Python并发编程06 /阻塞.异步调用/同步调用.异步回调函数.线程queue.事件 ...
- 多线程编程学习笔记——使用异步IO(一)
接上文 多线程编程学习笔记——使用并发集合(一) 接上文 多线程编程学习笔记——使用并发集合(二) 接上文 多线程编程学习笔记——使用并发集合(三) 假设以下场景,如果在客户端运行程序,最的事情之一是 ...
- 多线程编程学习笔记——使用异步IO
接上文 多线程编程学习笔记——使用并发集合(一) 接上文 多线程编程学习笔记——使用并发集合(二) 接上文 多线程编程学习笔记——使用并发集合(三) 假设以下场景,如果在客户端运行程序,最的事情之一是 ...
- C#多线程和异步(三)——一些异步编程模式
一.任务并行库 任务并行库(Task Parallel Library)是BCL中的一个类库,极大地简化了并行编程,Parallel常用的方法有For/ForEach/Invoke三个静态方法.在C# ...
- socket编程的同步、异步与阻塞、非阻塞示例详解
socket编程的同步.异步与阻塞.非阻塞示例详解之一 分类: 架构设计与优化 简介图 1. 基本 Linux I/O 模型的简单矩阵 每个 I/O 模型都有自己的使用模式,它们对于特定的应用程序 ...
- 第十一章:Python高级编程-协程和异步IO
第十一章:Python高级编程-协程和异步IO Python3高级核心技术97讲 笔记 目录 第十一章:Python高级编程-协程和异步IO 11.1 并发.并行.同步.异步.阻塞.非阻塞 11.2 ...
- 【读书笔记】C#高级编程 第十三章 异步编程
(一)异步编程的重要性 使用异步编程,方法调用是在后台运行(通常在线程或任务的帮助下),并不会阻塞调用线程.有3中不同的异步编程模式:异步模式.基于事件的异步模式和新增加的基于任务的异步模式(TAP, ...
随机推荐
- 常见Web源码泄露总结
来自:http://www.hacksec.cn/Penetration-test/474.html 摘要 背景 本文主要是记录一下常见的源码泄漏问题,这些经常在web渗透测试以及CTF中出现. .h ...
- iOS 根据农历日期 获取当前的农历年份 即 干支纪年法算农历年
前言:我国古代是用干支纪年的,近代史上提到的甲午战争.戊戌变法.辛亥革命等名词就是干支纪年.所谓干支就是十天干和十二地支的简称.天干.地支按照一定规则(单配单,双配双)可以搭配成60对,也就是一个甲子 ...
- iOS 11 Xcode9开发 新特性学习 (警告篇)
最新版本SDK优化了开发体验,编译过程会提供更多提示警告,建议你修改.这些功能也可以自主选择用或者不用,当然,苹果喜欢你用他推荐的东西... 1 . @avalibale 语法,同步判断当前iOS系统 ...
- 【转载】wget 命令用法详解
wget是在Linux下开发的开放源代码的软件,作者是Hrvoje Niksic,后来被移植到包括Windows在内的各个平台上.它有以下功能和特点:(1)支持断点下传功能:这一点,也是网络蚂蚁和Fl ...
- Java变量修饰符volatile
volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以 ...
- centos_mysql5.6.35_rpm安装
1.查看操作系统相关信息.[root@linuxidc ~]# cat /etc/issue CentOS release 6.5 (Final) Kernel \r on an \m [root@l ...
- 3.django连接mysql数据库及安装mysqldb驱动报错解决办法
1.在setting.py设置连接数据库 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'djang ...
- 【bzoj1318】[Spoj744] Longest Permutation(乱搞)
题目传送门:https://www.lydsy.com/JudgeOnline/problem.php?id=1318 这道题的大意是要求一个长度为len,并包含1~len所有数,并使len最大的子区 ...
- composer启用国内镜像网站的配置更改办法
用法: 有两种方式启用本镜像服务: 将以下配置信息添加到 Composer 的配置文件 config.json 中(系统全局配置).见“例1” 将以下配置信息添加到你的项目的 composer.jso ...
- C语言下文件目录查看
C语言下文件目录遍历通常会用到下面这些函数 _access() /* 判断文件或文件夹路径是否合法 */ _chdir() /* 切换当前工作目录 */ _findfirst() / ...