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组合器

线程

  1. 线程是一个可执行路径,它可以独立于其它线程执行
  2. 每一个线程都在操作系统的进程(Process)内支线,而操作系统进程提供了程序运行的独立环境
  3. 单线程应用,在进程的独立环境里只跑一个线程,所以该线程拥有独占权
  4. 多线程应用,单个进程中会跑多个线程,它们会共享当前的执行环境(尤其是内存)
    例如,一个线程在后台读取数据,另一个线程在数据到达后进行展示
    这个数据就被称作是共享的状态
  5. 例子:
    1. 在单核计算机上,操作系统必须为每个线程分配“时间片”(在Windows中通常为20毫秒)来模拟并发,从而导致重复的x和y块
    2. 在多核或多处理器计算机上,这两个线程可以真正地并行执行(可能受到计算机上其它活动进程的竞争)
    3. 在本例中,由于控制台处理并发请求的机制的微妙性,您仍然会得到重复的x和y块
public static void Main()
{
    //开辟了一个新的线程 Thread
    Thread t=new Thread(WriteY);
    t.Start();//运行WirteY
    Console.WriteLine("Thread t has ended!");

    //同时在主线程也做一些工作
    for(int i=0;i<10;i++)
    {
        Console.Write("x");
    }
}

public static void WriteY()
{
    for(int i=0;i<1000;i++)
    {
        Console.Write("y");
    }
}
  1. 术语:线程被抢占
    线程在这个时候就可以被称为抢占了:它的执行与另外一个线程上代码的执行交织的那一点

线程的一些属性

  1. 线程一旦开始执行,IsAlive就是true,线程结束就变成false
  2. 线程结束的条件就是:线程构造函数传入的委托结束了执行
  3. 线程一旦结束,就无法再重启
  4. 每个线程都有个Name属性,通常用于调试
    线程Name只能设置一次,以后更改就会抛出异常
  5. 静态的Thread.CurrentThread属性,会返回当前执行的线程

例子

public static void Main()
{
    //开辟了一个新的线程 Thread
    Thread t=new Thread(WriteY);
    t.Name="Y Thread ...";
    t.Start();//运行WirteY

    Console.WriteLine("Thread.CurrentThread.Name");
    //同时在主线程也做一些工作
    for(int i=0;i<10;i++)
    {
        Console.Write("x");
    }
}

public static void WriteY()
{
    Console.WriteLine(Thread.CurrentThread.Name);
    for(int i=0;i<1000;i++)
    {
        Console.Write("y");
    }
}

结果

Join and Sleep

  1. 调用Join方法,就可以等待另一个线程结束
    可以理解为“等待该线程终止”,也就是在子线程调用了Join()方法后面的代码,只有等到子线程结束了才能执行。

例子

public static void Main()
{
    Thread t=new Thread(Go);
    t.Start();
    t.Join();
    Console.WriteLine("Thread t has ended!");
}

public static void Go()
{
    for(int i=0;i<1000;i++)
    {
        Console.Write("y");
    }
}

结果

  1. 添加超时:
    调用Join的时候,可以设置一个超时,用毫秒或者TimeSpan都可以
      如果返回true,那就是线程结束了
      如果超时了(在限制时间内未完成),就返回false
    例子
static Thread thread1, thread2;
public static void Main()
{
    thread1 = new Thread(ThreadProc);
    thread1.Name = "Thread1";
    thread1.Start();

    thread2 = new Thread(ThreadProc);
    thread2.Name = "Thread2";
    thread2.Start();
}
private static void ThreadProc()
{
    Console.WriteLine("\nCurrent thread:{0}", Thread.CurrentThread.Name);
    if (Thread.CurrentThread.Name == "Thread1" && thread2.ThreadState != ThreadState.Unstarted)
    {
        if (thread2.Join(2000))
        {
        Console.WriteLine("Thread2 has termminated.");
        }
        else
        {
        Console.WriteLine("The timeout has elapsed and Thread1 will resume.");
        }
    }

    Thread.Sleep(4000);
    Console.WriteLine("\nCurrent thread:{0}", Thread.CurrentThread.Name);
    Console.WriteLine("Thread1:{0}", thread1.ThreadState);
    Console.WriteLine("Thread2:{0}", thread2.ThreadState);
}

结果

  1. Thread.Sleep()方法会暂停当前的线程,并等一段时间,参数可以时毫秒,也可以是TimeSpan
    例子
