前言

学习这件事情是一个习惯,不能停。。。另外这篇已经看过两个月过去,但觉得有些事情不总结跟没做没啥区别,遂记下此文

1.CLR线程池基础

2.ThreadPool的简单使用练习

3.执行上下文

4.协作式取消和超时,System.Threading.CancellationTokenSource的简单使用

5.任务

6.任务调度器

一、CLR线程池基础

如26章所述,创建和销毁线程是一个昂贵的操作,要耗费大量的时间。另外太多的线程会浪费内存资源。由于操作系统必须调度可运行的线程并执行上下文切换,所以太多的线程还对性能不利。

为了改善这个情况,CLR包含了代码管理它自己的线程池(thread pool),线程池是你的应用程序能使用的线程的集合。

每CLR一个线程池,这个线程池由CLR控制的所有AppDomain共享。

CLR初始化时,线程池中是没有线程的。在内部,线程池维护了一个操作请求队列。应用程序执行一个异步操作时,就调用某个方法,将一个记录项(entry)追加到线程池的队列中,线程池的代码从这个队列中提取记录项,将这个记录项派发(dispatch)给一个线程池线程。如果线程池中没有线程,就创建一个新线程。

如果应用程序向线程池发出许多请求,线程池会尝试只用一个线程来服务所有请求。然而,如果你的应用程序发出请求的速度超过了线程池线程处理它们的速度,就会创建额外的线程。

当一个线程池线程闲着没事一段时间之后,线程会自己醒来终止自己以释放资源。

