学编程,来源栈;先学习,再交钱
当前系列: C#语法 修改讲义
复习:J&C:多线程


Task

因为多个线程之间的协调调度是一件非常麻烦而且容易出错(error-prone)的,.NET已经不建议开发人员直接使用Thread,而是使用.NET 4.0之后引入的Task参考:托管线程处理的最佳做法

  • 从逻辑层面上讲,Task代表一份工作/任务,该工作/任务不保证同步的执行。
  • 从代码层面上讲,是一个实现了IAsyncResult的类,Task<T>继承了Task。(演示)

泛型和非泛型

Task要完成的任务由Action和Func体现,对应着两个Task类:

  • Task:使用Action,该Task没有返回 
  • Task<T>:使用Func,该Task还要返回一个T类型对象

要得到一个Task实例,必须指明相应的Action和Func。

最直观的方式是将Action或Func作为Task的构造函数参数传入:

    Action getup = () =>
    {
        Console.WriteLine("getUp()……");
    };
Task t1 = new Task(getup);

或者:

    Func<long> getup = () =>
    {
        Console.WriteLine("getUp()……");
        return DateTime.Now.Ticks;
    };
    Task<int> t1 = new Task<int>(getup);

得到一个Task之后,还需要显式调用Start()才开始运行:

t1.Start();

演示:t1中的内容并没有输出到控制台,@想一想:为什么?

Console.ReadLine();

异步运行

为了演示Task如何异步运行,我们在其前后加点代码,运行10次:
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine($"第{i + 1}次:");
        Console.WriteLine();

        Task t1 = new Task(getup);
        t1.Start();     //new出来的task还需要显式调用Start()才开始运行

        for (int j = 0; j < 5; j++)
        {
            Console.WriteLine($"after:t1.Start(): j={j}");
        }

        Console.WriteLine();
    }

演示运行结果:getup()和其后for循环代码不分先后的运行

@想一想@:如果Task需要的方法有参数,比如getup(name),怎么传递?

Action<int> getup = i =>
{
    Console.WriteLine($"getUp({i})……");
};
Task t1 = new Task(() =>
{
    getup(i);  //闭包
});

Task和Thread

注意新开一个Task并不总是新开一个Thread!(参考:What is the difference between task and thread?)

  • 从逻辑上讲,Task是比线程“更高层的抽象”,它是对任务(work)的封装,而不是线程(thread)的封装
  • 从实现上讲,异步并不一定需要一个线程。是否开启一个新线程,是由scheduler决定的,目前来说:
    • I/O相关异步,是不开新线程的,异步由底层I/O实现
    • CPU相关的线程,会利用TaskSchedular从线程池获取

线程池

如果Task需要一个线程,默认它会利用线程池来获取。

一个进程只有一个线程池,池中的线程都是:

  • 后台线程
    前/后台线程的区别:如果前台线程终止,后台线程也会被结束;反之不成立。
    之前的(主线程和工作线程)都是前台(foreground)线程。
    可以查看/改变线程的前后台状态:
    current.IsBackground = true;
  • 使用默认的priority:Normal
  • 使用相同的栈大小

可以用IsThreadPoolThread检查:

    Console.WriteLine(current.IsThreadPoolThread);  //是否线程池线程

TaskScheduler

而且.NET中还有一个专门的TaskSchedular负责线程池中线程的调度。

大体上来说,它利用以下一些方式提供其运行效率:

  • 使用队列,先进先出
  • 使用算法防止某些线程偷懒(work stealing)
  • 处理一些长运行(long-running)线程

绝大多数场景,我们使用.NET内置的调度就OK了。

其他属性方法

获得已经开始运行的Task实例:

Task t1 = Task.Run(getup);    //t1被创建的同时立即执行
Task t1 = Task.Factory.StartNew(getup);  //适合于精确控制task实例的生成

推荐顺序:Task.Run() >Task.Factory.StartNew() >new Task()

