一、概念
在C#中,Task是一种异步编程模型,它允许您在调用方法时不会阻塞线程,而是将工作放入队列中,允许该线程继续执行其他操作。这种模型在处理I/O、网络请求、数据库操作等操作时非常有用,可以提高应用程序的性能和响应能力。Task是.NET框架的一部分,可以轻松使用,同时它也提供了许多与线程相关的API,例如同步、等待、取消、报告进度等。
二、功能使用
我这里定义了一个一会可供多线程调用的方法
public class TaskClass2
{
public static void A()
{
Thread.Sleep(2000);
Console.WriteLine("我是TaskClass2类的A方法d等待两秒钟后的输出:当前线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}
}
1、开启线程
在c#中, Task开启线程有三种方法
(1) 实例化 Task 然后通过 task.start(); 开启线程
(2) 直接Task.Run 新跑一个线程
(3) 创建一个线程对象Task.Factory,使用的时候通过 .StartNew(委托: 方法参数)调用
我这里写了一个接口,分别用三种不认同的方式开启了线程,打印了不同的数据
/// <summary>
/// 开启线程
/// </summary>
/// <returns></returns>
[HttpGet("StartTask")]
public string StartTask()
{
Console.WriteLine("主线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
//第一种方法: 实例化 Task 然后通过 task.start(); 开启线程
//默认Task实例 需要传递一个委托: 方法参数
Task task = new Task(TaskClass2.A);
task.Start();
//第二种方法: 直接Task.Run 新跑一个线程
Task.Run(() =>
{
TaskClass2.A();
});
//第三种方法: 创建一个线程对象Task.Factory,使用的时候通过 .StartNew(委托: 方法参数)调用
TaskFactory taskFactory = Task.Factory;
Task taskf = taskFactory.StartNew(TaskClass2.A);
Console.WriteLine("主线程执行完毕");
return "value";
}
如图,可以看到这三个方式开启的线程,打印了不同的线程id。
2、等待线程 Thread.Sleep()
Thread.Sleep()等待方法,在哪个线程调用,就会阻塞哪个线程进行等待
例: A线程中嵌套了一个B线程 在B中Thread.Sleep() 只会阻塞B 并不会影响A的运行,反之也是
/// <summary>
/// 等待线程
/// Thread.Sleep()等待方法,在哪个线程调用,就会阻塞哪个线程进行等待
/// 例: A线程中嵌套了一个B线程 在B中Thread.Sleep() 只会阻塞B 并不会影响A的运行,反之也是
/// </summary>
/// <returns></returns>
[HttpGet("SleepTask")]
public string SleepTask()
{
Console.WriteLine("当前时间为:" + DateTime.Now);
Thread.Sleep(10000);
Console.WriteLine("等待10秒钟后的当前时间为:" + DateTime.Now);
return "value";
}
如图可以看到,我们成功阻塞了线程,让他等待了我们指定的时间 再进行后续代码的执行。
3、线程回调
(1)异步单线程回调 ContinueWith
异步线程执行完之后,执行需要的方法
/// <summary>
/// 异步单线程回调
/// 异步线程执行完之后,执行需要的方法
/// </summary>
/// <returns></returns>
[HttpGet("ContinueWithTask")]
public string ContinueWithTask()
{
Console.WriteLine("主线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
//单线程回调
Task.Run(TaskClass2.A).ContinueWith(t =>
{
Console.WriteLine($"我是TaskClass2.A方法执行完毕之后的单线程回调函数,当前id为: {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
});
Console.WriteLine("主线程执行完毕");
return "value";
}
如图,这是异步单线程回调结果。在执行完A方法之后,自动执行一遍我们设置的回调函数。
(2)异步多线程回调 ContinueWhenAny
只完成其中一个执行回调
/// <summary>
/// 异步多线程回调
/// 只完成其中一个执行回调
/// </summary>
/// <returns></returns>
[HttpGet("ContinueWhenAnyTask")]
public string ContinueWhenAnyTask()
{
Console.WriteLine("主线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
//多线程,执行回调
TaskFactory taskFactory = new TaskFactory();
List<Task> taskList = new List<Task>();
taskList.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(1000);
Console.WriteLine("我是同时开启的线程1,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
taskList.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(2000);
Console.WriteLine("我是同时开启的线程2,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
taskList.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(3000);
Console.WriteLine("我是同时开启的线程3,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
taskList.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(4000);
Console.WriteLine("我是同时开启的线程4,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
//多线程,只完成其中一个执行回调
taskFactory.ContinueWhenAny(taskList.ToArray(), t =>
{
Console.WriteLine("我是多线程,只完成其中一个执行的回调");
});
Console.WriteLine("主线程执行完毕");
return "value";
}
如图,在执行完多个线程中的其中一个,就执行了回调函数。
(3)异步多线程回调 ContinueWhenAll
全部完成再执行回调
/// <summary>
/// 异步多线程回调
/// 全部完成再执行回调
/// </summary>
/// <returns></returns>
[HttpGet("ContinueWhenAllTask")]
public string ContinueWhenAllTask()
{
Console.WriteLine("主线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
//多线程,执行回调
TaskFactory taskFactory = new TaskFactory();
List<Task> taskList = new List<Task>();
taskList.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(1000);
Console.WriteLine("我是同时开启的线程1,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
taskList.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(2000);
Console.WriteLine("我是同时开启的线程2,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
taskList.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(3000);
Console.WriteLine("我是同时开启的线程3,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
taskList.Add(taskFactory.StartNew(() =>
{
Thread.Sleep(4000);
Console.WriteLine("我是同时开启的线程4,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
//多线程,完成全部执行回调
taskFactory.ContinueWhenAll(taskList.ToArray(), t =>
{
Console.WriteLine("我是多线程,完成全部执行的回调");
});
Console.WriteLine("主线程执行完毕");
return "value";
}
如图,该回调函数会等在所有的线程执行完之后再调用。
4、阻塞当前线程,
(1)等着任意一个任务完成 WaitAny
/// <summary>
/// 阻塞当前线程,等着任意一个任务完成
/// </summary>
/// <returns></returns>
[HttpGet("WaitAnyTask")]
public string WaitAnyTask()
{
Console.WriteLine("主线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
List<Task> taskList = new List<Task>();
taskList.Add(Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine("我是同时开启的线程1,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
taskList.Add(Task.Run(() =>
{
Thread.Sleep(2000);
Console.WriteLine("我是同时开启的线程2,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
taskList.Add(Task.Run(() =>
{
Thread.Sleep(3000);
Console.WriteLine("我是同时开启的线程3,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
taskList.Add(Task.Run(() =>
{
Thread.Sleep(4000);
Console.WriteLine("我是同时开启的线程4,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
//阻塞当前线程,等着任意一个任务完成
Task.WaitAny(taskList.ToArray());
Console.WriteLine("主线程执行完毕");
return "value";
}
如图,主线程会等待所有子线程中其中一个完成,再继续往下执行主线程。
(2)等待全部线程执行完成,继续执行 WaitAll
/// <summary>
/// 阻塞当前线程,等待全部线程执行完成,继续执行
/// </summary>
/// <returns></returns>
[HttpGet("WaitAllTask")]
public string WaitAllTask()
{
Console.WriteLine("主线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
List<Task> taskList = new List<Task>();
taskList.Add(Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine("我是同时开启的线程1,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
taskList.Add(Task.Run(() =>
{
Thread.Sleep(2000);
Console.WriteLine("我是同时开启的线程2,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
taskList.Add(Task.Run(() =>
{
Thread.Sleep(3000);
Console.WriteLine("我是同时开启的线程3,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
taskList.Add(Task.Run(() =>
{
Thread.Sleep(4000);
Console.WriteLine("我是同时开启的线程4,线程id为: " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
//阻塞当前线程,等待全部线程执行完成
Task.WaitAll(taskList.ToArray());
Console.WriteLine("主线程执行完毕");
return "value";
}
如图,主线程会等待所有子线程执行完毕之后,在执行主线程代码。
5、控制线程并发数
在工作中,经常会遇到控制并发线程数量在多少以内,防止太多线程紊乱问题。
我们在这里建设5个并发数。
/// <summary>
/// 控制线程并发数量
/// 假设5个
/// </summary>
/// <returns></returns>
[HttpGet("ControlTaskCount")]
public string ControlTaskCount()
{
List<Task> taskList = new List<Task>();
for (int i = 0; i < 100; i++)
{
//临时变量问题,线程是非阻塞的,延迟启动的;线程执行的时候,i已经是5了
//k是闭包里面的变量,每次循环都有一个独立的k
//5个k变量 1个i变量
int k = i;
//在集合中找未执行成功的线程个数
if (taskList.Count(t => t.Status != TaskStatus.RanToCompletion) >= 5)
{
//等待这五个线程中其中一个完成
Task.WaitAny(taskList.ToArray());
Console.WriteLine();
//筛除已经执行成功的线程
taskList = taskList.Where(t => t.Status != TaskStatus.RanToCompletion).ToList();
}
//向list线程列表添加新的线程
taskList.Add(Task.Run(() =>
{
Console.WriteLine($"我是第{k}个线程, 线程id为:{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
}));
}
return "value";
}
如图,可以看到他是按照我们需求5个线程并发数执行的。
6、多线程异常处理
工作中常规建议:多线程的委托里面不允许异常,包一层try-catch,然后记录下来异常信息,完成需要的操作
/// <summary>
/// 多线程异常处理
/// 工作中常规建议:多线程的委托里面不允许异常,包一层try-catch,然后记录下来异常信息,完成需要的操作
/// </summary>
/// <returns></returns>
[HttpGet("ExceptionTask")]
public string ExceptionTask()
{
Console.WriteLine("主线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
try
{
List<Task> taskList = new List<Task>();
for (int i = 0; i < 20; i++)
{
int k = i;
taskList.Add(Task.Run(() =>
{
if (k == 10 || k == 11)
{
throw new Exception("代码异常");
}
Console.WriteLine($"我是子线程{k},线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}));
}
//多线程里面抛出的异常,会终结当前线程;但是不会影响别的线程;
//捕获所有线程中报错的线程,到catch中打印
Task.WaitAll(taskList.ToArray());
}
catch (AggregateException aex)//Task.WaitAll捕获发生的异常
{
foreach (var exception in aex.InnerExceptions)//当前异常的所有实例
{
Console.WriteLine(exception.Message);
}
}
catch (Exception ex)
{
Console.WriteLine("代码异常:" + ex.Message);
}
Console.WriteLine("主线程执行完毕");
return "value";
}
如图,多线程里面抛出的异常,会终结当前线程,但是不会影响别的线程,并没有输出我们的线程id,并且我们也成功的通过/Task.WaitAll捕获到我们假设的10和11的异常线程,并通过catch处理打印了出来。
7、线程取消
例:一共十个线程,运行到第三个报错了,需要通知其余七个停止运行
/// <summary>
/// 线程取消
/// 例:一共十个线程,运行到第三个报错了,需要通知其余七个停止运行
/// </summary>
/// <returns></returns>
[HttpGet("CancelTask")]
public string CancelTask()
{
Console.WriteLine("主线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
//解决方法:
//1、 Thread.Abort--终止线程;向当前线程抛一个异常然后终结任务;线程属于OS资源,可能不会立即停下来
//2、 Task不能外部终止任务,只能自己终止自己(上帝才能打败自己)
//3、cts有个bool属性IsCancellationRequested 初始化是false,调用Cancel方法后变成true(不能再变回去),可以重复cancel
try
{
//代表线程被取消的实例
CancellationTokenSource cts = new CancellationTokenSource();
List<Task> taskList = new List<Task>();
for (int i = 0; i < 20; i++)
{
try
{
int k = i;
taskList.Add(Task.Run(() =>
{
//验证是否设置取消了
if (!cts.IsCancellationRequested)
{
//假设遇到10 11 会代码异常 则给他设置cts.Cancel(); 取消
if (k == 10)
{
throw new Exception("代码异常");
}
if (k == 11)
{
cts.Cancel();
}
Console.WriteLine($"我是子线程{k},线程id; " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
}
else
{
Console.WriteLine($"线程已经被取消");
}
}, cts.Token));
}
catch (Exception)
{
//代码发生异常
//设置取消线程
cts.Cancel();
}
}
//1 准备cts 2 try-catch-cancel 3 Action要随时判断IsCancellationRequested
//尽快停止,肯定有延迟,在判断环节才会结束
Task.WaitAll(taskList.ToArray());
//如果线程还没启动,能不能就别启动了?
//1 启动线程传递Token 2 异常抓取
//在Cancel时还没有启动的任务,就不启动了;也是抛异常,cts.Token.ThrowIfCancellationRequested
}
catch (AggregateException aex)
{
foreach (var exception in aex.InnerExceptions)
{
Console.WriteLine("取消线程:" + exception.Message);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
Console.WriteLine("主线程执行完毕");
return "value";
}
注意: 在开启线程的时候Run,要把cts.Token线程取消器的Token传递进去,把线程绑定到取消器上,这样如果取消器设置取消,那么绑定的所有线程都会被取消。
如图,我们通过CancellationTokenSource线程取消器的Cancel事件设置取消状态,使用IsCancellationRequested来判断是否取消并打印我们取消信息。
8、取消调用回调函数 TaskContinuationOptions.OnlyOnRanToCompletion
在项目中,经常会遇到用一个线程去执行业务A,然后在这个线程执行完之后调用回调函数,执行另一个业务B,那么如果业务A在执行过程中发生异常报错,就不应再在执行业务B了,此时就需要用到 TaskContinuationOptions.OnlyOnRanToCompletion,验证相对回调函数的主线程是否发生异常。
/// <summary>
/// 判断多线程报错,不执行回调函数
/// </summary>
/// <returns></returns>
[HttpGet("ExceptionContinueWithTask")]
public string ExceptionContinueWithTask()
{
Task.Run(() =>
{
Student student = null;
if (student.Id == "1")
{
Console.WriteLine("123312");
}
}).ContinueWith(s=>
{
if (s.Exception == null || s.Exception.InnerExceptions.Count == 0)
{
Console.WriteLine(1312);
}
}, TaskContinuationOptions.OnlyOnRanToCompletion);
return "value";
}
如图,发生代码异常的输出中没有输出回调函数输出内容,而不会发生代码异常的片段中输出了回调函数内容,达到了我们预期的效果。
今天的异步多线程Task的分享就到这里,大家还有更换的管用多线程的使用,欢迎到评论区讨论!!!