c#中线程和异步编程

目录

    • 一 thread
    • 二 task
      • 1 什么是task
      • 2 两种创建task的模式
      • 3 task的生命周期
      • 4 task的控制
      • 5 连续任务
      • 6 task的取消
      • 7 task的嵌套
      • 8 任务执行的结果
      • 9 task死锁的问题
      • 10 对Spinlock的使用
    • 三 Thread & Task比较
      • 5 IsBackground作用
    • 四 threadpoll和task的结构图
    • 五 Async/Await
      • 1 示例
      • 2 async/await 结构
      • 3 What’s 异步方法
      • 4 返回类型
    • 参考文章

一 thread

  当我们提及多线程的时候会想到thread和threadpool,这都是异步操作,threadpool其实就是thread的集合,具有很多优势,不过在任务多的时候全局队列会存在竞争而消耗资源。thread默认为前台线程,主程序必须等线程跑完才会关闭,而threadpool相反。
  总结:threadpool确实比thread性能优,但是两者都没有很好的api区控制,如果线程执行无响应就只能等待结束,从而诞生了task任务。

二 task

1 什么是task

  .NET 4.0推出了新一代的多线程模型Task,task简单地看就是任务,那和thread有什么区别呢?Task的背后的实现也是使用了线程池线程,但它的性能优于ThreadPoll,因为它使用的不是线程池的全局队列,而是使用的本地队列,使线程之间的资源竞争减少。同时Task提供了丰富的API来管理线程、控制。但是相对前面的两种耗内存,Task依赖于CPU,对于多核的CPU性能远超前两者,单核的CPU三者的性能没什么差别。

2 两种创建task的模式

  创建一个task任务有两种模式:使用factory创建会直接执行,使用new创建不会执行,必须等到start启动之后才执行。

class Program
{
    static void DownLoad(object str)
    {
        Console.WriteLine("DownLoad Begin ID = " + Thread.CurrentThread.ManagedThreadId + " " + str);
        Thread.Sleep(1000);
        Console.WriteLine("DownLoad End");
    }
    static void Main(string[] args)
    {
    
        //创建任务
        //Task task = new Task(DownLoad, "人民日报");
        //启动任务
        //task.Start();
        
		//创建任务工厂
        TaskFactory taskFactory = new TaskFactory();
        //开始新的任务
        taskFactory.StartNew(DownLoad, "纽约时报");
        
        Console.WriteLine("Main");   
        Console.ReadKey();
    }
}

3 task的生命周期

var testTask = new Task(() =>
{
    Console.WriteLine("task start");
    System.Threading.Thread.Sleep(2000);
});
Console.WriteLine(testTask.Status);
testTask.Start();
Console.WriteLine(testTask.Status);
Console.WriteLine(testTask.Status);         
testTask.Wait();
Console.WriteLine(testTask.Status);
Console.WriteLine(testTask.Status);
/*
输出结果:
Created
task start
Running
Running
RanToCompletion
RanToCompletion
*/

  可以看出task确实是异步执行,并且wait很好地控制了task。

4 task的控制

var testTask = new Task(() =>
    {
        Console.WriteLine("task start");
        System.Threading.Thread.Sleep(2000);
    });
testTask.Start();
testTask.Wait();
var testTask = new Task(() =>
    {
       Console.WriteLine("task start");
       System.Threading.Thread.Sleep(2000);
    });
testTask.Start();
var factoryTeak = Task.Factory.StartNew(() =>
    {                 
        Console.WriteLine("factory task start");
     });
Task.WaitAll(testTask, factoryTeak);
Console.WriteLine("end");
var testTask = new Task(() =>
{
    Console.WriteLine("task start");
    System.Threading.Thread.Sleep(2000);
});
testTask.Start();
var factoryTeak = Task.Factory.StartNew(() =>
{                  
    Console.WriteLine("factory task start");
});
Task.WaitAny(testTask, factoryTeak);
Console.WriteLine("end");

  通过wait()对单个task进行等待,Task.waitall()对多个task进行等待,waitany()执行任意一个task就往下继续执行。

5 连续任务

var testTask = new Task(() =>
{
    Console.WriteLine("task start");
    System.Threading.Thread.Sleep(2000);
});
testTask.Start();
var resultTest = testTask.ContinueWith<string>((Task) =>
{
    Console.WriteLine("testTask end");
    return "end";
});
Console.WriteLine(resultTest.Result);

6 task的取消

  首先创建一个取消task的令牌的实例,在不启动task直接取消:

var tokenSource = new CancellationTokenSource();//创建取消task实例
var testTask = new Task(() =>
{
    for (int i = 0; i < 6; i++)
    {
        System.Threading.Thread.Sleep(1000);
    }
},tokenSource.Token);
Console.WriteLine(testTask.Status);
tokenSource.Token.Register(()=>
{
    Console.WriteLine("task is to cancel");
});
tokenSource.Cancel();
Console.WriteLine(testTask.Status);
//输出结果:
/*
Created
task is to cancel
Canceled
*/

  如果task启动了真的取消了task?

CancellationTokenSource tokenSource = new CancellationTokenSource();//创建取消task实例
Task testTask = new Task(() =>
{
    for (int i = 0; i < 11; i++)
    {
        Console.WriteLine("fdsaf");
        Thread.Sleep(1000);
    }
}, tokenSource.Token);
Console.WriteLine(testTask.Status);
testTask.Start();
Console.WriteLine(testTask.Status);
tokenSource.Token.Register(() =>
{
    Console.WriteLine("task is to cancel");
});
Thread.Sleep(6000);
tokenSource.Cancel();
Console.WriteLine(testTask.Status);
for (int i = 0; i < 20; i++)
{
    Thread.Sleep(1000);
    Console.WriteLine(testTask.Status);
}
/*
输出结果:
Created
WaitingToRun
task is to cancel
Running
Running
Running
Running
Running
Running
RanToCompletion
RanToCompletion
RanToCompletion
RanToCompletion
RanToCompletion
*/

  可以看出其实并没有取消task,此时task还在继续跑。
  当我们调用了Cancel()方法之后,.NET Framework不会强制性的去关闭运行的Task。
  我们自己必须去检测之前在创建Task时候传入的那个CancellationToken。
  我们在创建Task是传入CancellationToken到构造函数,其实这个CancellationToken就是.NET Framework用来避免我们再次运行已经被取消的Task,可以说就是一个标志位。
  对于线程的取消,查看这里。

7 task的嵌套

  在一个任务中可以启动子任务,两个任务异步执行。默认情况下,子任务(即由外部任务创建的内部任务)将独立于其父任务执行。使用TaskCreationOptions.AttachedToParent显式指定将任务附加到任务层次结构中的某个父级。

var parentTask = new Task(()=>
{
    var childTask = new Task(() =>
    {
        System.Threading.Thread.Sleep(2000);
        Console.WriteLine("childTask to start");
    });
    childTask.Start();
    Console.WriteLine("parentTask to start");
});
parentTask.Start();
parentTask.Wait();
Console.WriteLine("end");

  此时为普通关联,父task和子task没影响

var parentTask = new Task(()=>
{
	var childTask = new Task(() =>{
	    System.Threading.Thread.Sleep(2000);
	    Console.WriteLine("childTask to start");
	}, TaskCreationOptions.AttachedToParent);
	childTask.Start();
	Console.WriteLine("parentTask to start");
} );
parentTask.Start();
parentTask.Wait();
Console.WriteLine("end");

  此时为父task和子task关联,wait会一直等待父子task执行完。
  如果父任务执行完了但是子任务没有执行完,则父任务的状态会被设置为WaitingForChildrenToComplete,只有子任务也执行完了,父任务的状态才会变成RunToCompletion

8 任务执行的结果

  使用Task的泛型版本,可以返回任务的执行结果。
  下面例子中的TaskWithResult的输入为object类型,返回一个元组Tuple
  定义调用TaskWithResult的任务时,使用泛型类Task>,泛型的参数定义了返回类型。通过构造函数,传递TaskWithResult,构造函数的第二个参数定义了TaskWithResult的输入值。
  任务完成后,通过Result属性获取任务的结果。

class Program
{
    static Tuple<int, int> TaskWithResult(object obj)
    {
        Tuple<int, int> div = (Tuple<int, int>)obj;
        Thread.Sleep(1000);
        return Tuple.Create<int, int>(div.Item1 + div.Item2, div.Item1 - div.Item2);
    }
    static void Main(string[] args)
    {
        var task = new Task<Tuple<int, int>>(TaskWithResult, Tuple.Create<int, int>(8, 3));            
        task.Start();
        Console.WriteLine(task.Result);
        task.Wait();
        Console.WriteLine("Result: {0} {1}", task.Result.Item1, task.Result.Item2);
        Console.ReadLine();
    }
}
/*
执行结果
(11, 5)
result:11 5
*/

9 task死锁的问题

  我们可以设置最大等待时间,如果超过了等待时间,就不再等待,下面我们来修改代码,设置最大等待时间为5秒(项目中可以根据实际情况设置),如果超过5秒就输出哪个任务出错了。

10 对Spinlock的使用

  举例来说Parallel.for和Parallel.foreach是线程不安全的,有可能达不到你的预期,此时就需要加锁来解决此问题,我们可以加lock和spinlock(自旋锁)来解决。