还有其他的一些方法,比如同步运行(不异步执行了!)

t1.RunSynchronously();
属性,比如状态、是否成功完成……
Console.WriteLine(t1.Status);
Console.WriteLine(t1.IsCompletedSuccessfully);

阻塞 vs 非阻塞

Task有两个实例方法,会等着task执行完成之后再继续运行:

  • Wait():用于Task
    t1.Wait();    //当前(主)线程等着t1完成之后才继续运行   
  • Result:用于Task<T>,会返回Task方法运行之后的结果
    Console.WriteLine("t1.Result:" + t1.Result);    //使用Result属性获得返回值

@想一想@:t1.Wait()和直接调用getup()有啥区别?

在Wait()或Result之前,还是存在异步执行的!

演示:getup()...和k={k}交替输出:

  • 给getup增加点输出:
    Func<long> getup = () =>
    {
        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine("getup()...");
        }
        return DateTime.Now.Ticks;
    };
  • 在t1运行启动之后还增加点输出:
    Task<long> t1 = Task.Run(getup);
    for (int k = 0; k < 5; k++)
    {
        Console.WriteLine($"k={k}");
    }
    Console.WriteLine(t1.Result);

这就叫做阻塞(block)的等待:当前线程停下来,等这个task完成,其他啥事不做。

Sleep() vs Delay()

Task有一个Delay()方法,看上去就和Thread.Sleep()类似,

Task.Delay(1000);    //Delay()获得的Task不能Start()   

但是,运行它的话就会发现:不对劲,没反应(演示)

因为它的延迟是非阻塞的,即不会阻止当前线程的执行。

如果要实现和Thread.Sleep()类似的效果,就得这样做:

Task t1 = Task.Delay(2000);  //非阻塞
//t1.Start();    不能再Start() t1.Wait();   //阻塞
Console.WriteLine("………………");


异步方法

.NET为我们提供了简洁优雅的异步方法,只需要两个关键字:

async 和 await

被async标记的方法被称为异步方法,

  • 但是,async不一定(没有强制力保证)异步。同步的方法一样可以标记async。async的作用只是:
  • 告诉编译器方法中可以出现await。如果只有async没有await,报编译时警告

只有await没有async,报编译错误。

static async void Process()
{
    await Task.Run(() => Console.WriteLine("async process..."));
}

await,可以理解为:异步(async)等待,后接 awaitable 实例。

我们可以简单的把awaitable理解成Task。

非阻塞等待

异步方法一直同步运行,直到 await。

从 await 开始异步(分叉):

  • 执行 awatable 中的内容,同时
  • 返回方法的调用处,执行其后内容
直到 awaitable 中内容执行完毕,才暂停方法调用处内容,继续执行await之后的代码。

异步方法执行完毕,继续方法调用后内容。

static async void Process()
{
    for (int i = 0; i < 5; i++)
    {
        Console.WriteLine($"before await {i} with thread ({Thread.CurrentThread.ManagedThreadId})");
    }
    await Task.Run(() =>   //开始同Process()方法后的代码异步:
    {
        Thread.Sleep(1);
        for (int k = 0; k < 5; k++)
        {
            Console.WriteLine($"async processing {k} with thread ({Thread.CurrentThread.ManagedThreadId})...");
        }
    });   //继续await后面的代码
    for (int j = 0; j < 5; j++)
    {
        Console.WriteLine($"after await {j} with thread ({Thread.CurrentThread.ManagedThreadId})");
    }
}
Process();
for (int i = 0; i < 5; i++)
{
    Thread.Sleep(1);
    Console.WriteLine($"after process() {i} with thread ({Thread.CurrentThread.ManagedThreadId})");
}

