专栏首页不做码农的开发者C# 基础知识系列- 12 任务和多线程

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来判断任务是否完成。

3. 总结

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

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

本文分享自微信公众号 - 不做码农的开发者(attachie_1),作者:高先生工作室

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-04-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【Java Spring Cloud 实战之路】- 使用Nacos和网关中心的创建

    Nacos可以用来发现、配置和管理微服务。提供了一组简单易用的特性集,可以快速实现动态服务发现、服务配置、服务元数据及流量管理。

    程序员小高
  • C# 基础知识系列- 13 常见类库介绍(二)日期时间类

    上一篇内容介绍了Console类和Math类,这篇内容着重介绍一下C#中时间日期的处理方式。

    程序员小高
  • 【asp.net core 系列】4. 更高更强的路由

    在之前我们介绍了请求通过路由寻找到控制器,以及控制器与视图的数据流转。那么,我们回过头来,再看看路由的一些其他用法。

    程序员小高
  • 基于Zmq的后台通信模型介绍

    Zmq是一个简单好用的传输组建,使得socket变成更加简洁、高效、高性能。本文主要介绍后台服务实现、多线程任务实现、线程无锁计数实现。

    airingzeng
  • Java并发编程之线程封闭

    当访问共享变量时,往往需要加锁来保证数据同步。一种避免使用同步的方式就是不共享数据。如果仅在单线程中访问数据,就不需要同步了。这种技术称为线程封闭。在Java语...

    技术训练营
  • java基础题目总结

    有些基础题目由于工作中用的比较少但却又是不可少的,这样回答起来就会反应慢,不确定,不准确,特此开了文章记录遇到的不确定或者回答比较拗口的问题。 1.servle...

    Ryan-Miao
  • 40个多线程问题总结

    源 / 架构师小秘圈 文 / 五月的仓颉 1、多线程有什么用? 一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?在我看来,这个回...

    顶级程序员
  • 【Java】基础30:线程与进程,并行与并发

    其实很好理解,第一个while循环是死循环,如果我们不强行将程序停止的话,它是会无止境的永远运行下去的,那么第二个死循环语句根本就没法运行到。

    刘小爱
  • 用java写一个死锁

    多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

    yaphetsfang
  • 猿思考系列2——一文搞懂同步并发套路

    看完上一个章节,相信你已经充分的理解java代码的执行套路了,猿人工厂君也知道,内容对于新手而言,理解起来还是很吃力的,不过上一章节涉及编译原理、类加载机制和一...

    山旮旯的胖子

扫码关注云+社区

领取腾讯云代金券