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#异步编程系列》吧。

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

C# 基础知识系列- 12 任务和多线程的更多相关文章

  1. C# 基础知识系列- 17 小工具优化

    0. 前言 不知道有没有动手能力强的小伙伴照着上一篇的内容写过程序呢?如果有的话,应该会在使用的时候发现以下几个问题: 每次启动都需要经过漫长的时间去遍历磁盘里的文件目录 因为数据是用的字典保存的,所 ...

  2. C# 基础知识系列- 3 集合数组

    简单的介绍一下集合,通俗来讲就是用来保管多个数据的方案.比如说我们是一个公司的仓库管理,公司有一堆货物需要管理,有同类的,有不同类的,总而言之就是很多.很乱.我们对照集合的概念对仓库进行管理的话,那么 ...

  3. 基础知识系列☞Abstract和Virtual→及相关知识

    转载地址→http://www.cnblogs.com/blsong/archive/2010/08/12/1798064.html 在C#的学习中,容易混淆virtual方法和abstract方法的 ...

  4. C# 基础知识系列- 9 字符串的更多用法(一)

    0. 前言 在前面的文章里简单介绍了一下字符串的相关内容,并没有涉及到更多的相关内容,这一篇将尝试讲解一下在实际开发工作中会遇到的字符串的很多操作. 1. 创建一个字符串 这部分介绍一下如何创建一个字 ...

  5. C# 基础知识系列-13 常见类库(三)

    0. 前言 在<C# 基础知识系列- 13 常见类库(二)>中,我们介绍了一下DateTime和TimeSpan这两个结构体的内容,也就是C#中日期时间的简单操作.本篇将介绍Guid和Nu ...

  6. C# 基础知识系列- 16 开发工具篇

    0. 前言 这是C# 基础知识系列的最后一个内容讲解篇,下一篇是基础知识-实战篇.这一篇主要讲解一下C#程序的结构和主要编程工具. 1. 工具 工欲善其事必先利其器,在实际动手之前我们先来看看想要编写 ...

  7. 基础知识系列☞C#中→属性和字段的区别

    "好吧...准备写个'基础知识系列',算是记录下吧,时时看看,更加加深记忆···" 其实本来准备叫"面试系列"... 字段.属性.你先知道的哪个概念? ***我 ...

  8. 学习javascript基础知识系列第二节 - this用法

    通过一段代码学习javascript基础知识系列 第二节 - this用法 this是面向对象语言中的一个重要概念,在JAVA,C#等大型语言中,this固定指向运行时的当前对象.但是在javascr ...

  9. 学习javascript基础知识系列第三节 - ()()用法

    总目录:通过一段代码学习javascript基础知识系列 注意: 为了便于执行和演示,建议使用chrome浏览器,按F12,然后按Esc(或手动选择)打开console,在console进行执行和演示 ...

随机推荐

  1. win 7 系统过期处理办法

    超级尴尬,刚装的win7 系统居然过期了.下次再也不装盗版了,吼吼吼 处理方法就是下载oem7F7 软件安装,不用再怎么操作就好了. 就是这么简单,并不用像网上那样找什么激活码.

  2. 使用 PyTorch 进行 风格迁移(Neural-Transfer)

    1.简介 本教程主要讲解如何实现由 Leon A. Gatys,Alexander S. Ecker和Matthias Bethge提出的Neural-Style 算法.Neural-Style 或者 ...

  3. 编译原理:非确定的自动机NFA确定化为DFA

    1.设有 NFA M=( {0,1,2,3}, {a,b},f,0,{3} ),其中 f(0,a)={0,1}  f(0,b)={0}  f(1,b)={2}  f(2,b)={3} 画出状态转换矩阵 ...

  4. iOS 继承

    是否使用继承需要考虑三个点: 父类只是给子类提供服务,并不涉及子类的业务逻辑 层级关系明显,功能划分清晰,父类和子类各做各的. 父类的所有变化,都需要在子类中体现,也就是说此时耦合已经成为需求 万不得 ...

  5. flex布局你真的搞懂了吗?通俗简洁,小白勿入~

    flex布局 用以代替浮动的布局手段: 必须先把一个元素设置为弹性容器://display:flex: 一个元素可以同时是弹性容器和弹性元素; 设为flex布局以后,子元素的float.clear和v ...

  6. Python——铅球飞行计算问题

    一. 1.IPO描述为:输入:铅球发射角度. 初始速度(m/s). 初始高度(m) 处理:模拟铅球飞行,时刻更新铅球在飞行中的位置 输出:铅球飞行距离(m) 可以拆分小的时间段.任意时刻的位置,都是 ...

  7. 实现ls

    一.任务详情 参考伪代码实现ls的功能,提交代码的编译,运行结果截图,码云代码链接 打开目录文件 针对目录文件 读取目录条目 显示文件名 关闭文件目录文件 二.伪代码 opendir while(re ...

  8. 使用 Visual Studio 开发、测试和部署 Azure Functions(二)测试,部署

    1,引言 上一篇介绍了使用使用 Visual Studio 开发 "Azure Functions" 函数,此篇介绍 “Azure Functions” 的测试以及直接从 Vist ...

  9. 深入解读ES6系列(四)

    来自老曾es6的前言: 哈喽小伙伴们,爱说'废'话的Z又回来了,欢迎来到Super IT曾的博客时间,上一节说了字符串,面向对象以及json的知识,这一节我们继续我们知识的海洋,一起奋斗不秃头!不足的 ...

  10. c期末笔记(1)

    运算符 1.i++与++i的细微区别 i++与++i 和i++放在一个语句中,则i原本的值先被利用.语句结束后,i的值加一. i的原始值失效,直接加一. 2.int加法 整形数据(int)加上任何类型 ...