上面一篇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());如下图:

则会捕获到异常。所以通过上面能够说明:

  1. 多线程里面抛出的异常,会终结当前线程;但是不会影响别的线程;
  2. 那线程异常可以通过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}看似简单,实际上有三个意思,这对于适当地使用它至关重要:

  1. objectA被lock了吗?没有则由我来lock,否则一直等待,直至objectA被释放。
  2. lock以后在执行codeB的期间其他线程不能调用codeB,也不能使用objectA。
  3. 执行完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 篇幅二的更多相关文章

  1. C# Task 篇幅一

    在https://www.cnblogs.com/loverwangshan/p/10415937.html中我们有讲到委托的异步方法,Thread,ThreadPool,然后今天来讲一下Task, ...

  2. C# 多线程六之Task(任务)二

    前面介绍了Task的由来,以及简单的使用,包括开启任务,处理任务的超时.异常.取消.以及如果获取任务的返回值,在回去返回值之后,立即唤起新的线程处理返回值.且如果前面的任务发生异常,唤起任务如果有效的 ...

  3. Activity启动场景Task分析(二)

    场景分析 下面通过启动Activity的代码来分析一下: 1.桌面 首先,我们看下处于桌面时的状态,运行命令: adb shell dumpsys activity 结果如下 ACTIVITY MAN ...

  4. Dynamics CRM2016 业务流程之Task Flow(二)

    接上篇,Page页设置完后,按照业务流程管理也可以继续设置Insert page after branch 或者 Add branch,我这里选择后者,并设置了条件,如果Pipeline Phase ...

  5. 【5min+】 秋名山的竞速。 ValueTask 和 Task

    系列介绍 简介 [五分钟的dotnet]是一个利用您的碎片化时间来学习和丰富.net知识的博文系列.它所包含了.net体系中可能会涉及到的方方面面,比如C#的小细节,AspnetCore,微服务中的. ...

  6. Activity的task相关 详解

    task是一个具有栈结构的容器,可以放置多个Activity实例.启动一个应用,系统就会为之创建一个task,来放置根Activity:默认情况下,一个Activity启动另一个Activity时,两 ...

  7. .Net进阶系列(13)-异步多线程(Task和Parallel)(被替换)

    一. Task开启多线程的三种形式 1. 利用TaskFactory下的StartNew方法,向StartNew传递无参数的委托,或者是Action<object>委托. 2. 利用Tas ...

  8. C# 多线程六之Task(任务)三之任务工厂

    1.知识回顾,简要概述 前面两篇关于Task的随笔,C# 多线程五之Task(任务)一 和 C# 多线程六之Task(任务)二,介绍了关于Task的一些基本的用法,以及一些使用的要点,如果都看懂了,本 ...

  9. C#中Task的使用简单总结

    Task在并行计算中的作用很凸显,但是他的使用却有点小复杂,下面是任务的一些基本使用说明(转载与总结于多篇文章) 简单点说说吧! 创建 Task 创建Task有两种方式,一种是使用构造函数创建,另一种 ...

随机推荐

  1. Android Architecture Components--项目实战

    转载请注明出处,谢谢! 上个月Google Android Architecture Components 1.0稳定版发布,抽工作间隙写了个demo,仅供参考 Github地址:https://gi ...

  2. 清理 zabbix 历史数据, 缩减 mysql 空间

    zabbix 由于历史数据过大, 因此导致磁盘空间暴涨,  下面是结局方法步骤 1. 停止 ZABBIX SERER 操作 [root@gd02-qa-plxt2-nodomain-web-95 ~] ...

  3. CentOS 5.9裸机编译安装搭建LAMP

    Linux系统:CentOS 5.9,查看CentOS版本,命令如下: [root@localhost /]# cat /etc/redhat-release CentOS release 5.9 ( ...

  4. 完整的系统帮助类Utils

    //来源:http://www.cnblogs.com/yuangang/p/5477324.html using System; using System.Collections.Generic; ...

  5. django 标签的使用

    首先重建一个common的app 然后创建__init__使common成为一个包   注意templatetags 名字使固定的 并在下面创建一个名字为fitter的过滤器 注册过滤器app htm ...

  6. 【安富莱专题教程第5期】工程调试利器RTT实时数据传输组件,替代串口调试,速度飞快,可以在中断和多任务中随意调用

    说明:1.串口作为经典的调试方式已经存在好多年了,缺点是需要一个专门的硬件接口.现在有了SEGGER的RTT(已经发布有几年了),无需占用系统额外的硬件资源,而且速度超快,是替代串口调试的绝佳方式.2 ...

  7. [Swift]LeetCode299. 猜数字游戏 | Bulls and Cows

    You are playing the following Bulls and Cows game with your friend: You write down a number and ask ...

  8. [Swift]LeetCode457. 环形数组循环 | Circular Array Loop

    You are given an array of positive and negative integers. If a number n at an index is positive, the ...

  9. [Swift]LeetCode600. 不含连续1的非负整数 | Non-negative Integers without Consecutive Ones

    Given a positive integer n, find the number of non-negativeintegers less than or equal to n, whose b ...

  10. [Swift]LeetCode878. 第 N 个神奇数字 | Nth Magical Number

    A positive integer is magical if it is divisible by either A or B. Return the N-th magical number.  ...