C# 基础知识系列- 12 任务和多线程

0. 前言

照例一份前言,在介绍任务和多线程之前,先介绍一下异步和同步的概念。我们之间介绍的知识点都是在同步执行,所谓的同步就是一行代码一行代码的执行,就像是我们日常乘坐地铁通过安检通道一样,想象我们每个人都是一行代码,我们依次通过安检仪器的时候就是同步。

那么,什么是异步呢?有一个时间利用率的故事,讲的是在烧水的同时,顺便准备茶叶,清洗茶杯等工序可以节省时间。这个故事就是异步的一个典型范例。异步通俗的将就是不暂停也不等待当前耗时的流程执行完成,继续执行后续的流程。

那么这和任务与多线程有什么关系呢?在C#中,基于任务可以很简单的创建一个异步程序或者异步方法;同时任务也是一个简单的多线程模式。不过值得注意的是,C#的异步可以由多线程实现,但多线程更多的是用来实现并行。所谓并行,顾名思义,就是多任务同时执行,这里的任务指的是程序需要完成的事,而不是C#中的任务机制。

这一篇是《C#基础知识系列》的一篇,简单介绍一下如何创建、使用任务和多线程,这部分的内容很多,包括有很多注意事项,将会另开一个系列专门讲解C#的异步和并行编程,名字暂定为《C#异步编程系列》。

1. 线程

了解过计算机的人可能知道程序最小执行单元是线程,最小资源分配单位是进程。进程里必然至少有一个线程,而一个程序也必然至少有一个进程。这里不过多的介绍进程和线程的区别于关系,只需要记着线程是程序最小执行单元,我们在开发中最常用的也是线程。

在很多不太严谨的编程教程中,都会把多线程和并行化作等号。但是这里有一个很微妙的区别,对于单核CPU来说,多进程和多线程一样,都不会产生并行的效果;对于多核CPU而言,多进程必然是并行的,但是多线程则不一定并行。所以C#中,线程更多的用作异步处理上,而不是并行计算上。

在C#程序中,需要引用System.Threading。C#的入门级线程操作只需要知道Thread类、一个带参数的无返回值方法和一个不带参数的无返回值方法,这三个要点就可以了。

1.1 创建一个线程

var thread = new Thread(() =>{});

以上示例代码演示了如果创建一个线程。但创建了线程,并不代表线程就会运行。

说到这里就必须说一下线程的状态,一般情况线程分为五个阶段,也就是五种状态:分别是准备、就绪、运行、阻塞、死亡。当然在不同的地方,状态可能会细分为更多的级别,这里只做初步的介绍。状态之间的切换如下:

线程的状态之间切换顺序有着严格的限制,而且只能从就绪态由CPU切换到运行态,运行态无法从其他状态切换过去,而且这一步的切换开发者不能控制。

现在,我们回到线程的创建方法,先来看看Thread构造方法的声明:

public Thread (System.Threading.ParameterizedThreadStart start);public Thread (System.Threading.ThreadStart start);

碰到了两个没见过的类型,我们继续看看?

public delegate void ParameterizedThreadStart(object obj);public delegate void ThreadStart();

到这里,线程的创建为我们揭开了它的谜底。根据之前《C# 基础知识系列- 11 委托和事件》那篇的介绍,我们可以很明确的得到 ThreadStart是一个 无返回值也没有参数的委托,而ParameterizedThreadStart表示有一个object的参数。所以,创建线程的时候,可以直接传一个方法进去。

有的同学可能要问了,为什么创建线程的委托参数那么少?这里涉及到一个并发概念,因为线程访问过多的主线程可能会导致锁,所以最佳的线程实践就是让线程的运行保持一个相对封闭的环境。

当然,C#的线程其实放宽了这部分的限制,这部分将在《C#异步编程系列》中继续探讨。

现在我们回过头来,再看看如何创建一个标准的线程:

class Program{    static void Main(string[] args)    {        var thread1 = new Thread(ThreadTest1);        var thread2 = new Thread(ThreadTest2);    }    /// <summary>    /// 不带参数的线程    /// </summary>    public static void ThreadTest1()    {        // 业务代码    }    /// <summary>    /// 带参数的线程    /// </summary>    /// <param name="obj"></param>    public static void ThreadTest2(object obj)    {        //业务代码    }}

其中thread1就是一个没有参数的线程,thread2是一个带参数的线程。

注:Main方法是C#程序入口的固定写法,之前所有的示例代码都是在这个方法里执行的,后续这部分会在《C#基础篇之开发工具和项目的基本结构》这一篇中详细介绍,这里先记住这是一个固定写法。

1.2 启动并使用线程

在启动线程之前,我们先介绍一个概念:主线程。主线程指伴随着当前程序启动而启动的线程,以代码来看就是Main方法所在线程。

线程通过调用Thread.Start方法,来将线程标记为就绪态。

注意:线程不能直接进入运行态,该状态只能由CPU决定。

所以上一小节的创建的两个线程可以通过以下方式通知已经准备就续:

thread1.Start();

咦?是不是少了一个?注意力集中的小伙伴会发现,我没有演示thread2的调用方法。thread2与threa1有个不同的地方,thread2的委托参数有一个参数。那么必然Start也有一个对应的带参版本的重载,所以thread2就会有以下两种调用方式:

thread2.Start();

object obj;// 省略来源thread2.Start(obj);

两种方法有什么区别吗?

有,但是区别不大。第一种调用方式对于方法ThreadTest2而言就是参数为null,第二种就是参数为obj的值。所以第一种调用约等于thread2.Start(null)

1.3 暂停或销毁线程

这一小节的标题是,暂停或销毁线程。当线程运行起来后,如果没有突发情况或者外力干涉会直接运行到结束。这时候,后续程序觉得这个线程执行时间过长,需要暂停或者取消线程的执行,那么就需要了解一下如何暂停或者销毁线程了。

thread1.Suspend();//挂起thread1.Resume();//继续

中断线程,也就是终止线程:

thread1.Abort();// 已挂起的线程无法中断

强制终止销毁:

thread1.Interrupt();//在执行中的线程无法终止

以上是线程操作的基本概念,这部分并不是为了能让大家精通多线程,这是为了让大家有个初步概念。在C# 中,创建一个线程需要传递一个委托进去,因为委托的性质,并没有限制是否是静态方法,所以这里也可以传一个对象的方法。当然了,我们十分不提倡这样做,因为会导致一些多线程领域里的一些问题。

2. 任务

C#中的任务与线程的区别不是很大,因为C#的任务就是基于线程实现的,而任务比线程更友好,使用也更方便,当然使用也更加复杂。不过对于开发者而言,任务取消了线程的状态切换,只保留了有限的一部分。而且,在C# 更推荐使用任务,任务也是对线程的进一步抽象和改进。

2.1 创建一个任务

如线程相同的一点是,任务的创建也是通过传递一个方法(严格上讲是一个委托)。不同的是,线程的委托没有返回值而且也不接受从线程返回的值,而任务则不同,调用方可以期待任务是有返回值的而且也可以正常使用。

我们先来看看任务是什么,任务的命名空间System.Threading.Tasks,任务的类有以下两种声明:

public class Task : IAsyncResult, IDisposable;public class Task<TResult> : System.Threading.Tasks.Task;

第一个,没有泛型的Task类表示一个没有返回值的任务;

第二个,泛型Task类表示该任务有一个返回值,返回值的类型为传递进来的泛型参数。

两个任务类的初始化类似于Thread类,不过与之不同的是 泛型Task的参数是Func,都有一个带Object参数的委托。

与线程不同,任务的创建就有很多种方法:

1 通过构造函数创建

var task1 = new Task(() => { });var task2 = new Task<int>(()=> {    int i = 0;    return i;});

2 使用任务工厂:

var task1 = Task.Factory.StartNew(() => { });var task2 = Task.Factory.StartNew(() =>{    int i = 0;    return i;});

3 通过Task.Run创建:

var task1 = Task.Run(() => { });var task2 = Task.Run(() =>{    int i = 0;    return i;});

以上三种方式创建的任务是等效的。当然实际上任务的创建并非只有这么几种,但这几种是任务创建的基础,使用频率相当高。

2.2 执行任务

与线程不同的是,任务创建完成之后就会自动执行,不需要调用方法。

关于任务的运行有以下需要注意的地方:

  1. 任务的运行不会阻塞主线程;