SpinLock slock = new SpinLock(false);
var testLock= new object();
long sum1 = 0;
long sum2 = 0;
long sum3 = 0;
Parallel.For(0, 100000, i =>
{
    sum1 += i;
});

Parallel.For(0, 100000, i =>
{
    bool lockTaken = false;
    try
    {
        slock.Enter(ref lockTaken);
        sum2 += i;
    }
    finally
    {
        if (lockTaken)
            slock.Exit(false);
    }
});
Parallel.For(0, 100000, i =>
{
    lock(testLock)
    {
        sum3 += i;
    };
});
Console.WriteLine("Num1的值为:{0}", sum1);
Console.WriteLine("Num2的值为:{0}", sum2);
Console.WriteLine("Num3的值为:{0}", sum3);
/*            
输出结果:
Num1的值为:1660913202
Num2的值为:4999950000
Num3的值为:4999950000

Num1的值为:2754493646
Num2的值为:4999950000
Num3的值为:4999950000

Num1的值为:4999950000
Num2的值为:4999950000
Num3的值为:4999950000
*/

三 Thread & Task比较

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace DemoAsync{
class Program{
    static void Main(string[] args)
    {
        Console.WriteLine("Task   With Thread  Start !");
        for (int i = 0; i <= 5; i++)
        {
            Thread t = new Thread(Dotaskfunction);
            t.Start();
        }
        Console.WriteLine("Task   With Thread End !");
        Console.WriteLine("Task   With Task   Start !");
        for (int i = 0; i <= 5; i++)
        {
            Task.Run(() => { Dotaskfunction(); });
        }
        Console.WriteLine("Task   With Task End !");
        Console.ReadLine();
    }
    public static void Dotaskfunction()
    {
        Console.WriteLine("task  has been done! ThreadID: {0},IsBackGround:{1} ", Thread.CurrentThread.ManagedThreadId,Thread.CurrentThread .IsBackground );
    }
}
}

c#中线程和异步编程_第1张图片
  可以看到Thread方法每次的Thread Id都是不同的,而Task方法的Thread Id是重复出现的。我们知道线程的创建和销毁是一个开销比较大的操作,Task每次执行将不会立即创建一个新线程,而是到CLR线程池查看是 否有空闲的线程,有的话就取一个线程处理这个请求,处理完请求后再把线程放回线程池,这个线程也不会立即撤销,而是设置为空闲状态,可供线程池再次调度, 从而减少开销。

5 IsBackground作用

要点:
1、当在主线程中创建了一个线程,那么该线程的IsBackground默认是设置为FALSE的。
2、当主线程退出的时候,IsBackground=FALSE的线程还会继续执行下去,直到线程执行结束。
3、只有IsBackground=TRUE的线程才会随着主线程的退出而退出。
4、当初始化一个线程,把Thread.IsBackground=true的时候,指示该线程为后台线程。后台线程将会随着主线程的退出而退出。
5、原理:只要所有前台线程都终止后,CLR就会对每一个活在的后台线程调用Abort()来彻底终止应用程序。
Net的公用语言运行时(Common Language Runtime,CLR)能区分两种不同类型的线程:前台线程和后台线程。这两者的区别就是:应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
既然前台线程和后台线程有这种差别,那么我们怎么知道该如何设置一个线程的IsBackground属性呢?下面是一些基本的原则:对于一些在后台运行的线程,当程序结束时这些线程没有必要继续运行了,那么这些线程就应该设置为后台线程。比如一个程序启动了一个进行大量运算的线程,可是只要程序一旦结束,那个线程就失去了继续存在的意义,那么那个线程就该是作为后台线程的。而对于一些服务于用户界面的线程往往是要设置为前台线程的,因为即使程序的主线程结束了,其他的用户界面的线程很可能要继续存在来显示相关的信息,所以不能立即终止它们。这里我只是给出了一些原则,具体到实际的运用往往需要编程者的进一步仔细斟酌。
一般后台线程用于处理时间较短的任务,如在一个Web服务器中可以利用后台线程来处理客户端发过来的请求信息。而前台线程一般用于处理需要长时间等待的任务,如在Web服务器中的监听客户端请求的程序,或是定时对某些系统资源进行扫描的程序。

四 threadpoll和task的结构图

  threadpool:
在这里插入图片描述
   task:
在这里插入图片描述

五 Async/Await

  本节内容来自这里。
  async/await特性是与Task紧密相关的,所以在了解async/await前必须充分了解Task的使用。

1 示例

