C# 异步编程基础(二)线程安全、向线程传递数据和异常处理

此入门教程是记录下方参考资料视频的过程
开发工具:Visual Studio 2019

参考资料:https://www.bilibili.com/video/BV1Zf4y117fs

目录

C# 异步编程基础(一)线程和阻塞

C# 异步编程基础(二)线程安全、向线程传递数据和异常处理

C# 异步编程基础(三)线程优先级、信号和线程池

C# 异步编程基础(四) 富客户端应用程序的线程 和 同步上下文 Synchronization Contexts

C# 异步编程基础(五)Task

C# 异步编程基础(六)Continuation 继续/延续 、TaskCompletionSource、实现 Task.Delay

C# 异步编程基础(七)异步原理

C# 异步编程基础(八) 异步函数

C# 异步编程基础(九) 异步中的同步上下文、ValueTask

C# 异步编程基础(十) 取消(cancellation)、进度报告、TAP(Task-Based Asynchronous Pattern)、Task组合器

本地 vs 共享的状态 Local vs Stared State

  1. Local 本地独立
    CLR为每个线程分配自己的内存栈(Stack),以便使本地变量(局部变量)保持独立

例子,结果会输出10个 ?

public static void Main(string[] args)
{
      //在新线程上调用Go()
      new Thread(Go).Start();
      //在main线程上调用Go()
      Go();
}
public static void Go()
{
      //cycles是本地变量
      //在每个线程的内存栈上,都会创建cycles独立的副本
      for (int cycles = 0; cycles < 5; cycles++)
      {
            Console.Write("?");
      }
}
  1. hared 共享
    如果多个线程都引用到同一个对象的实例,那么它们就共享了数据

例子,由于两个线程是在同一个 ThreadTest 实例上调用Go(),所以它们共享_done,结果就是只打印一次 Done

public static void Main(string[] args)
{
      //创建了一个共同的实例
      Program tt = new Program();
      new Thread(tt.Go).Start();
      tt.Go();
}
//这是一个实例方法
void Go()
{
      if (false == _done)
      {
            _done = true;
            Console.WriteLine("Done");
      }
}

   被Lambda表达式或匿名委托所捕获的本地变量,会被编译器转化为字段(field),所以也会被共享

例子,结果就是只打印一次 Done

public static void Main(string[] args)
{
      bool done = false;

      ThreadStart action = () =>
      {
            if (false == done)
            {
                  done = true;
                  Console.WriteLine("Done");
            }
      };

      new Thread(action).Start();
      action();
}

   静态字段(field)也会在线程间共享数据

例子,结果就是只打印一次 Done

public static void Main(string[] args)
{
      new Thread(Go).Start();
      Go();
}

public static void Go()
{
      if (false == _done)
      {
            _done = true;
            Console.WriteLine("Done");
      }
}

线程安全

  1. 后三个例子就引出了线程安全这个关键概念(或者说缺乏线程安全)
  2. 上述例子的输出实际上是无法确定的:
    有可能(理论上)“Done”会被打印两次
    如果交换Go()方法里语句的顺序,那么“Done”被打印两次的几率会大大增加
    因为一个线程可能正在评估if,而另外一个线程在执行WriteLine语句,它还没来得及把done设置为true
  3. 尽量的避免使用共享状态

例子,会输出两次Done

public static void Main(string[] args)
{
      new Thread(Go).Start();
      Go();
}

public static void Go()
{
      if (false == _done)
      {
            Console.WriteLine("Done");
            Thread.Sleep(100);
            _done = true;
      }
}

锁定与线程安全 简介 Locking & Thread Safety

  1. 在读取和写入共享数据的时候,通过使用一个互斥锁(exclusive lock),就可以修复前面例子的问题
  2. C#使用 lock 语句来加锁
  3. 当两个线程同时竞争一个锁的时候(锁可以基于任何引用类型对象),一个线程会等待或阻塞,直到锁变成可用状态
  4. 在多线程上下文中,以这种方式避免不确定性的代码就叫做线程安全
  5. lock不是线程安全的银弹,很容易忘记对字段加锁,lock也会引起一些问题(死锁)

例子,结果输出一次Done

static bool _done;
static readonly object _locker = new Object();

static void Main(string[] args)
{
      new Thread(Go).Start();
      Go();
}
static void Go()
{
      lock (_locker)
      {
            if (false == _done)
            {
                  Console.WriteLine("Done");
                  _done = true;
            }
      }
}