演示并体会:

  • before await和after await总是同步(有序)的
  • async processing和after process()异步(乱序)执行
  • async processing和after await使用同一线程(await采用的是Task的ContinueWith()机制

两种异步方法

返回 void 或 Task

异步方法中的 void 可以被直接替换成 Task(推荐),以便于该方法进一步的被 await 传递。

static async void OuterProcess()
{
    await Process();
}

static async Task Process()

void通常做为顶级(top-level)方法使用。

返回Task<T>

返回值被Task包裹,写成Task<T>,T指方法体内声明返回的类型

static async Task<long> Process()    //但方法声明的返回是Task<long> 
{
    return DateTime.Now.Ticks;    //return的是long

如果要获得当前时间的ticks,有两种办法:

  • Result:阻塞式
    Console.WriteLine(Process().Result);
  • await:非阻塞式
    long ticks = await Process();

@想一想@:这两种方式的区别?演示:略,以及:for循环在OuterProcess()方法中和方法调用后的区别:

static async void OuterProcess()
{
    Console.WriteLine(await Process());
    for (int i = 0; i < 5; i++)
    {
        Console.WriteLine($"after process() {i} with thread ({Thread.CurrentThread.ManagedThreadId})");
    }



TPL

任务并行库(Task Parallel Library),在System.Threading 和System.Threading.Tasks名称空间下。

简化异步/并行开发,在底层实现:

  • 动态调整并行规模
  • 处理分区
  • 线程(池)调度(器)等……

以下都是基于Task的并行

Parallel类

Invoke()方法

for (int i = 0; i < 5; i++)
{
    Console.WriteLine();
    Parallel.Invoke(
        () =>
        {
            Console.WriteLine(i + $":task-{Task.CurrentId} in thread-{Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine($"task-{Task.CurrentId} begin in thread-{Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine($"task-{Task.CurrentId} in thread-{Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine($"task-{Task.CurrentId} end in thread-{Thread.CurrentThread.ManagedThreadId}");
        },
        () =>
        {
            Console.WriteLine(i + $":task-{Task.CurrentId} in thread-{Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine($"task-{Task.CurrentId} in begin in thread-{Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine($"task-{Task.CurrentId} in thread-{Thread.CurrentThread.ManagedThreadId}");
            Console.WriteLine($"task-{Task.CurrentId} in end in thread-{Thread.CurrentThread.ManagedThreadId}");
        }
    );
}

演示:运行结果。

观察其规律:两个Action异步/并发运行。

循环

  • For():x的值为0到10的整数
    Parallel.For(0, 10, x => { Console.WriteLine(x); });
  • ForEach():Enumerable.Range(1, 10)生成1到10的IEnumerable
    Parallel.ForEach(Enumerable.Range(1,10), x => Console.WriteLine(x));

调度控制

可以使用Task的静态方法:

  • WaitAll / WaitAny:等待所有/任意一个Task结束,就可以继续之后的代码
  • WhenAll / WhenAny:返回一个Task,这个Task会在所有/任意一个Task结束后完成

需要引入线程数组Task[]作为参数

Task[] tasks =
{
    Task.Run(() => { Thread.Sleep(3); Console.WriteLine("1-洗脸");  }),
    Task.Run(() => { Thread.Sleep(2); Console.WriteLine("2-刷牙");  }),
    Task.Run(() => { Thread.Sleep(4); Console.WriteLine("3-吃早餐"); }),
    Task.Run(() => { Thread.Sleep(1); Console.WriteLine("4-背单词"); })
};

演示:

  • Wait方法是阻塞式的:
  • When方法是非阻塞的,要想实现和Wait一样的效果,需要:
    Task.WhenAll(tasks).Wait();

@想一想@:When方法有啥用呢?放在async方法中哟!

await Task.WhenAny(tasks);
Console.WriteLine("after tasks……");


作业

利用代码演示:
  1. Task和Thread的区别
  2. Wait()和await的区别

其他,见:J&C:多线程:current / 属性状态 / 异步和并发 / 异常捕获 / 线程安全 / join / 锁 / 池


学习笔记
源栈学历
今天学习不努力,明天努力找工作

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 C#语法 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码