异步编程基础
注:前段时间学习杨旭老师出的“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");