异步编程初探

本文深入探讨了异步编程的概念,从线程创建、线程状态管理到线程池的使用,详细阐述了线程安全、阻塞、任务(Task)的使用以及异常处理。此外,还介绍了异步函数的编写、await关键字的工作原理和优化技巧,如ValueTask的使用,以及取消和进度报告的机制。最后,讨论了基于Task的异步模式(TAP)及其组合器,如Task.WhenAny和Task.WhenAll。
摘要由CSDN通过智能技术生成

异步编程基础


注:前段时间学习杨旭老师出的“C# 异步编程基础(完结)”视频,特地总结下笔记, 视频地址点这里

线程:创建线程

什么是线程

  • 线程是一个可执行路径,它可以独立于其他线程执行
  • 每个线程都在操作系统的进程(Process)内执行,而操作系统进程提供了程序运行的独立环境
  • 单线程应用,在进程的独立环境里只跑一个线程,所以该线程拥有独占权
  • 多线程应用,单个进程中会跑多个线程,它们会共享当前的执行环境(尤其是内存),内存中的数据被称作共享的状态

例子

static void Main(string[] args)
{
   
   Thread thread = new Thread(WriteY);
    thread.Name = "Y Thread ...";
    thread.Start();

    for (int i = 0; i < 1000; i++)
    {
   
        System.Console.Write("x");
    }

}

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

// 程序运行结果
// xxxxxxxxxxxyyyxxxxxx...xxxyyyxxxxxxxxxxyyyxxxxxx
  • 在单核计算机上,操作系统必须为 每个线程分配“时间片”(在windows中通常为20ms) 来模拟并发,从而导致重复的x和y块
  • 在多核或多处理器计算机上,这两个线程可以真正的并行执行(可能受到计算机上其它活动进程的竞争)

线程被强占

线程的执行与另外一个线程上代码的执行交织的那一点

线程的属性

  • 线程一旦开始执行,IsAlive就是true,线程结束就变成false
  • 线程结束的条件就是:线程构造函数传入的委托结束了执行
  • 线程一旦结束,就无法再重启
  • 每个线程都有个Name属性,通常用于调试
    • 线程的Name只能设置一次,以后更改会抛出异常
  • 静态的Thread.CurrentThread属性,会返回当前执行的线程
// 针对Thread.CurrentThread 的例子
static void Main(string[] args)
{
   
    Thread.CurrentThread.Name = "Main Thread ...";

    Thread thread = new Thread(WriteY);
    thread.Name = "Y Thread ...";
    thread.Start();

    System.Console.Write(Thread.CurrentThread.Name);

    for (int i = 0; i < 1000; i++)
    {
   
        System.Console.Write("x");
    }

}

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

Thread.Join() & Thread.Sleep()

Join and Sleep

  • 线程调用了Join方法后,只有当线程的所有程序执行完毕后,其它线程才会继续执行
// Join 简单例子
static void Main(string[] args)
{
   
    Thread t1 = new Thread(Go);
    t1.Start();
    t1.Join();
    System.Console.WriteLine("Thread t1 has ended");
}

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

// 程序运行结果
// YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
// ...
// YYYYYYYYYYYYYYYYYYYYYYYYYYYYYThread t1 has ended
// Join 复杂例子
static Thread thread1, thread2;

static void Main(string[] args)
{
   
    thread1 = new Thread(ThreadProc);
    thread1.Name = "thread1";
    thread1.Start();

    thread2 = new Thread(ThreadProc);
    thread2.Name = "thread2";
    thread2.Start();
}

static void ThreadProc()
{
   
    System.Console.WriteLine("\nCurrent Thread {0}", Thread.CurrentThread.Name);

    System.Console.WriteLine("Thread2 State : {0}", thread2.ThreadState);

    if (Thread.CurrentThread.Name == "thread1" &&
        thread2.ThreadState != ThreadState.Unstarted)
    {
   
        thread2.Join();
    }

    // Thread.Sleep(2000);
    System.Console.WriteLine("\nCurrent Thread : {0}", Thread.CurrentThread.Name);
    System.Console.WriteLine("Thread1 State : {0}", thread1.ThreadState);
    System.Console.WriteLine("Thread2 State : {0}", thread2.ThreadState);
}

// 程序运行结果
// Current Thread thread1
// Thread2 State : WaitSleepJoin
// Current Thread thread2
// Thread2 State : Running
// Current Thread : thread2
// Thread1 State : WaitSleepJoin
// Thread2 State : Running
// Current Thread : thread1
// Thread1 State : Running
// Thread2 State : Stopped

添加超时

  • 调用 Join 的时候,可以设置一个超时,用毫秒或者TimeSpan都可以
    • 如果返回 true ,那就是线程结束了;如果超时了,就返回 false
  • Thread.Sleep() 方法会暂停当前线程,并等一段时间,参数可以是毫秒,也可以是TimeSpan

注意⚠️:

  • Thread.Sleep(0) 这样调用会导致线程立即放弃本身当前的时间片,自动将 CPU 移交给其它线程
  • Thread.Yield() 做同样的事情,但是它只会把执行交给同一处理器上的其它线程
  • 当等待Sleep和Join的时候,线程处于阻塞的状态
