c# Task 篇幅二
上面一篇https://i.cnblogs.com/EditPosts.aspx?postid=10444773我们介绍了Task的启动,Task的一些方法以及应用,今天我们着重介绍一下Task其它概念以及用法,具体说说下面三大块
- 多异常处理和线程取消
- 多线程的临时变量
- 线程安全和锁lock
一:多线程异常
多线程异常捕获一般都是使用AggregateException这个异常类来捕获
我们先通过代码详细介绍:
try
{
List<Task> taskList = new List<Task>();
for (int i = ; i < ; i++)
{
string name = $"btnThreadCore_Click_{i}";
taskList.Add(Task.Run(() =>
{
if (name.Equals("btnThreadCore_Click_1"))
{
throw new Exception("btnThreadCore_Click_1异常");
}
Console.WriteLine($"This is {name}成功 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("")}");
}));
}
Task.WaitAll(taskList.ToArray());//1 可以捕获到线程的异常
}
catch (AggregateException aex) //2 需要try-catch-AggregateException
{
foreach (var exception in aex.InnerExceptions)
{
Console.WriteLine(exception.Message);
}
}
catch (Exception ex)//可以多catch 先具体再全部
{
Console.WriteLine(ex);
}
下面是图一是没有加 Task.WaitAll(taskList.ToArray());
从上面结果我们可以得出:线程异常不会被捕获,但是线程之间互不影响,一个线程出现问题不会影响其它的线程。
如果增加了 Task.WaitAll(taskList.ToArray());如下图:
则会捕获到异常。所以通过上面能够说明:
- 多线程里面抛出的异常,会终结当前线程;但是不会影响别的线程;
- 那线程异常可以通过Task.WaitAll(taskList.ToArray());被捕获(说明必须要等到线程执行完task.Wait()或者得到线程的结果task.result的时候才会捕获到异常),没有使用Task.WaitAll(taskList.ToArray()),子线程出现的异常则会被吞掉,
我们上面一章Task晓得,Task.WaitAll和Task.WaitAny()都是会线程堵塞的,这样会大大影响用户的体验,所以我们一般项目中多线程里面的委托里面不允许异常,则会在委托里面包一层try-catch,然后记录下来异常信息,完成需要的操作,可以参考下面代码:
try
{
List<Task> taskList = new List<Task>();
for (int i = ; i < ; i++)
{
string name = $"btnThreadCore_Click_{i}";
taskList.Add(Task.Run(() =>
{
try
{
if (name.Equals("btnThreadCore_Click_1"))
{
throw new Exception("btnThreadCore_Click_1异常");
}
Console.WriteLine($"This is {name}成功 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("")}");
}
catch (Exception ex)
{
Console.WriteLine($"委托里面Exception捕获的异常:{ex.Message}");
}
}));
}
}
catch (AggregateException aex) //2 需要try-catch-AggregateException
{
foreach (var exception in aex.InnerExceptions)
{
Console.WriteLine($"任务外面AggregateException捕获到的异常:{exception.Message}");
}
}
catch (Exception ex)//可以多catch,先具体再全部,如果具体的catch捕获到,则其它的异常将不会再次捕获
{
Console.WriteLine(ex);
}
二:线程如何取消
之前我们有讲过Thread,这个方法中有一个Abort,字面意思是取消线程,向当前线程抛一个异常然后终结任务,但是我们知道线程是由资源系统OS调度的,即使我们调用了这个方法,也没有办法立即去执行这件事情,所以我们不建议随便使用abort方法来取消线程,但是我们在工作中又会遇到这种情况,某个线程出现了问题,然后其他运行的线程要取消或者没有开始运行的线程不运行,那如果这样我们该如何实现呢?请看下面代码:
try
{
CancellationTokenSource cts= new CancellationTokenSource();
List<Task> taskList = new List<Task>();
for (int i = ; i < ; i++)
{
string name = $"btnThreadCore_Click_{i}";
taskList.Add(Task.Run(() =>
{
try
{
if (!cts.IsCancellationRequested)
{
Console.WriteLine($"This is {name} 开始 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("")}");
} Thread.Sleep(new Random().Next(, )); if (name.Equals("btnThreadCore_Click_11"))
{
throw new Exception("btnThreadCore_Click_11异常");
}
else if (name.Equals("btnThreadCore_Click_13"))
{
cts.Cancel(); //可以多次调用,这个是只能设置IsCancellationRequested 属性为true,没有办法变为false
}
if (!cts.IsCancellationRequested)
{
Console.WriteLine($"This is {name}成功结束 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("")}");
}
else
{
Console.WriteLine($"This is {name}中途停止 ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("")}");
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
cts.Cancel();
}
}, cts.Token));
}
Task.WaitAll(taskList.ToArray());
}
catch (AggregateException aex)
{
foreach (var exception in aex.InnerExceptions)
{
Console.WriteLine($"外部AggregateException={exception.Message}");
}
}
catch (Exception ex)
{
Console.WriteLine($"外部Exception={ex.Message}");
}
运行结果如下:
我们发现结果中有中途停止的线程(可以使用一个变量的改变来实现),实现这个是下面三个步骤:
- 1 准备CancellationTokenSource ,里面有个bool属性IsCancellationRequested 初始化是false,调用Cancel方法后变成true(IsCancellationRequested 只能变为true,不能再次变回false),可以重复cancel
- 2 try-catch中捕获到异常然后调用Cancel方法,把 IsCancellationRequested 只能变为true
- 3 Action要随时判断IsCancellationRequested,如果这个值为true,则线程需要停止。
另外我们还有发现有捕获到已取消一个任务的提示,实现的方法如下:
- 1 启动线程Task.Run()方法中传递CancellationTokenSource 的Token这个参数,这能做到:在调用方法Cancel后即IsCancellationRequested变为true时,还没有启动的任务,就不启动了;也是抛异常,cts.Token.ThrowIfCancellationRequested
- 2 异常抓取 ,此时是捕获子线程的异常,如果要抓取异常则一定要使用Task.WaitAll()方法,或者线程异常捕获不到。
注意:
- CancellationTokenSource则是能外部对Task的控制,如取消、定时取消
- Task不能外部终止任务,只能自己终止自己
三:线程中的临时变量和全局变量
for (int i = ; i < ; i++)
{
int k = i;
Task.Run(() =>
{
Console.WriteLine($"This is btnThreadCore_Click_{i}_{k} ThreadId={Thread.CurrentThread.ManagedThreadId.ToString("")}");
});
}
运行结果:
是不是发现 i一直为5,然后k为正规的0-4,为啥为出现这样的结果,分为以下2点来说明:
- 线程是非阻塞的,延迟启动的;线程执行的时候,i已经是5了
- k是闭包里面的变量,每次循环都有一个独立的k,即是5个k变量, 1个i变量
四:线程锁lock
上面我们说的全局变量i会发生改变,其实就可以说是线程安全的问题,有时候我们使用多线程,发现我们写的代码和实际得到的结果不一致,比如:
for (int i = ; i < ; i++)
{
this.iNumSync++;
}
for (int i = ; i < ; i++)
{
Task.Run(() =>
{
this.iNumAsync++;
});
}
for (int i = ; i < ; i++)
{
int k = i;
Task.Run(() => this.iListAsync.Add(k));
} Thread.Sleep( * );
Console.WriteLine($"iNumSync={this.iNumSync} iNumAsync={this.iNumAsync} listNum={this.iListAsync.Count}");
运行得到结果如下:
我们发现同步方法是我们想要的结果,但是其它的两个都小于10000,这就是我们所说的线程安全问题,比如线程并发同时操作一个变量,会把这个变量同时覆盖掉为同一个值,才会出现累计加仍然不到10000,如果多运行几次会发现iNumSync一直是10000,然后 iNumAsync会是1-10000之间的值。
所以如果想要保证线程安全一定要加锁。Lock是语法糖,Monitor.Enter,占据一个引用,别的线程就只能等着,一般推荐锁为:private static readonly object这种格式。lock(objectA){codeB}看似简单,实际上有三个意思,这对于适当地使用它至关重要:
- objectA被lock了吗?没有则由我来lock,否则一直等待,直至objectA被释放。
- lock以后在执行codeB的期间其他线程不能调用codeB,也不能使用objectA。
- 执行完codeB之后释放objectA,并且codeB可以被其他线程访问。
加锁一般避免使用如下几种格式:
1:不能Lock(Null),可以编译但是不能运行;
try
{
List<Task> tasks = new List<Task>();
for (int i = ; i <; i++)
{
tasks.Add(Task.Run(() =>
{
lock (null)//任意时刻只有一个线程能进入方法块儿,这不就变成了单线程
{
this.iNumAsync++;
}
}));
}
Task.WaitAll(tasks.ToArray());
}
catch (AggregateException ex)
{
foreach (var exception in ex.InnerExceptions)
{
Console.WriteLine($"AggregateException={exception.Message}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Exception={ex.Message}");
}
运行会报如下错误:
2:不推荐lock(this),外面如果也要用实例,就冲突了
比如:
public class Test
{
private int iDoTestNum = ;
/// <summary>
/// 测试lock(this)
/// </summary>
public void DoTest()
{
//这里是同一个线程,这个引用就是被这个线程所占据,所以不会发生死锁
lock (this)
{
Thread.Sleep();
this.iDoTestNum++;
if (DateTime.Now.Day < && this.iDoTestNum < )
{
Console.WriteLine($"This is {this.iDoTestNum}次 {DateTime.Now.Day}");
this.DoTest();
}
else
{
Console.WriteLine("结束!!!!");
}
}
}
}
执行:
Test test = new Test();
Task.Delay().ContinueWith(t =>
{
lock (test)
{
Console.WriteLine("*********Start**********");
Thread.Sleep();
Console.WriteLine("*********End**********");
}
});
test.DoTest();
会出现:
通过上面我们看到,我们Test类中的方法使用的lock(this),即是相当于Test的实例test,然后我们发现的结果是只有把test.DoTest()里面的方法全部执行后,才会执行ContinueWith里面的方法,如果我们把把代码修改为如下:
public class Test
{
private int iDoTestNum = ;
private static readonly object obj=new object();
/// <summary>
/// 测试lock(this)
/// </summary>
public void DoTest()
{
//这里是同一个线程,这个引用就是被这个线程所占据,所以不会发生死锁
lock (obj)
{
Thread.Sleep();
this.iDoTestNum++;
if (DateTime.Now.Day < && this.iDoTestNum < )
{
Console.WriteLine($"This is {this.iDoTestNum}次 {DateTime.Now.Day}");
this.DoTest();
}
else
{
Console.WriteLine("结束!!!!");
}
}
}
}
执行:
Test test = new Test();
Task.Delay().ContinueWith(t =>
{
lock (test)
{
Console.WriteLine("*********Start**********");
Thread.Sleep();
Console.WriteLine("*********End**********");
}
});
test.DoTest();
会出现:
这样两个锁则不会发生冲突。
注意:
- 1.lock(this)的缺点就是在一个线程锁定某对象之后导致整个对象无法被其他线程访问。
- 2.锁定的不仅仅是lock段里的代码,锁本身也是线程安全的。
- 3.我们应该使用不影响其他操作的私有对象作为locker。
- 4.在使用lock的时候,被lock的对象(locker)一定要是引用类型的,如果是值类型,将导致每次lock的时候都会将该对象装箱为一个新的引用对象(事实上如果使用值类型,c#编译器在编译时会给出个错误)
3:不推荐使用lock(string)
public class Test
{
private int iDoTestNum = ;
private string Name = "wss";
public void DoTestString()
{
lock (this.Name)
//递归调用,lock this 会不会死锁? 98%说会! 不会死锁!
//这里是同一个线程,这个引用就是被这个线程所占据
{
Thread.Sleep();
this.iDoTestNum++;
if (DateTime.Now.Day < && this.iDoTestNum < )
{
Console.WriteLine($"This is {this.iDoTestNum}次 {DateTime.Now.Day}");
this.DoTestString();
}
else
{
Console.WriteLine("28号,课程结束!!");
}
}
}
}
执行:
Test test = new Test();
string student = "wss";
Task.Delay().ContinueWith(t =>
{
lock (student)
{
Console.WriteLine("*********Start**********");
Thread.Sleep();
Console.WriteLine("*********End**********");
}
});
test.DoTestString();
结果:
也是会出现跟lock(this)的问题,因为string字符串在c#代码中内存是分配引用是复用的,即是你声明的两个string s=“wss” 和string s1=“wss”,然后引用内存是统一的,下图则可以证明:
五:线程安全
如果想要保证线程安全,会有以下几种途径
1:使用lock来加锁,具体使用参考第四条
2: 线程安全集合,这个一般命名空间System.Collections.Concurrent.ConcurrentQueue<int>,这个可以直接拿来使用,是能够直接保证线程安全的,不需要我们额外去做一些操作,它们对应的类型很多。
3:因为加lock和使用线程安全数据类型都会损耗性能的,如果环境允许的话,可以使用数据分拆,避免多线程操作同一个数据;又安全又高效,比如操作数据库或者文件,可以让每个线程去做不重复的东西,则不会有安全问题,当然这个拆分是有局限性的,是要从场景出发的。
c# Task 篇幅二的更多相关文章
- C# Task 篇幅一
在https://www.cnblogs.com/loverwangshan/p/10415937.html中我们有讲到委托的异步方法,Thread,ThreadPool,然后今天来讲一下Task, ...
- C# 多线程六之Task(任务)二
前面介绍了Task的由来,以及简单的使用,包括开启任务,处理任务的超时.异常.取消.以及如果获取任务的返回值,在回去返回值之后,立即唤起新的线程处理返回值.且如果前面的任务发生异常,唤起任务如果有效的 ...
- Activity启动场景Task分析(二)
场景分析 下面通过启动Activity的代码来分析一下: 1.桌面 首先,我们看下处于桌面时的状态,运行命令: adb shell dumpsys activity 结果如下 ACTIVITY MAN ...
- Dynamics CRM2016 业务流程之Task Flow(二)
接上篇,Page页设置完后,按照业务流程管理也可以继续设置Insert page after branch 或者 Add branch,我这里选择后者,并设置了条件,如果Pipeline Phase ...
- 【5min+】 秋名山的竞速。 ValueTask 和 Task
系列介绍 简介 [五分钟的dotnet]是一个利用您的碎片化时间来学习和丰富.net知识的博文系列.它所包含了.net体系中可能会涉及到的方方面面,比如C#的小细节,AspnetCore,微服务中的. ...
- Activity的task相关 详解
task是一个具有栈结构的容器,可以放置多个Activity实例.启动一个应用,系统就会为之创建一个task,来放置根Activity:默认情况下,一个Activity启动另一个Activity时,两 ...
- .Net进阶系列(13)-异步多线程(Task和Parallel)(被替换)
一. Task开启多线程的三种形式 1. 利用TaskFactory下的StartNew方法,向StartNew传递无参数的委托,或者是Action<object>委托. 2. 利用Tas ...
- C# 多线程六之Task(任务)三之任务工厂
1.知识回顾,简要概述 前面两篇关于Task的随笔,C# 多线程五之Task(任务)一 和 C# 多线程六之Task(任务)二,介绍了关于Task的一些基本的用法,以及一些使用的要点,如果都看懂了,本 ...
- C#中Task的使用简单总结
Task在并行计算中的作用很凸显,但是他的使用却有点小复杂,下面是任务的一些基本使用说明(转载与总结于多篇文章) 简单点说说吧! 创建 Task 创建Task有两种方式,一种是使用构造函数创建,另一种 ...
随机推荐
- vue项目使用webpack构建的本地服务环境,在手机上访问调试
使用vue脚手架构建的项目,一般在本地localhost运行,配合浏览器的模拟调试工具开发. 如果想看真机环境,又不想build到线上. webpack能配置电脑本地内网环境指向公网访问的! 1.打开 ...
- js数组和对象相等判断、拷贝详解(结合几个现象讲解引用数据类型的趣事)
序言 最近遇到几个js引用数据类型造成的bug,今天结合bug详细分析一下,避免以后再犯,也希望能帮大家提个醒,强化js基本功. 目录 1.浅拷贝.深拷贝,解决变量赋值相互影响问题 2.判断2个数组. ...
- centOS7上编译hadoop-2.7.7
一.阅读编译文档 在hadoop源码包根目录下有个一个BUINDING.txt的文件,文件说明了编译hadoop所需要的一些编译hadoop所需要的一些编译环境相关的东西.不同hadoop版本的要求都 ...
- SSIS - 5.优先约束
一.优先约束和执行逻辑 任务和容器是SSIS中的可执行文件,一个优先约束连接着两个可执行文件:优先的可执行文件和约束的可执行文件,如下图. 它的执行逻辑如下图: 1)先执行优先可执行文件 2)判断 ...
- JS实现数组去重方法大总结
js数组根据对象中的元素去重: var arr2 = [ { name: "name1", num: "1" }, { name: "name2&qu ...
- 【计算机篇】Office 2016 for Mac 安装和破解教程
免责声明 请亲们支持正版.这教程旨在分享,供参考. 为啥写这篇文章 对于大多数使用 Mac 的用户而言,虽然有苹果自家的办公软件,但功能少,用起来不舒服.而 Offer 2016 版的需要登录激活购买 ...
- SQL 常用语法记录
SQL语法 注意:SQL 对大小写不敏感 可以把 SQL 分为两个部分:数据操作语言 (DML) 和 数据定义语言 (DDL). 数据操作语言 (DML) SQL (结构化查询语言)是用于执行查询的语 ...
- [Swift]LeetCode707. 设计链表 | Design Linked List
Design your implementation of the linked list. You can choose to use the singly linked list or the d ...
- [Swift]LeetCode914.一副牌中的X | X of a Kind in a Deck of Cards
In a deck of cards, each card has an integer written on it. Return true if and only if you can choos ...
- ubuntu16.04安装lnmp环境
1.安装mysql sudo apt install mysql-server 2.安装nginx和php #添加nginx和php的ppa源 sudo apt-add-repository ppa ...