  2. 主线程结束后,任务一定也会结束;

任务可以IsCompleted属性确定任务是否执行完成,所以可以通过访问任务对象的IsCompleted确认该任务是否执行完成,但有一个问题,这个属性只会表示当前任务是否完成。所以如果需要等待任务完成,则可以通过访问Wait()方法,强制主线程等待任务结束。

如果使用的任务是泛型Task也就是待返回值的任务,可以通过访问Result属性获取任务执行结果。有意思的地方就是,这个属性能获取到结果的时候,也是任务执行完成的时候,所以不需要调用Wait()IsCompleted来判断任务是否完成。

注:通过构造方法创建的任务需要调用 Start方法才能启动,而通过Task.Run和Task.Factory.StartNew创建的则不需要。

3. 总结

C#中任务基于线程,对其做了更多的抽象和封装,将线程的粒度进一步细分。所以线程在C#中就没有那么重要了,任务逐渐替代了线程在C#程序中的地位。

任务与线程,有共通的地方,也有完全不一样的地方。线程的运行环境相对封闭,所以线程出现错误导致线程中断,不会影响主线程的运行。但任务则不一样了,任务与主线程的关联性更大,一旦任务出现异常导致任务中断,如果没有正确处理,则会影响主线程的运行。

以上是本篇的全部内容,也请大家期待一下《C#异步编程系列》吧。

更多内容烦请关注我的博客

(0)

相关推荐

  • C#多线程编程(二)线程池与TPL

    一.直接使用线程的问题 每次都要创建Thread对象,并向操作系统申请创建一个线程,这是需要耗费CPU时间和内存资源的. 无法直接获取线程函数返回值 无法直接捕捉线程函数内发生的异常 使用线程池可以解 ...

  • 用一个开源工具实现多线程 Python 程序的可视化 | Linux 中国

    原创 邀你一起成为开源贡献者 Linux中国   导读:VizTracer 可以跟踪并发的 Python 程序,以帮助记录.调试和剖析. 本文字数:4686,阅读时长大约:6分钟 https://li ...

  • C# 异步编程

    基于Task的异步编程模式(TAP)是Microsoft为.Net平台下使用Task进行编程所提供的一组建议,这种模式提供了可以被await消耗(调用)方法的APIs,并且当使用async关键字编写遵 ...

  • 多线程之旅(Thread)

    在上篇文章中我们已经知道了多线程是什么了,那么它到底可以干嘛呢?这里特别声明一个前面的委托没看的同学可以到上上上篇博文查看,因为多线程要经常使用到委托.源码一.异步.同步1.同步(在计算的理解总是要你 ...

  • 奇门遁甲之基础知识(12)—开门

    每天学一点,认识深一点.大家好,我是胡说易道,易学的搬运工. 八门还剩最后一个没说完,现在就有请我们的压轴嘉宾闪亮等场.对,开门!就是你啦.开门又有什么意象呢? 朋友们,又到我们联想的时刻了.与开相关 ...

  • K线密码之裸K线入门基础知识系列教程(一)

    2019-06-26拾荒网 编辑:K线炮手 在股票市场如果不能坚持学习 那你是不可能有进步的,想进步就要不断学习股票各种技巧,其中k线的形态就能完美的体现出主力的意图 只要紧跟主力庄家我们才能收获多多 ...

  • K线密码之裸K线入门基础知识系列教程(二)

    K线密码之裸K线入门基础知识系列教程(二)

  • K线密码之裸K线入门基础知识系列教程(三)

    31.看涨吞没形态 应用法则: 1.看涨吞没形态出现在一轮明显的下跌趋势中,如果吞没形态具有下面列出了这样的一些参考性要素和特征,那么它们构成重要反转信号的可能性将大大地增强; 2.在看涨吞没形态中, ...

  • K线密码之裸K线入门基础知识系列教程(四)

    51.思量红三兵 应用法则: 1.虽然思量红三兵形态在一般情况下不属于顶部反转形态,但是有时候,它也能引出不容忽视的下跌行情.特别是若思量红三兵形态出现在一段上升行情的后期,当紧接着出现一个巨大的阴线 ...

  • 葡萄病害综合管理基础知识系列:来自链霉菌家族的农用抗生素

    在医学上,青霉素的发现与应用,可谓是人类健康史上的一次革命.但青霉素对结核杆菌的效果并不好.链霉素,发现于1943年10月19日,是美国罗格斯大学一名叫Albert Schatz 的博士生在著名微生物 ...

  • C# 基础知识系列- 1 数据类型

    常见数据类型 C#的类型一般分为值类型.引用类型两大类型. 值类型的实例存放在栈中,引用类型会在栈中放置一个指针指向堆中的某一块内容. C#为我们内置了几个数据类型供我们使用: 关键词简写 对应的类全 ...

  • C# 基础知识系列-7 Linq详解

    前言 在上一篇中简单介绍了Linq的入门级用法,这一篇尝试讲解一些更加深入的使用方法,与前一篇的结构不一样的地方是,这一篇我会先介绍Linq里的支持方法,然后以实际需求为引导,分别以方法链的形式和类S ...

  • C# 基础知识系列- 8 Linq最后一部分查询表达式语法实践

    C# 基础知识系列- 8 Linq最后一部分查询表达式语法实践