// Join 超时 毫秒例子🌰
static Thread thread1, thread2;

static void Main(string[] args)
{
   
    thread1 = new Thread(ThreadProc);
    thread1.Name = "thread1";
    thread1.Start();

    thread2 = new Thread(ThreadProc);
    thread2.Name = "thread2";
    thread2.Start();
}

static void ThreadProc()
{
   
    System.Console.WriteLine("\nCurrent Thread {0}", Thread.CurrentThread.Name);

    System.Console.WriteLine("Thread2 State : {0}", thread2.ThreadState);

    if (Thread.CurrentThread.Name == "thread1" &&
        thread2.ThreadState != ThreadState.Unstarted)
    {
   
        if (thread2.Join(2000))
        {
   
            System.Console.WriteLine("Thread2 has termminated.");
        }
        else
        {
   
            System.Console.WriteLine("The timeout has elapsed and Thread1 will resume.");
        }
    }

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

// 程序运行结果
// Current Thread thread2

// Current Thread thread1
// Thread2 State : WaitSleepJoin
// Thread2 State : Running

// Current Thread : thread2
// Thread1 State : WaitSleepJoin
// Thread2 State : Running
// Thread2 has termminated.

// Current Thread : thread1
// Thread1 State : Running
// Thread2 State : Stopped
// Join 超时 TimeSpan 例子🌰
static TimeSpan waitTime = new TimeSpan(0, 0, 1);
static void Main(string[] args)
{
   
    Thread newThread = new Thread(Work);
    newThread.Start();

    if (newThread.Join(waitTime - waitTime))
    {
   
        System.Console.WriteLine("New thread terminated");
    }
    else
    {
   
        System.Console.WriteLine("Join timed out.");
    }
}
static void Work()
{
   
    Thread.Sleep(waitTime);
}

// 输出结果
// Join timed out.

阻塞 Blocking

阻塞

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

ThreadState

  • ThreadState 是一个flags enum,通过按位的形式,可以合并数据的选项
  • 常用的ThreadState的值:Unstarted、Running、WaitSleepJoin、Stopped

解除阻塞(Unblocking)

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

上下文切换

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

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

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

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

  • I/O-bound 操作的工作方式有两种:
    • 在当前线程上同步的等待
      • Console.ReadLine(),Thread.Sleep(),Thread.Join()…
    • 异步的操作,在稍后操作完成时触发一个回调动作
  • 同步等待的 I/O-bound 操作将大部分时间花在阻塞上
  • 它们也可以周期性的在一个循环里进行“打转(自旋)”
while (DateTime.Now < nexeStartTime)
	Thread.Sleep(100);

while (DateTime.Now < nexeStartTime);
  • 在忙等待和阻塞方面有一些细微差别
    • 首先,如果您希望条件很快得到满足(在几微秒之内),则短暂的自旋可能更有效,因为它避免了上下文的切换的开销和延迟
      • .NetFramework 提供了特殊的方法和类来提供帮助 SpingLock 和 SpinWait
    • 其次,阻塞也不是零成本。这是因为每个线程在生存期间会占用大约1MB的内存,并会给CLR和操作系统带来持续的管理开销
      • 因此,在需要处理成百上千个并发操作的大量 I/O-boung 程序的上下文中,阻塞可能会很麻烦
      • 所以,此类程序需要使用回调的方法,在等待时完全撤销其线程

什么是线程安全

本地 vs 共享的状态(Local vs Shared State)

  • Local 本地独立
    • CLR 为每个线程分配自己的内存栈(Stack),以便使本地变量保持独立
  • Shared 共享
    • 如果多个线程都引用同一个对象的实例,那么它们就共享这个对象的数据
    • 被Lambda表达式或匿名委托所捕获的本地变量,会被编译器转化为字段(field),所以也会被共享
    • 静态字段(field)也会在线程间共享数据

线程安全(Thread Safety)

// 线程不安全的例子
static bool _done = false;

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

static void Go()
{
   
   if (!_done)
   {
   
       System.Console.WriteLine("Done");

       Thread.Sleep(50);

       _done = true;
   }
}

// 输出结果
// Done
// Done

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

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

static readonly object _object = new object();

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

static void Go()
{
   
    lock (_object)
    {
   
        if (!_done)
        {
   
            System.Console.WriteLine("Done");
            Thread.Sleep(50);
            _done = true;
        }
    }
}

// 结果
// Done

向线程传递数据 & 异常处理

向线程传递数据

  • 往线程的启动方法里面传递数据,最简单的方法就是使用 Lambda 表达式,在里面使用参数调用方法
static void Main(string[] args)
{
   
    new Thread(() => Go("hi xiao ming")).Start(); 
}
static void Go(string msg)
{
   
    System.Console.WriteLine(msg);
}
  • 在C# 3.0 之前,没有 Lambda 表达式,可以使用 Thread 的 Start 方法来传递参数
static void Main(string[] args)
{
   
    new Thread(Go).Start("hi xiao ming");
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值