二、ThreadPool的简单使用练习

    class Program
{
static void Main(string[] args)
{
Console.WriteLine($"Main Thread,当前线程:{Thread.CurrentThread.ManagedThreadId}");
ThreadPool.QueueUserWorkItem(Calculate,);
Console.WriteLine($"Main Thread doing other work,当前线程:{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep();
Console.WriteLine("hi <Enter> to end this program~~");
Console.Read();
} //这个方法的签名必须匹配waitcallback委托
public static void Calculate(object state)
{
//这个方法由一个线程池线程执行
Console.WriteLine($"In Calculate:state={state},当前线程:{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep();
//这个方法返回后,线程回到池中,等待另一个任务
}
}

运行结果:

有时上图标注这两行输出结果顺序会颠倒,这是因为两个方法相互之间是异步运行的,windows调度器决定先调度哪一个线程。

三、执行上下文

每个线程都关联一个执行上下文数据结构。

执行上下文(execution context)包括的东西有安全设置(压缩栈、Thread的Principal属性和Windows的身份)、宿主设置(System.Threading.HostExecutionContextManager)以及逻辑调用上下文数据(参见System.Runtime.Remoting.Messaging.CallContext的LogicalSetData和LogicalGetData方法)。

默认情况下,CLR自动造成初始线程的执行上下文“流向”任何辅助线程。这造成将上下文信息传给辅助线程,但这会对性能造成一定影响。

这是因为执行上下文中包含大量信息,而收集所有这些信息,再把它们复制到辅助线程,要耗费不少时间。

System.Threading.ExecutionContext类,允许你控制线程的执行上下文如何从一个线程“流向”另一个。可用这个类 阻止上下文流动以提升应用程序的性能。

    class Program
{
static void Main(string[] args)
{
//将一些数据放到Main线程的逻辑调用上下文中
CallContext.LogicalSetData("Name", "Michael");
//初始化要由线程池线程做的一些工作
//线程池线程能访问逻辑调用上下文结构
ThreadPool.QueueUserWorkItem(
state => Console.WriteLine($"Name={CallContext.LogicalGetData("Name")}"));
//阻止Main线程的执行上下文的流动
ExecutionContext.SuppressFlow();
//初始化要由线程池做的工作
//线程池线程不能访问逻辑调用上下文数据
ThreadPool.QueueUserWorkItem(
state => Console.WriteLine($"Name={CallContext.LogicalGetData("Name")}"));
//恢复Main线程的执行上下文的流动,
//以免将来使用更多的线程池线程
ExecutionContext.RestoreFlow(); Console.ReadLine();
}
}

编译后运行结果如下:

四、协作式取消和超时,System.Threading.CancellationTokenSource的简单使用

Microsoft.NET Framework提供了标准的取消操作模式。这个模式是协作式的,意味着要取消的操作必须显式支持取消。

CancellationToken实例是轻量级值类型,包含单个私有字段,即对其CancellationTokenSource对象的引用。

在计算限制操作的循环中,可定时调用CancellationToken的IsCancellationRequsted属性,了解循环是否应该提前终止,从而终止计算限制的操作。

提前终止的好处在于,CPU不需要再把时间浪费在你对结果不感兴趣的操作上。

    static void Main(string[] args)
{
Go();
} public static void Go()
{
CancellationTokenSource token = new CancellationTokenSource();
//将CancellationTokenSource和参数 传入操作
ThreadPool.QueueUserWorkItem(
o => Count(token,));
Console.WriteLine($"Hit <Enter> to cancel operation");
Console.ReadLine();
token.Cancel();//如果Count方法已返回,Cancel没有任何效果
//执行cancel后 立即返回,方法从这里继续运行
Console.ReadLine();
}
public static void Count(CancellationTokenSource token,Int32 counto)
{
for (int count = ; count < counto; count++)
{
if(token.IsCancellationRequested)
{
Console.WriteLine("操作被取消");
break;
}
Console.WriteLine(count);
Thread.Sleep(); //出于显示目的而浪费一些时间你
}
Console.WriteLine("Count is done");
}

运行结果如下图所示:

可调用CancellationTokenSource的Register方法登记一个或多个在取消一个CancellationTokenSource时调用的方法。

向被取消的CancellationTokenSource登记一个回调方法,将由调用Register的线程调用回调方法(如果为useSynchronizationContext参数传递了true值,就可能要通过调用线程的SynchronizationContext进行)。

多次调用Register,多个调用方法都会调用。这些回调方法可能抛出未处理的异常。

如果调用CancellationTokenSource的Cancel方法,向它传递true,那么抛出了未处理异常的第一个回调方法会阻止其他回调方法的执行,抛出的异常也会从Cancel中抛出。

如果调用Cancel并向它传递false,那么登记的所有回调方法都会调用。所有未处理的异常都会添加到一个集合中。所有回调方法都执行好后,其中任何一个抛出了未处理的异常,Cancel就会抛出一个AggregateException,该异常实例的InnerExceptions属性被设为已抛出的所有异常对象的集合。

        static void Main(string[] args)
{
var cts1 = new CancellationTokenSource();
cts1.Token.Register(() => Console.WriteLine($"cts1被取消"));
var cts2 = new CancellationTokenSource();
cts2.Token.Register(() => Console.WriteLine($"cts2被取消"));
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);
linkedCts.Token.Register(() => Console.WriteLine($"linkedCts 被取消"));
cts2.Cancel();
Console.WriteLine($"cts1 canceled={cts1.IsCancellationRequested},cts2 canceled={cts2.IsCancellationRequested}," +
$"linkedCts={linkedCts.IsCancellationRequested}");
Console.ReadLine();
}

运行结果如下图:

如果要在过一段时间后取消操作,要么用接收延时参数的构造器构造一个CancellationTokenSource对象,要么调用CancellationTokenSource的CancelAfter方法。

五、任务 

通过观察,我们发现 ThreadPool最大的问题是没有内建的机制让你知道 操作在什么时候完成,以及操作完成时获得返回值。鉴于此,Microsoft引入了任务的概念。

下面展示一个使用task的简单例子:

        static void Main(string[] args)
{
Console.WriteLine($"当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
//创建一个Task,现在还没有开始运行
Task<Int32> t = new Task<int>(n => Sum((Int32)n), );
//可以后等待任务
t.Start();
//可选择显示等待任务完成
t.Wait();
//可获得结果(result属性内部会调用Wait)
Console.WriteLine($"the Sum is:{t.Result},当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
Console.ReadLine();
}
private static Int32 Sum(Int32 n)
{
Int32 sum = ;
for (; n>; n--)checked
{
sum += n; //如果n太大,会抛出System.OverflowException
}
Console.WriteLine($"In Sum,当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
return sum;
}

运行结果如右图:

如果计算限制任务抛出未处理的异常,异常会被“吞噬”并存储到一个集合中,调用wait方法或Result属性时,这些成员会抛出一个System.AggregateException对象。

AggregateException提供了一个Handle方法,它为AggregateException中包含的每个异常都调用一个回调方法。回调方法可以为每个异常决定如何对其处理;回调返回true表示异常已处理;返回false表示未处理。调用Handle后,如果至少有一个异常没有处理,就创建一个新的AggregateException对象,其中只包含未处理的异常。

Task的静态WaitAny方法会阻塞调用线程,直到数组中的任何Task对象完成。方法返回Int32数组索引值,指明完成的是哪个Task的对象

Task的静态WaitAll方法也会阻塞调用线程,直到数组中的所有Task对象完成。

下面演示下task取消操作和task的异常处理

         static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
Task<Int32> t = Task.Run(() => Sum(cts.Token, ), cts.Token); cts.Cancel();
try
{
Console.WriteLine($"the Sum is:{t.Result},当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
}
catch (AggregateException ex)
{
//将任何OperationCanceledException对象都是为已处理
//其他任何异常都造成抛出一个新的AggregateException
//其中只包含未处理异常
ex.Handle(e => e is OperationCanceledException);
Console.WriteLine("Sum was canceled");
}
Console.ReadLine();
}
private static Int32 Sum(CancellationToken ct, Int32 n)
{
Int32 sum = ;
for (; n>; n--)checked
{
//再取消标志引用的CancellationTokenSource上调用Cancel,
//下面这行代码就会抛出OperationCanceledException
ct.ThrowIfCancellationRequested();
sum += n; //如果n太大,会抛出System.OverflowException
}
Console.WriteLine($"In Sum,当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
return sum;
}

调用Wait,或者在任务尚未完成时查询任务的Result属性,极有可能造成线程池创建新线程,这增大了资源的消耗,也不利于性能和伸缩性。

要知道一个任务在什么时候结束,任务完成时可启动另一个任务。

Microsoft为我们提供了ContinueWith,下面简单展示使用

        static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
Task<Int32> t = Task.Run(() => Sum(cts.Token, ), cts.Token);
Task cwt= t.ContinueWith(task => Console.WriteLine($"Sum result is {task.Result}"));
}
private static Int32 Sum(CancellationToken ct, Int32 n)
{
Int32 sum = ;
for (; n>; n--)checked
{
//再取消标志引用的CancellationTokenSource上调用Cancel,
//下面这行代码就会抛出OperationCanceledException
ct.ThrowIfCancellationRequested();
sum += n; //如果n太大,会抛出System.OverflowException
}
Console.WriteLine($"In Sum,当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
return sum;
}

Task对象内部包含了ContinueWith任务的一个集合。可在调用ContinueWith时传递对一组TaskContinuationOptions枚举值进行判断满足什么情况才执行ContinueWith。

偷个懒,哈哈。。。

任务可以启动多个子任务,下面简单展示下使用

        static void Main(string[] args)
{
Task<Int32[]> task = new Task<Int32[]>(() =>
{
var results = new Int32[];
new Task(() => results[] = Sum(), TaskCreationOptions.AttachedToParent).Start();
new Task(() => results[] = Sum(), TaskCreationOptions.AttachedToParent).Start();
new Task(() => results[] = Sum(), TaskCreationOptions.AttachedToParent).Start();
return results;
});
var cwt = task.ContinueWith(
parentTask => Array.ForEach(parentTask.Result, Console.WriteLine));
task.Start();
Console.ReadLine();
}
private static Int32 Sum( Int32 n)
{
Int32 sum = ;
for (; n>; n--)checked
{
sum += n; //如果n太大,会抛出System.OverflowException
}
Console.WriteLine($"In Sum,当前线程ID:{Thread.CurrentThread.ManagedThreadId}");
return sum;
}

TaskCreationOptions.AttachedToParrent标志将一个Task和创建它的Task关联,结果是除非所有子任务(以及子任务的子任务)结束运行,否则创建任务(父任务)不认为已经结束。

在一个Task对象的存在期间,可查询Task的只读Status属性了解它在其生存期的什么位置。

要创建一组共享相同配置的Task对象。可创建一个任务工厂来封装通用的配置。即TaskFactory。

在调用TaskFactory或TaskFactory<TResult>的静态ContinueWhenAll和ContinueWhenAny方法,无论前置任务是如何完成的,ContinueWhenAll和ContinueWhenAny都会执行延续任务。

六、任务调度器

对于不了解任务调度的小白来讲,可能遇到过下面这个场景

啊,怎么会这样呢?为什么不能在线程里更新UI组件。

TaskScheduler对象负责执行被调度的任务,同时向Visual Studio调试器公开任务信息。

FCL提供了两个派生自TaskScheduler的类型:线程池任务调度器(thread pool task scheduler),和同步上下文任务调度器(synchronization context task scheduler)。

默认情况下,所有应用程序使用的都是线程池任务调度器。可查询TaskScheduler的静态Default属性来获得对默认任务调度器的引用。

同步上下文任务调度器适合提供了图形用户界面的应用程序。它将所有任务都调度给应用程序的GUI线程,使所有任务代码都能成功的更新UI组件。该调度不使用线程池。可执行TaskScheduler的静态FromCurrentSynchronizationContext方法来获得对同步上下文任务调度器的引用。

下面展示一个简单的例子,演示如何使用同步上下文任务调度器

     public partial class MainForm : Form
{
private readonly TaskScheduler m_syncContextTaskScheduler;
public MainForm()
{
//获得一个对同步上下文任务调度器的引用
m_syncContextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Text = "Synchronization Context Task Scheduler Demo";
Visible = true; Width = ; Height = ;
} private CancellationTokenSource m_cts; protected override void OnMouseClick(MouseEventArgs e)
{
if(m_cts!=null)
{
m_cts.Cancel(); //一个操作正在运行,取消它
m_cts = null;
}
else
{
//任务没有开始启动它
Text = "Operation running";
m_cts = new CancellationTokenSource();
//这个任务使用默认任务调度器,在一个线程池线程上运行
Task<Int32> t = Task.Run(()=>Sum(),m_cts.Token);
//这些任务使用 同步上下文任务调度器,在GUI线程上执行
t.ContinueWith(task => Text = "Result:" + t.Result,
CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion,
m_syncContextTaskScheduler);
t.ContinueWith(task => Text = "Operation canceled ",
CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled,
m_syncContextTaskScheduler);
t.ContinueWith(task => Text = "Operation defaulted ",
CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted,
m_syncContextTaskScheduler);
} base.OnMouseClick(e);
}
private static Int32 Sum(Int32 n)
{
Int32 sum = ;
for (; n > ; n--) checked
{
sum += n; //如果n太大,会抛出System.OverflowException
}
return sum;
}
}

单击窗体的客户区域,就会在线程池线程上启动一个计算限制操作。使用线程池线程,因为GUI线程在此期间不会被阻塞,能响应其他UI操作。

Parallel就留在下篇来介绍吧。。。

天道酬勤,大道至简,坚持。

CLR via C# 读书笔记-27.计算限制的异步操作(上篇)的更多相关文章

  1. CLR Via CSharp读书笔记(26) - 计算限制的异步操作

    执行上下文: 执行上下文包括安全设置(压缩栈.Thread的Principal属性和Windows身份),宿主设置(System.Threading.HostExecutionContextManag ...

  2. 《CLR via C#》读书笔记 之 计算限制的异步操作

    <CLR via C#>读书笔记 之 计算限制的异步操作 2014-07-06 26.1 CLR线程池基础 返回 如25章所述,创建和销毁线程是一个比较昂贵的操作: 太多的线程也会浪费内存 ...

  3. Clr Via C#读书笔记---I/O限制的异步操作

    widows如何执行I/O操作      构造调用一个FileStream对象打开一个磁盘文件-----FileStream.Read方法从文件中读取数据(此时线程从托管代码转为本地/用户模式代码)- ...

  4. 《CLR Via C#》读书笔记:27.计算限制的异步操作

    一.CLR 线程池基础 一般来说如果计算机的 CPU 利用率没有 100% ,那么说明很多进程的部分线程没有运行.可能在等待 文件/网络/数据库等设备读取或者写入数据,又可能是等待按键.鼠标移动等事件 ...

  5. <数据结构与算法分析>读书笔记--运行时间计算

    有几种方法估计一个程序的运行时间.前面的表是凭经验得到的(可以参考:<数据结构与算法分析>读书笔记--要分析的问题) 如果认为两个程序花费大致相同的时间,要确定哪个程序更快的最好方法很可能 ...

  6. Clr Via C#读书笔记---垃圾回收机制

    #1 垃圾回收平台的基本工作原理: 访问一个资源所需的具体步骤: 1)调用IL指令newobj,为代表资源的类型分配内存.在C#中使用new操作符,编译器就会自动生成该指令.2)初始化内存,设置资源的 ...

  7. Clr Via C#读书笔记---计算限制的异步操作

    线程池基础 1,线程的创建和销毁是一个昂贵的操作,线程调度以及上下文切换耗费时间和内存资源. 2,线程池是一个线程集合,供应你的用程序使用. 3,每个CLR有一个自己的线程池,线程池由CLR控制的所有 ...

  8. Clr Via C#读书笔记---线程基础

    趣闻:我是一个线程:http://kb.cnblogs.com/page/542462/ 进程与线程 进程:应用程序的一个实例使用的资源的集合.每个进程都被赋予了一个虚拟地址空间. 线程:对CPU进行 ...

  9. CLR via C# 读书笔记-26.线程基础

    前言 这俩个月没怎么写文章做记录分享,一直在忙项目上线的事情,但是学习这件事情,停下来就感觉难受,clr线程这章也是反复看了好多遍,书读百遍其义自见,今天我们来聊下线程基础 1.进程是什么,以及线程起 ...

随机推荐

  1. Quartz.NET文档 入门教程

    概述 Quartz.NET是一个开源的作业调度框架,非常适合在平时的工作中,定时轮询数据库同步,定时邮件通知,定时处理数据等. Quartz.NET允许开发人员根据时间间隔(或天)来调度作业.它实现了 ...

  2. java之api讲解

    1:数值运算 Java提供了java.lang.Math类支持数值运算 看文档 java.lang叫做核心语言包,里面包含的是Java中最基础的一些类,此包中的类,可以使用,不用import该包 举例 ...

  3. Hash表算法详解

    Hash表定义 散列表(Hash table,也叫哈希表),是根据关键字值(Key value)直接进行访问的数据结构.也就是说,它通过把关键字(关键字通过Hash算法生成)映射到表中一个位置来访问记 ...

  4. CMDB-客户端

    配置文件的设置 大体思路: 1,通过开始文件将用户配置信息的文件放置到环境变量中. 2,在lib文件中的config文件中,从环境变量中获取到用户的配置,通过importlib模块导入用户配置文件,通 ...

  5. lucene4

    在lucene通过对应的API建立索引.在学习的过程中我们了解到lucene下面索引的建立与关系数据库有相似的地方. IndexReader.delete删除有两种删除的形式. 第三个改变Docume ...

  6. java动态规划取硬币问题

    最近一直在研究动态规划的问题.今天遇到了取硬币问题. 其实动态规划还是,我从底部向顶部,依次求出每个状态的最小值,然后就可以标记上. 这道题目就是,假如有1,5,7,10这四种币值的硬币,我取14元, ...

  7. shell编程之sed语法

    首先插播条广告:  想要进一个文件夹去 看下面有那些文件 必须对这个文件夹有执行权限. sed p  打印对应的行  2p 打印第二行. -n  只输出经过sed 命令处理的行 看图吧 不太会擅长言语 ...

  8. C++ std::vector

    std::vector template < class T, class Alloc = allocator<T> > class vector; // generic te ...

  9. mongodb修改用户名密码

    首先先将启动mongo的配置文件里面的 auth:用户认证,改为false. 正确做法,利用db.changeUserPassword db.changeUserPassword('tank2','t ...

  10. Boost智能指针使用总结

    内存管理是一个比较繁琐的问题,C++中有两个实现方案: 垃圾回收机制和智能指针.垃圾回收机制因为性能等原因不被C++的大佬们推崇, 而智能指针被认为是解决C++内存问题的最优方案. 1. 智能指针定义 ...