public static void Main()
{
    for(int i=0;i<5;i++)
    {
        Console.WriteLine("Sleep for 2 seconds");
        Thread.Sleep(2000);
    }

    Console.WriteLine("Main thread exits");
}

结果就是每过 2000 毫秒输出一个 Sleep for 2 seconds ,最后输出 Main thread exits

  1. 注意:
    1. Thread.Sleep(0)这样调用会导致线程立即放弃当前的时间片,自动将CPU移交给其它线程
    2. Thread.Yield()做同样的事情,但是它只会把执行交给同一处理器上的其它线程
    3. 当等待Sleep或Join的时候,线程处于阻塞的状态
    4. Sleep(0)或Yield有时在高级性能调试的生产代码中很有用。它也是一个很好的诊断工具,有助于发现线程安全问题:
      如果在代码中的任何地方插入Threa.Yield()就破坏了程序,那么你的程序几乎肯定有bug

阻塞 Blocking

  1. 如果线程的执行由于某种原因被导致暂定,那么就认定该线程被阻塞了
    例如在Sleep()或者通过Join()等待其它线程的结束
  2. 被阻塞的线程会立即将其处理器的时间片生成给其它线程,从此不会再消耗处理器时间,直到满足其阻塞条件为止
    可以通过ThreadState这个属性来判断线程是否处于被阻塞的状态
    bool blocked=(someThread.ThreadState & ThreadState.WaitSleepJoin)!=0;
    必须这样写

ThreadState

  1. ThreadState是一个flags enum,通过按位的形式,可以合并数据的选项
  2. 但是它大部分的枚举值都没什么用,下面的代码将ThreadState剥离为四个最有用的值之一:Unstarted、Running、WaitSleepJoin和Stopped
public static ThreadState SimpleThreadState(ThreadState ts)
{
    return ts&(ThreadState.Unstarted|ThreadState.WaitSleepJoin|ThreadState.Stopped);
}
  1. ThreadState属性可用于诊断的目的,但不适用于同步,因为线程状态可能会在测试ThreadState和对该信息进行操作之间发生变化

解除阻塞 Unblocking

  1. 当遇到下列四种情况的时候,就会解除阻塞:
    阻塞条件被满足
    操作超时(如果设置超时的话)
    通过Thread。Interrupt()进行打断
    通过Thread.Abort()进行中止

上下文切换

  1. 当线程阻塞或解除阻塞时,操作系统将执行上下文切换。这会产生少量开销,通常为1或2微秒

I/O-bound vs Compute-bound(或CPU-bound)

  1. 一个花费大部分时间等待某事发生的操作称为I/O-bound(什么事都不干)
    I/O绑定操作通常涉及输入或输出,但这不是硬性要求:Thread.Sleep()也被视为I/O-bound
  2. 相反,一个花费大部分时间执行CPU密集型工作的操作称为Compute-bound(一直在运行)

阻塞 vs 忙等待(自旋) Blocking vs Spinning

  1. IO-bound操作的工作方式有两种
    在当前线程上的同步等待
      Console.ReadLine(),Thread.Sleep(),Thread.Join()...
    异步的操作,在稍后操作完成时触发一个回调动作
  2. 同步等待的I/O-bound操作将大部分时间花在阻塞线程上
  3. 它们也可以周期性的在一个循环里进行“打转(自旋)”
//不彻底的忙等待(自旋)
while(DateTime.Now < nextStartTime)
{
      Thread.Sleep(100);
}
//彻底的忙等待(自旋),CPU一直在工作
while(DateTime.Now < nextStartTime);
  1. 在忙等待和阻塞方面有有一些细微差别
    首先,如果您希望条件很快得到满足(可能在几微秒之内),则短暂自旋可能会很有效,因为它避免了上下文切换的开销和延迟
      .NET Framework提供了特殊的方法和类来提供帮助SpinLock和SpinWait
    其次,阻塞也不是零成本。这是因为每个线程在生存期间会占用大约 1 MB的内存,并会给CLR和操作系统带来持续的管理开销
      因此,在需要处理成百上千个并发操作的大量I/O-bound程序的上下文中,阻塞可能会很麻烦
      所以,此类程序需要使用基于回调的方法,在等待时完全撤销其线程

线程和阻塞 结束

posted @ 2021-02-05 16:36  .NET好耶  阅读(6263)  评论(1编辑  收藏  举报