c#中线程和异步编程_第2张图片
c#中线程和异步编程_第3张图片
  1、从 Main 方法执行到CountCharactersAsync(1, url1)方法时,该方法会立即返回,然后才会调用它内部的方法开始下载内容。该方法返回的是一个Task类型的占位符对象,表示计划进行的工作。这个占位符最终会返回 int 类型的值。
  2、这样就可以不必等CountCharactersAsync(1, url1)方法执行完成就可以继续进行下一步操作。到执行CountCharactersAsync(2, url2)方法时,一样返回Task对象。
  3、然后,Main方法继续执行三次ExtraOperation方法,同时两次 CountCharactersAsync方法依然在持续工作 。
  4、t1.Resultt2.Result是指从CountCharactersAsync方法调用的Task对象取结果,如果还没有结果的话,将阻塞,直有结果返回为止。

2 async/await 结构

  async/await 结构可分成三部分:
  (1)调用方法:该方法调用异步方法,然后在异步方法执行其任务的时候继续执行;
  (2)异步方法:该方法异步执行工作,然后立刻返回到调用方法;
  (3)await 表达式:用于异步方法内部,指出需要异步执行的任务。一个异步方法可以包含多个 await 表达式(不存在 await 表达式的话 IDE 会发出警告)。

3 What’s 异步方法

  异步方法:在执行完成前立即返回调用方法,在调用方法继续执行的过程中完成任务。
  语法分析:
  (1)关键字:方法头使用async修饰。
  (2)要求:包含 N(N>0) 个await表达式(不存在await表达式的话 IDE 会发出警告),表示需要异步执行的任务,没有的话,就和普通方法一样执行了。
  (3)返回类型:只能返回 3 种类型(voidTaskTask\)。TaskTask\标识返回的对象会在将来完成工作,表示调用方法和异步方法可以继续执行。
  (4)参数:数量不限,但不能使用 out 和 ref 关键字。
  (5)命名约定:方法后缀名应以 Async 结尾。
  (6)其它:匿名方法和 Lambda 表达式也可以作为异步对象;async 是一个上下文关键字;关键字 async 必须在返回类型前。
  关于 async 关键字:
  1、在返回类型之前包含 async 关键字;
  2、它只是标识该方法包含一个或多个 await 表达式,即,它本身不创建异步操作;
  3、它是上下文关键字,即可作为变量名。

4 返回类型

  1、Task:调用方法要从调用中获取一个 T 类型的值,异步方法的返回类型就必须是Task。调用方法从 Task 的 Result 属性获取的就是 T 类型的值。

internal class Calculator
{
   private static int Add(int n, int m)
   {
       return n + m;
   }

   public static async Task<int> AddAsync(int n, int m)
   {
       int val = await Task.Run(() => Add(n, m));
       return val;
   }
}
private static void Main(string[] args)
{
    Task<int> t = Calculator.AddAsync(1, 2);
    //一直在干活
    Console.WriteLine($"result: {t.Result}");
    Console.Read();
}

  2、Task:调用方法不需要从异步方法中取返回值,但是希望检查异步方法的状态,那么可以选择可以返回 Task 类型的对象。不过,就算异步方法中包含 return 语句,也不会返回任何东西。

internal class Calculator
{
   private static int Add(int n, int m)
   {
       return n + m;
   }

   public static async Task AddAsync(int n, int m)
   {
       int val = await Task.Run(() => Add(n, m));
       Console.WriteLine($"Result: {val}");
   }
}
private static void Main(string[] args)
{
    Task t = Calculator.AddAsync(1, 2);

    //一直在干活

   t.Wait();
   Console.WriteLine("AddAsync 方法执行完成");

   Console.Read();
}

  (3)void:调用方法执行异步方法,但又不需要做进一步的交互。

internal class Calculator
{
   private static int Add(int n, int m)
    {
        return n + m;
    }
    public static async void AddAsync(int n, int m)
    {
        int val = await Task.Run(() => Add(n, m));
        Console.WriteLine($"Result: {val}");
    }
}
private static void Main(string[] args)
{
    Calculator.AddAsync(1, 2);
    //一直在干活
    Thread.Sleep(1000); //挂起1秒钟
    Console.WriteLine("AddAsync 方法执行完成");
    Console.Read();
}

参考文章

  c#之task与thread区别及其使用:https://blog.csdn.net/qq_40677590/article/details/102797838
  c#中任务Task:https://blog.csdn.net/liyazhen2011/article/details/81262582
  走进异步编程的世界 - 剖析异步方法(上):https://www.cnblogs.com/liqingwen/p/5844095.html
  走进异步编程的世界 - 剖析异步方法(下):cnblogs.com/liqingwen/p/5866241.html
  

你可能感兴趣的:(#,.NET,Core)