C# Advanced Tutorial 1-14-Threads03


主要内容概要

1 多异常处理和线程取消
2 多线程的临时变量
3 线程安全和锁lock
4 await async

多线程异常

思考:
多线程中如果某一个线程异常了,就会终结当前线程;对其他的线程是没有影响的;
多线程中的异常去哪儿了? 被吞掉了。

try
{
  List<Task> taskList = new List<Task>();
  for (int i = 0; i < 100; i++)
  {
    string name = $"btnThreadCore_Click_{i}";
    int k = i;
    taskList.Add(Task.Run(() =>
    {
      if (k == 5)
      {
        throw new Exception($"{name} 异常了");
      }
      else if (k == 6)
      {
        throw new Exception($"{name} 异常了");
      }
      else if (k == 10)
      {
        throw new Exception($"{name} 异常了");
      }
      Console.WriteLine($"this is {name} Ok!");
    }));
  };
  Task.WaitAll(taskList.ToArray());
}
catch (AggregateException aex)  //可以有多个Catch 在匹配异常类型的时候,先具体,然后在寻找父类
{
  foreach (var exception in aex.InnerExceptions)
  {
    Console.WriteLine(exception.Message);
  }
}
catch (Exception ex)
{
  Console.WriteLine(ex.Message);
  throw;
}

通过异常AggregateException 可以捕获到多线程中的异常,然后遍历InnerExceptions就可以得到具体的线程异常。
在工作中常规建议是:多线程的委托里面不想允许异常,包一层try catch,然后记录下来信息,完成一些操作。
更进一步: 在实际的工作中,往往有很多场景是如果发生异常之后,其他的线程就需要取消,不再继续往下执行;问题就是如何取消线程;

线程取消

多线程并发任务,某个失败后,希望通知别的线程,都停下来,how?
Thread.Abort() 终止线程,向当前线程抛一个异常然后终结任务:线程属于OS资源,可能不会立即停下来。
Task不能从外部终止任务,只能自己终止自己(上帝才能打败自己)

CancellationTokenSource cts = new CancellationTokenSource();// 通知式的
try
{ 
  List<Task> taskList = new List<Task>();
  for (int i = 0; i < 100; i++)
  {
    string name = $"btnThreadCore_Click_{i}";
    int k = i;
    taskList.Add(Task.Run(() =>
    {
      if (k == 5)
      { 
        throw new Exception($"{name} 异常了");
      }
      if (!cts.IsCancellationRequested)//是否取消
      {
        Console.WriteLine($"this is {name} Ok!");
      }
      else
      {
        Console.WriteLine($"this is {name} Stop!");
      } 
    }));
  };
  Task.WaitAll(taskList.ToArray());
}
catch (AggregateException aex)  //可以有多个Catch 在匹配异常类型的时候,先具体,然后在寻找父类
{
  cts.Cancel(); //执行该方法以后,IsCancellationRequested会被指定为false
  foreach (var exception in aex.InnerExceptions)
  {
    Console.WriteLine(exception.Message);
  }
}
catch (Exception ex)
{
  Console.WriteLine(ex.Message);
  throw;
} 

CancellationTokenSource这个对象有个bool属性IsCancellationRequested,对外提供一个方法Cancel()方法。可以重复Cancel,别的线程可以通过属性IsCancellationRequested来知道,是否需要撤销。
如果线程还没启动,能不能就不要启动了。
Task在Run的时候可以接受一个参数CancellationToken,可以进一步升级一下:
1.启动线程传递token
2.异常抓取
3.在Cancel时还没有启动的任务就不启动了,二十抛出异常

taskList.Add(Task.Run(() =>
{
    if (k == 5)
    { 
      throw new Exception($"{name} 异常了");
    }
    if (!cts.IsCancellationRequested)//是否取消
    {
      Console.WriteLine($"this is {name} Ok!");
    }
    else
    {
      Console.WriteLine($"this is {name} Stop!");
    } 
},cts.Token));

临时变量

先看一下下面两个例子:

for (int i = 0; i < 20; i++)   //for 循环很快
{
  Task.Run(() => // 开启线程;不会阻塞的,线程会延迟启动
  {
    Console.WriteLine($"btnThreadCore_Click_{i} 线程Id={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
  });
}

为什么最后输出btnThreadCore_Click_20
这里有个临时变量的问题,线程时非阻塞的,延迟启动的;线程执行的时候,i已经是20了。

for (int i = 0; i < 20; i++)
{
  int k = i; //作用域 这个k 是不是只是针对于某一次for 循环,循环20次就会有20 k
  Task.Run(() =>
  {
    Console.WriteLine($"btnThreadCore_Click_{k} 线程Id={Thread.CurrentThread.ManagedThreadId.ToString("00")}");
  });
}

k为什么是对的呢?k是闭包里面的变量,没次循环都有一个独立的k,但是i是共享变量。事实上有5个k变量,但是只有一个i变量。

线程安全&lock

线程安全:如果你的代码在进程中有多个线程同时运行这一段,如果每次运行的结果都跟单线程运行时的结果一致,那么就是线程安全的。

private int iNumSync;
private int iNumAsync;
for (int i = 0; i < 10000; i++)
{
  iNumSync++;
}

for (int i = 0; i < 10000; i++)
{
  Task.Run(() =>iNumAsync++);
}

Thread.Sleep(100000);
Console.WriteLine($"iNumSync={iNumSync},iNumAsync={iNumAsync}")

可以发现单线程的结果就是稳定的10000,多线程的计算结果不是10000,且不能被预测。
上述过程就是线程不安全。
线程不安全一般出现在全局变量/共享变量/磁盘文件/静态变量/数据库的值/只要是多项去访问修改的时候,就可能会出现线程安全。
发生是因为多个线程相同操作,出现了覆盖,怎么解决呢?

Lock

  • Lock是语法糖,Monitor.Enter 占据一个应用,别的线程就只能等着
  • 推荐锁是 private static readonly object
  • 不能是null,也不能是string;
  • 不推荐使用lock(this) 注意是一个线程还是多线程,如果外面也要用实例,就冲突了。
  • 锁的作用:排他
  • lock里面的代码不要太多
private static readonly object Obj_Lock = new object();

for (int i = 0; i < 100000; i++)
{
  Task.Run(() =>
  {
    try
    {
      lock (Obj_Lock)//可以 避免多线程并发,如果锁住以后,其实这里跟单线程基本上没啥区别;
      {
        this.NumTow += 1;
      }
    }
    catch (Exception)
    {
      throw;
    }
  });
}

调用DoTest的时候,递归作用,lock this 这里会不会死锁?

Test test = new Test();
test.DoTest();

不会死锁!!!因为是同一个线程,

public class Test
{
  private int TestNum = 0;
  public void DoTest()
  {
    lock (this) // 为了排他  锁的正常作用:应该在这儿等待,
    {
      Thread.Sleep(2000);
      TestNum += 1;
      if (DateTime.Now.Day < 13 && TestNum < 5)
      {
        this.DoTest();
      }
      else
      {
        Console.WriteLine("结束了");
      }
    }
  }
}

private string Str_Lock = "ChaoqiangLock";为什么string也不能用于锁呢?
因为string对不同的变量,如果字符串内容相同,是共享内存的,所以也是同一把锁。

线程安全集合

System.Collections.Concurrent.ConcurrentStack System.Collections.Concurrent.ConcurrentQueue<int>像这些都是基于线程安全。

数据分拆

避免多个线程操作同一堆数据,安全又高效率。


文章作者: Chaoqiang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Chaoqiang !
评论
 上一篇
C# Advanced Tutorial 1-15-AwaitAsync C# Advanced Tutorial 1-15-AwaitAsync
主要内容概要1 await/async语法和使用2 原理探究和使用建议 Await Asyncawait/async 是C# 的保留关键字 ,.Net framework4.5 版本出现的,通常是成对出现。async修饰方法,可以单独出现
下一篇 
C# Advanced Tutorial 1-13-Threads02 C# Advanced Tutorial 1-13-Threads02
主要内容概要1 Task:Waitall WaitAny Delay2 TaskFactory:ContinueWhenAny ContinueWhenAll3 Parallel TaskTask 是.NetFramework3.0
  目录