向线程传递数据

  1. 如果你想往线程的启动方法里传递参数,最简单的方式就是使用lambda表达式,在里面使用参数调用方法(例子lambda,在lambda表达式里调用方法)
static void Main(string[] args)
{
    Thread t=new Thread(()=>Print("Hello from t!"));
    t.Start();
}
static void Print(string message)
{
    Console.WriteLine(message);
}
  1. 甚至可以把整个逻辑都放在lambda里面(例子multi-lambda)
static void Main(string[] args)
{
    new Thread(()=>
    {
        Console.WriteLine("I'm running on another thread!");
    }).Start();
}
  1. 向线程传递数据,在C# 3.0之前没有lambda表达式,可以使用Thread的Start方法来传递参数
static void Main(string[] args)
{
    Thread t=new Thread(Print);
    t.Start("Hello from t!");
}
static void Print(object messageObj)
{
    string message=(string)messageObj;
    Console.WriteLine(message);
}
  1. Thread的重载构造函数可以接受下列两个委托之一作为参数:
    public delegate void ThreadStart();
    public delegate void ParameterizedThreadStart(object obj);

  2. lambda表达式与被捕获的变量
    使用lambda表达式可以很简单的给Thread传递参数。但是线程开始后,可能会不小心修改了被捕获的变量,这要多加注意

例子,i在循环的整个生命周期内指向的是同一个内存地址,每个线程对Console.WriteLine()的调用都会在它运行的时候对它进行修改

static void Main(string[] args)
{
    for(int i=0;i<10;i++)
    {
        new Thread(()=>Console.Write(i)).Start();
    }
}

解决方案,但是顺序仍然无法保证

static void Main(string[] args)
{
    for(int i=0;i<10;i++)
    {
        int temp=i;
        new Thread(()=>Console.Write(temp)).Start();
    }
}

异常处理

  1. 创建线程时在作用范围内的try/catch/finally块,在线程开始执行后就与线程无关了(创建线程的地方不会捕获线程的异常)
public static void Main(string[] agrs)
{
    try
    {
        new Thread(Go).Start();
    }
    catch(Exception e)
    {
        Console.WriteLine("Exception!");
    }
}
static void Go()
{
    throw null;
}

补救办法是把异常处理放在Go方法里面

public static void Main(string[] agrs)
{
    new Thread(Go).Start();
}
static void Go()
{
    try
    {
        throw null;
    }
    catch(Exception e)
    {
        Console.WriteLine("Exception!");
    }    
}
  1. 在WPF、WinForm里,可以大约全局异常处理事件:
    Application.DispatcherUnhandledException
    Application.ThreadException
    在通过消息循环调用的程序的任何部分发生未处理的异常(这相相当于应用程序处于活动状态时在主线程上运行的所有代码)后,将触发这些异常
    人话:主线程上有未处理的异常就会被触发
    但是非UI线程上未处理异常,并不会触发它
  2. 而任何线程有任何未处理的异常都会触发
    AppDomain.CurrentDomain.UnhandledException

前台和后台线程 Foreground vs Background Threads

  1. 默认情况下,你手动创建的线程就是前台线程
  2. 只要有前台线程在运行,那么应用程序就会一直处于活动状态
    但是只有后台线程却不行
    一旦所有的前台线程停止,那么应用程序就停止了
    任何的后台线程也会突然停止
  3. 注意:线程的前台、后台状态与它的优先级无关(所分配的执行时间)
  4. 可以通过IsBackground属性判断线程是否是后台线程
static void Main(string[] args)
{
    Thread worker=new Thread(()=>Console.ReadLine());
    if(args.Length>0)
    {
        worker.IsBackground=true;
    }
    worker.Start();
}

如果是前台线程,会一直等待输入,不输入就不终止
如果是后台线程,主线程结束之后,程序就会终止,不会等待我们的输入

  1. 进程以这种形式终止的时候,后台线程执行栈中的finally块就不会被执行了
    如果想让它执行,可以在退出程序时使用Join来等待后台线程(如果是你创建的线程),或者使用signal construct,如果是线程池...
  2. 应用程序无法正常退出的一个常见原因是还有活跃的前台线程

线程安全、向线程传递数据和异常处理 结束

posted @ 2021-02-06 16:43  .NET好耶  阅读(1561)  评论(0编辑  收藏  举报