异步编程

Thread

线程是一个可执行路径,可以独立于其他线程执行。

线程被抢占:当前线程与另外一个线程执行交织的那一刻。

线程的属性

  • IsAlive:线程一旦开始执行,IsAlive就是True,线程结束就变成了False。线程结束的条件,线程构造函数传入的委托方法执行结束。
  • 线程一旦结束就无法再开启。
  • Name:只能设置一次,以后再更改就会抛出异常。
  • CurrentThread:返回当前执行的线程。

常用方法

  • Join():等待当前线程执行完成后再开始后续的执行。可设置一个超时时间,执行超时返回False。
  • Thread.Sleep():暂停当前线程,并等待指定的毫秒数。Thread.Sleep(0)会导致当前线程立即放弃当前的时间片,自动将cpu执行权移交给其他线程。
  • Thread.Yield():把当前线程的执行权交给同一处理器上的其他线程
  • Sleep(),Join()会让线程处于阻塞状态。

线程执行状态

阻塞(block):被阻塞的线程会立即将cpu的时间片生成给其他线程,从此不再消耗cpu时间,直到满足其阻塞条件为止才开始继续执行。

  • 阻塞判断:

    1
    bool blocked = (thread1.ThreadState & ThreadState.WaitSleepJoin) != 0;
  • 解除阻塞:

    • 阻塞条件被满足
    • 操作超时
    • Thread.Interrput()
    • Thread.Abort()
  • 阻塞或接触阻塞,操作系统会执行上下文切换,会产生少量开销,通常为1或2微秒。

  • I/O-bound与Cpu-bound

    • I/O-bound:花费大部分时间等待某事发生的操作,例如输入输出,Thead.Sleep()。

    • Cpu-bound:花费大部分时间执行cpu密集型工作的操作。

线程安全

本地(Local)与共享(Shared)

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

  • Shared:

    • 多个线程引用同一个对象的实例,那么他们就共享了数据。

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

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

在读取和写入共享数据的时候,通过使用互斥锁(exclusive lock),来解决线程安全问题。

C#中使用lock语句来加锁,当两个线程同时竞争一个锁(锁可以基于任何引用对象)时,其中一个线程就会等待或阻塞,直到锁变成可用状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class TestThreadSafe
{
static bool flag;
static readonly object _lock = new object();

static void Go()
{
lock (_lock)
{
if (!flag)
{
Console.WriteLine(flag);
// Thread.Sleep(1000);
flag = true;
}
}
}

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

前台线程与后台线程

  • 默认情况下,手动创建的线程就是前台线程。只要前台线程在运行,应用程序就还在运行;前台线程终止后,其余的后台线程也会全部终止,程序就会退出。
  • 以在任何时候将前台线程修改为后台线程,方式是设置Thread.IsBackground = true。

信号

让线程一直处于等待状态,直到接受到其他线程发来的信号(signaling),才会继续执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Program
{
static void Main(string[] args)
{
var signal = new ManualResetEvent(false);
new Thread(()=> {
Console.WriteLine("等待信号~");
signal.WaitOne();
signal.Dispose();
Console.WriteLine("获取到了信号!");
}).Start();
Thread.Sleep(3000);
signal.Set();// 打开信号
// signal.Reset();// 关闭信号
}
}

线程池

  • 不可以设置线程池的Name
  • 池线程都是后台线程
  • 阻塞池线程可使性能降级
  • Thread.CurrentThread.IsThreadPoolThread 判断是否执行在池线程上

Task

Thread的问题

线程是用来创建并发的一种低级别工具,它有一些限制。尤其是:

  • 虽然开始线程的时候可以方便的传入数据,但是当Join的时候,很难从线程中获得返回值。
    • 可能需要设置一些共享字段
    • 如果操作抛出异常,捕获和传播该异常都很麻烦
  • 无法告诉线程在结束时开始做另外的工作,必须进行Join操作(在进程中阻塞当前线程)。
  • 很难使用较小的并发来组件大型的并发。
  • 导致了对手动同步的更大依赖以及随之而来的问题。

Task类

Task类可以很好的解决Thread的问题。它代表了一个并发操作(可能由Thread支持,或不由Thread支持)。

  • Task.Run()开启一个任务
  • Task默认使用线程池,也就是后台线程。主线程结束,创建的所有Task都会结束
  • Task.Run()返回一个Task对象,可以用来监视其过程
  • Task.Wait()会阻塞当前线程,直到Task执行完成

默认情况下,CLR在线程池中运行Task,这非常适合短时间运行的Cpu-Bound类工作。
针对长时间运行的任务或者是阻塞操作,可以不采用线程池的方式来创建任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Program
{
static void Main(string[] args)
{
// 执行短时间的任务 采用默认的线程池创建方式
Task task = Task.Run(() => Console.WriteLine("执行Task~"));
task.Wait();

// 执行长时间的任务 不采用线程池
Task taskLong = Task.Factory.StartNew(() => {
Console.WriteLine("执行长时间任务~");
Thread.Sleep(3000);
}, TaskCreationOptions.LongRunning);

}
}

如果同时运行多个long-running tasks,尤其是由处于阻塞状态的,那么性能就会受到很大的影响,这时可采用:

  • 如果任务是IO-Bound,等待一个在 async 方法中返回 TaskTask<T> 的操作。
  • 如果任务是Cpu-Bound,等待一个使用 Task.Run方法在后台线程启动的操作。

同步与异步

  • 同步操作会在返回调用者之前完成它的工作
  • 异步操作会在返回调用者之后去做它的(大部分)工作
    • 异步方法会启用并发,因为它的工作会与调用者并行执行
    • 异步方法通常很快就会返回到调用者,所以叫非阻塞方法
  • 异步不会提升单个应用程序的运行速度,但是能提升服务器的并发访问量。

async

  • async关键字修饰的方法称为异步方法。
  • 异步方法的返回值一般是Task<T>,T是真正的返回值类型。惯例:异步方法名称一般以Async结尾。
  • 即使异步方法没有返回值,也最好声明为非泛型的Task
  • 调用泛型方法时,一般在方法前加上await关键字,这样拿到的返回值直接就是泛型指定的T类型。
  • 一个方法使用await关键字后,这个方法必须声明使用async修饰。

await

await关键字简化了附加continuation的过程

await关键字后面expression会马上返回,直接执行expression后续的语句,expression执行完成后自动回调。

await调用的等待期间,.NET会把当前的线程返还给线程池,等异步方法调用执行完毕后,再从线程池中取出一个线程执行后续的代码。

Task 与async Task

Task:在方法前面加Task或是Task<T>,就是表明方法是可等待的。也是C#中的一种Task异步编程模式,结合await可方便的进行异步并发编程。

async Task:表明方法就是异步方法。