异步编程初探

异步编程基础

    • 线程:创建线程
      • 什么是线程
      • 例子
      • 线程被强占
      • 线程的属性
    • Thread.Join() & Thread.Sleep()
      • Join and Sleep
      • 添加超时
    • 阻塞 Blocking
      • 阻塞
      • ThreadState
      • 解除阻塞(Unblocking)
      • 上下文切换
      • I/O-bound vs Compute-bound(或 CPU-Bound)
      • 阻塞Blocking vs 忙等待Spinging(自旋)
    • 什么是线程安全
      • 本地 vs 共享的状态(Local vs Shared State)
      • 线程安全(Thread Safety)
      • 锁定与线程安全简介(Locking & Thread Safety)
    • 向线程传递数据 & 异常处理
      • 向线程传递数据
      • Lambda 表达式与被捕获的变量
      • 异常处理
    • 前台线程 VS 后台线程
      • 前台(Foreground)和后台(Background)线程(Threads)
    • 线程优先级
      • 线程优先级
      • 提升线程的优先级
    • 信号简介
    • 富客户端应用处理耗时操作的一种办法
    • Synchronization Context
    • 线程池
      • 线程池
      • 使用线程池需要注意的几点
      • 进入线程池
      • 谁使用了线程池
      • 线程池中的整洁
        • CLR 对于保持线程池整洁的策略
    • 开始一个Task
      • Thread 的问题
      • Task class
      • 开始一个Task(Task.Run)
      • 长时间运行的任务(Long-running tasks)
    • Task 的返回值
    • Task 的异常
      • Task 的异常
      • Task 的异常
      • 异常与“自治”的Task
      • 未观察到的异常
    • Coniuation
      • awaiter
      • 如果发生故障
      • 非泛型Task
      • 同步上下文
      • ContinueWith
    • TaskCompletionSource
      • TaskCompletionSource
      • 使用TaskCompletionSource
      • TaskCompletionSource的真正魔力
      • Task.Delay
    • 同步和异步
      • 什么是异步编程
      • 异步编程的用途
      • 经验之谈
    • 异步和continuation 以及语言的支持
      • 异步和continuation
      • 语言对异步的支持非常重要
    • await
      • 异步函数
      • awaiting
      • async 修饰符
      • 异步方法如何执行的
      • 可以 await 什么?
      • 捕获本地状态
      • await 之后在哪个线程上执行
    • 编写异步函数
      • 编写异步函数
      • 返回Task< TResult>
      • C#中如何设计异步函数
      • 编译器能对异步函数生成 Task 意味着什么
      • 异步调用图的执行
      • 并行(Parallelism)
      • 异步 Lambda 表达式
    • 异步中的同步上下文
    • 优化:同步完成
    • ValueTask< T>
      • ValueTask< T>
      • 使用 ValueTask< T> 的注意事项
      • 避免过度的弹回
    • 取消
      • 取消(cancellation)
      • CancelationToken 和 CancelationTokenSource
      • 获取 CancellationToken
      • Delay
      • 同步方法
      • 其它内容
    • 进度报告
      • 进度报告
      • IPROGRESS< T> 和 PROGRESS< T>
        • 使用 IPROGRESS< T>
    • TAP
      • Task-Based Asynchronous Pattern
    • Task 组合器
      • WhenAny
      • WhenAll

注:前段时间学习杨旭老师出的“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 语句来加锁
  • 当两个线程同时竞争一个锁的时候(锁可以基于任何引用类型对象),一个线程会等待或阻塞,直到锁变成可用状态
  • 在多线程上下文中,以这种方式避免不确定性的代码就

你可能感兴趣的:(线程,.NET,C#,c#,多线程)