一 简介:
多线程的原理我就不讲了,做开发处处用到。C#也提供了几种多线程的使用方式,分别是Thread类、ThreadPool类、Parallel 类和Task类,这里只讲Task,其他的知道有就可以了,就像当初做iOS开发的时候,也把苹果原生的几种线程都学了,也做了博客记录,但是在实际开发当中这么多年却只用了GCD这一种方式来处理,问就是好用、性能高。就像人吃饭一样,有肉不吃干嘛非要去喝汤,对吧?
二 Task介绍
Task 是 .NET 中用于表示异步操作的类,出现在C#4.0时代,可以简单看作相当于Thead+TheadPool,其性能比直接使用Thread要好太多,它可以适用于很多种场景和功能。
比如:
- 异步执行代码:Task 允许在单独的线程上执行代码块,从而避免阻塞主线程,提高程序的响应性和并发性。
- 等待异步操作完成:通过 await 关键字可以等待 Task 完成,从而实现非阻塞的异步操作。
- 并行执行多个任务:Task 可以用于并行执行多个独立的任务,从而加快任务的完成速度,提高系统的性能。
- 处理异常:Task 提供了异常处理机制,可以在异步操作中捕获和处理异常。
- 轻量级的线程管理:Task 使用线程池来管理线程,避免了频繁创建和销毁线程的开销,使得线程的管理更加高效。
- 取消异步操作:Task 支持取消操作,可以通过 CancellationToken 来取消正在进行的异步操作。
总之,Task在多线程开发中起着非常重要的作用,一定要学好并用好。
三 Task开启方式
- Task对象的Start()方法
Console.WriteLine("当前线程1:{0}", Thread.CurrentThread.ManagedThreadId);
Task task1 = new Task(() =>// 无返回值
{
Console.WriteLine("当前线程2:{0}", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(2000);
});
task1.Start();
Console.WriteLine("主线程不受影响");
Task<string> task2 = new Task<string>(() => {// 有返回值string类型
return "task2";
});
task2.Start();
string str = task2.Result;
Console.WriteLine(str);
- Task的静态方法(函数)Run()
Task.Run(() =>
{
Console.WriteLine("这里是子线程喽");
});
Task<string> task = Task.Run<string>(() =>
{
return"这里是子线程喽";
});
string str = task.Result;
Console.WriteLine($"{str}");
- Task工厂
TaskFactory factory = Task.Factory;
factory.StartNew(() =>
{
Console.WriteLine("你好 task");
});
Task<string> task = factory.StartNew<string>(() =>
{
return "你好 task";
});
string str = task.Result;
Console.WriteLine($"{str}");
四 Task线程等待(阻塞线程)
- Wait()
Task task1 = Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine("我排第二");
});
Console.WriteLine("我先打印");
task1.Wait(2000); //等待2000毫秒后再往下走 如果写task1.Wait()就是必须要等task1子线程走完才能往下走(那我有必要开子线程嘛)
Task task2 = Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine("我垫底了嘛");
});
task2.Wait(500);
Console.WriteLine("我当老三");
- WaitAll()
Console.WriteLine("来任务咯");
Task task1 = Task.Run(() => {
Thread.Sleep(1000);
Console.WriteLine("我耗时1秒");
});
Task task2 = Task.Run(() => {
Thread.Sleep(2000);
Console.WriteLine("我耗时2秒");
});
Task.WaitAll(task1, task2);// 等待task1和task2任务全走完才能往后走
Console.WriteLine("没事,我等你们走完我再走");
- WaitAny()
Console.WriteLine("来任务咯");
Task task1 = Task.Run(() => {
Thread.Sleep(1000);
Console.WriteLine("我耗时1秒");
});
Task task2 = Task.Run(() => {
Thread.Sleep(2000);
Console.WriteLine("我耗时2秒");
});
Task.WaitAny(task1, task2);// task1和task2只要有一个任务完成就往后走
Console.WriteLine("只要有一个任务完成我就走喽");
五 Task任务的延续
- TaskAwaiter<TResult> GetAwaiter():Task类的实例方法,返回TaskAwaiter对象。
- TResult GetResult():TaskAwaiter类的实例方法,返回线程任务的返回结果。
Task<int> task1 = Task.Run<int>(() => { return 1; });
Task<int> task2 = Task.Run<int>(() => { return 2; });
TaskAwaiter<int> task1Awaiter = task1.GetAwaiter();
TaskAwaiter<int> task2Awaiter = task2.GetAwaiter();
Task<int> task = Task.Run<int>(() =>
{
return task1Awaiter.GetResult() + task2Awaiter.GetResult();
});
Console.WriteLine($"让我来看看你们给的数字和是:{task.Result}");
Console.WriteLine("会阻塞主线程哦");
- WhenAll(task1,task2,...):Task的静态方法,作用是异步等待指定任务完成后,返回结果。当线程任务有返回值时,返回Task<TResult[]>对象,否则返回Task对象。
- WhenAny()用法与WhenAll()是一样的,不同的是只要指定的任意一个线程任务完成则立即返回结果。
// WhenAll WhenAny + ContinueWith 不阻塞主线程
Task<int> task1 = Task.Run<int>(() => { Thread.Sleep(5000); return 1; });
Task<int> task2 = Task.Run<int>(() => { Thread.Sleep(5000); return 2; });
Task<String> task = Task<int>.WhenAll(task1, task2).ContinueWith((t) => {
// t:ContinueWith的返回值,一个新的延续task
return "3";
});
Console.WriteLine($"我立马打印,对我没影响");
Console.WriteLine($"我要等task走完才能打印哦:{task.Result}");
Console.WriteLine($"上面堵了,我也要等了");
}
- ContinueWith():Task类的实例方法,异步创建当另一任务完成时可以执行的延续任务。也就是当调用对象的线程任务完成后,执行ContinueWith()中的任务。
- ContinueWhenAny:某一个任务执行结束后,去触发一个动作,不卡主线程,TaskFactory实例方法,等价于WhenAny+ContinueWith
- ContinueWhenAll:所有任务执行完成后,去触发一个动作,不卡主线程,TaskFactory实例方法,等价于WhenAll+ContinueWith
六 Task枚举
-
AttachedToParent:父子任务
假设遇到如下需求,线程parentTask中开启了线程task1和task2,希望在开启parentTask线程任务的主线程中阻塞等待parentTask、task1和task2的任务完成。
此时可以将task1和task2线程依附到parentTask线程上作为parentTask的子线程,这样主线程在等待parentTask线程完成时,就必须同步等待task1和task2线程的任务完成
Task parentTask = new Task(() => {
Task task1 = new Task(() => { Console.WriteLine("task1任务。。。。。。"); }, TaskCreationOptions.AttachedToParent);
Task task2 = new Task(() => { Console.WriteLine("task2任务。。。。。。"); }, TaskCreationOptions.AttachedToParent);
task1.Start();
task2.Start();
});
parentTask.Start();
parentTask.Wait();
Console.WriteLine("我是主线程");
-
LongRunning:耗时任务
当要执行的线程任务比较耗时时,建议在创建线程对象时传入参数TaskCreationOptions.LongRunning,以此来声明为长时间运行的线程任务。
默认情况下,新建Task线程是从线程池ThreadPool中分配出来的,当使用TaskCreationOptions.LongRunning声明后则是直接新建一个线程。这样就可以避免耗时任务一直占用线程池资源的情况。当然了,也可以直接使用Thread,效果上是一样的。
Task task = new Task(() => { ...}, TaskCreationOptions.LongRunning);
task.Start();
六 Task中使用取消令牌
-
介绍说明:
Task中的取消功能使用的是CanclelationTokenSource,即取消令牌源对象,可用于解决多线程任务中协作取消和超时取消。
CancellationToken Token:CanclelationTokenSource类的属性成员,返回CancellationToken对象,可以在开启或创建线程时作为参数传入。
bool IsCancellationRequested:CanclelationTokenSource类的属性成员,表示当前任务是否已经请求取消。Token类中也有此属性成员,两者互相关联。
void Cancel():CanclelationTokenSource类的实例方法,取消线程任务,同时将自身以及关联的Token对象中的IsCancellationRequested属性置为true。
void CancelAfter(int millisecondsDelay):CanclelationTokenSource类的实例方法,用于延迟取消线程任务。
CancellationTokenRegistration Register(Action callback):Token类的实例方法,用于注册取消任务后的回调任务。
Dispose (): 当不再需要取消令牌时,应调用 CancellationTokenSource 的 Dispose 方法来释放资源。这是很重要的,特别是在长时间运行的应用程序中,以确保不会发生资源泄露
// Task任务的取消和判断
CancellationTokenSource cst = new CancellationTokenSource();
Task task = Task.Run(() => {
while (!cst.IsCancellationRequested)
{
Console.WriteLine("持续时间:" + DateTime.Now);
}
}, cst.Token);//这里第二个参数传入取消令牌
Thread.Sleep(2000);
cst.Cancel(); //两秒后结束
// 任务的延时取消可以用于访问超时、执行超时等情况下的任务强制终止
CancellationTokenSource cst = new CancellationTokenSource();
Task task = Task.Run(() => {
while (!cst.IsCancellationRequested)
{
Console.WriteLine("持续时间:" + DateTime.Now);
}
}, cst.Token);//这里第二个参数传入取消令牌
cst.CancelAfter(2000); //两秒后结束 也是异步进行
// Task任务取消回调:如果取消任务后希望做一些处理工作。
// 此时可以使用CancellationToken类的Register()函数来注册一个委托(回调函数),用于取消线程后调用。
CancellationTokenSource cst = new CancellationTokenSource();
Task task = Task.Run(() => {
while (!cst.IsCancellationRequested)
{
Console.WriteLine("持续时间:" + DateTime.Now);
Thread.Sleep(500);
}
}, cst.Token);//这里第二个参数传入取消令牌
cst.Token.Register(() => {
Console.WriteLine("开始处理工作......");
Thread.Sleep(2000);
Console.WriteLine("处理工作完成......");
});
Thread.Sleep(2000);
cst.Cancel(); //两秒后结束
- 常规用法:
private Task task;
private CancellationTokenSource cancellationSource;
private void TestTokenSource()
{
// 常规使用
cancellationSource = new CancellationTokenSource();
CancellationToken token = cancellationSource.Token;
task = Task.Run(() =>
{
while (!token.IsCancellationRequested)
{
Console.WriteLine("持续工作");
}
}, token);
task.Wait(5000);
cancellationSource?.Cancel();
cancellationSource?.Dispose();
}
-
CancellationTokenSource和CancellationToken区别:
CancellationTokenSource(CTS): 这是生成取消令牌的主要对象。可以通过调用它的 Cancel 方法来请求取消一个或多个操作。
CancellationToken(CT): 这是一个轻量级的结构,由 CancellationTokenSource 生成,并可以传递到多个操作中。操作可以检查此令牌的状态以确定是否应该取消其执行。
cancellationSource = new CancellationTokenSource();
cancellationSource.Cancel();
cancellationSource.Dispose();
if (cancellationSource.Token.IsCancellationRequested)// 报错 System.ObjectDisposedException:“CancellationTokenSource 已释放。”
{
Console.WriteLine("cancle");
}
else
{
Console.WriteLine("not cancle");
}
if (cancellationSource.IsCancellationRequested)
{
Console.WriteLine("cancle");// 打印
}
else
{
Console.WriteLine("not cancle");
}
if (cancellationSource != null)
{
Console.WriteLine("not null");// 打印
}
else
{
Console.WriteLine(" null");
}
七 Task跨线程访问控件
在使用Winform或WPF编写程序时,经常会遇到跨线程访问控件的情况,除了使用Invoke和委托等方法外,还可以有以下两种解决方法。
-
方式一:
直接将TaskScheduler对象做为参数传给Start()函数(使用TaskScheduler.FromCurrentSynchronizationContext()可以获得TaskScheduler对象),以此来将线程任务传送到指定的调度程序中运行,结合WPF编程宝典多线程章节中的内容,应该是将线程任务丢给控件元素所在线程的调度程序中运行。这样做虽然可以跨线程访问控件,但是带来的弊端就是,如果线程任务耗时,就会让整个窗体卡住。
Task task = new Task(() =>
{
Thread.Sleep(5000);//模拟耗时处理
txt_Info.Text = "test"; //此为文本控件
});
task.Start(TaskScheduler.FromCurrentSynchronizationContext());
-
方式二:
针对线程耗时的情况,如果直接使用方式一,会导致整个UI界面都卡住,等到控件处理完成才恢复,这样显然是不可以的。因此要改变一下用法,利用线程延续,将耗时的任务与访问UI控件的任务分为两个线程,访问UI的线程放到延续的线程中。
txt_Info.Text = "数据正在处理中......";
txt_Info.Text = "数据正在处理中......";
Task.Run(() =>
{
Thread.Sleep(5000);
}).ContinueWith(t => {
txt_Info.Text = "test";
}, TaskScheduler.FromCurrentSynchronizationContext());
八 Task的异常处理
1.线程外部使用Wait:Task线程的异常处理不能直接将线程对象相关代码try-catch来捕获,那样是捕获不到异常的,因为开始异常还没发生,主线程已经执行完毕,需要通过调用线程对象的wait()函数,通过wait()函数来进行线程的异常捕获。此外,线程的异常会聚合到AggregateException异常对象中(AggregateException是专门用来收集线程异常的异常类),需要通过遍历该异常对象,获取正确的异常信息。如果捕获到线程异常之后,还想继续往上抛出,就需要调用AggregateException对象的Handle函数,并返回false。(Handle函数遍历了一下AggregateException对象中的异常)
Task task1 = Task.Run(() =>
{
throw new Exception("线程1的异常抛出");
});
Task task2 = Task.Run(() =>
{
throw new Exception("线程2的异常抛出");
});
Task task3 = Task.Run(() =>
{
throw new Exception("线程3的异常抛出");
});
try
{
task1.Wait();
task2.Wait();
task3.Wait();
}
catch (AggregateException ex)
{
foreach (var item in ex.InnerExceptions)
{
Console.WriteLine(item.Message);
}
//如果希望再将异常往外抛出,可以调用AggregateException的Handle函数
//ex.Handle(p => false);
}
Console.Read();
2.线程内部可直接try-catch
try
{
Task task = Task.Run(() =>
{
try
{
int i = 0;
int j = 10;
int k = j / i; //尝试除以0,会异常
}
catch (Exception ex)
{
Debug.WriteLine($"线程内异常{ex.Message}");
}
});
}
catch (AggregateException aex)
{
foreach (var exception in aex.InnerExceptions)
{
Debug.WriteLine($"线程不等待:异常{exception.Message}");
}
}
网友评论