C# 语法 の 异步
文章目录
1.什么是线程?
- 每个线程都是在进程内执行。
- 每个线程都是独立的。
- 两个线程交叉点的那个数据,该数据被称为是数据共享状态。
- 当一个线程与另一个线程上代码的执行交织的那一点,就被称为线程被抢占了。
看下边的例子:
class Program
{
static void Main(string[] args)
{
Thread t = new Thread(WriteY);
t.Name = " cw Y";
t.Start();
for (int i = 0; i < 1000; i++)
{
Console.Write("X");
}
}
static void WriteY()
{
for (int i = 0; i < 1000; i++)
{
Console.Write("Y");
}
}
}
最后输出的结果:
注意:
- 在单核计算机上,操作系统为每个线程来分配 “时间片” 来模拟并发 。
- 在多核处理器上,两个线程才是真正的并行执行 。
注意线程的几个属性
- 线程一旦开始执行,IsAlive 就是 true ,线程结束就是False 。
- 线程结束的条件是:线程构造函数出入的委托执行完了。
- 线程一旦结束,就无法再重启。
- 每个线程都有一个 Name 属性,通常用于调试。且 Name 属性只能设置一次,以后再更改就会抛出异常。
- 静态的Thread.CurrentThread 属性,会返回当前执行的线程。
2.Joint属性
1. t.join 即 要等 t 这个线程执行完以后,再执行下边的线程;
代码如下:
class Program
{
static void Main(string[] args)
{
Thread.CurrentThread.Name = "Main thread ";
Thread t = new Thread(WriteY);
t.Name = "Y Thread";
t.Start();
t.Join();
for (int i = 0; i < 1000; i++)
{
Console.Write("X");
}
}
static void WriteY()
{
for (int i = 0; i < 1000; i++)
{
Console.Write("Y");
} } }
输出如下:
2.有两个方法,threat1, threat2。。如果在 Thread1 方法里面添加 Thread2.Join ()。那么就要等 Thread2方法执行完以后,再执行Thread1 。 Thread.Sleep(2000).是指当前的线程等待两秒。
在主线程里面调用 t.join() 这个方法,那么主线程就要等t线程进行完以后再进行继续进行。
具体看代码:
class Program
{
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();
}
private static void ThreadProc()
{
Console.WriteLine($"Current thread: {Thread.CurrentThread.Name}");
if (Thread.CurrentThread.Name=="Thread1"
&&thread2.ThreadState!=ThreadState.Unstarted)
{
thread2.Join();
}
Thread.Sleep(2000);
Console.WriteLine($"当前线程名{Thread.CurrentThread.Name}");
Console.WriteLine($"thread1 的状态{thread1.ThreadState}");
Console.WriteLine($"thread2 的状态{thread2.ThreadState}");
}
}
输出结果如下:(思考背后发生的流程)
3.Join()后边可以添加一个 超时参数 用毫秒或者 TimeSpan 都可以
如果返回值true 说明是线程结束了,如果是false,说明超时了;
下边相对于上边来讲,就是如果说这个线程超过2s了,就走上边 if 语句,如果没有超过两秒,就走else 语句。
class Program
{
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();
}
private static void ThreadProc()
{
Console.WriteLine($"Current thread: {Thread.CurrentThread.Name}");
if (Thread.CurrentThread.Name=="Thread1"
&&thread2.ThreadState!=ThreadState.Unstarted)
//意思就是等他两秒,如果两秒之内结束了,就走第一行,
//如果两秒之内没有结束,就走第二行。
if (thread2.Join(2000))
{
Console.WriteLine("两秒之内结束了");
}
else
{
Console.WriteLine("两秒没有结束");
}
Thread.Sleep(2000);
Console.WriteLine($"当前线程名{Thread.CurrentThread.Name}");
Console.WriteLine($"thread1 的状态{thread1.ThreadState}");
Console.WriteLine($"thread2 的状态{thread2.ThreadState}");
}
}
4.阻塞
如果线程的执行由于某些原因导致暂停,那么就认为该线程被阻塞了
例如:使用 sleep 方法和 Join 方法等待其他线程结束。
解除阻塞的方法
- 阻塞条件被满足
- 操作超时(如果设置超时的话)
- 通过 Thread.Interrupt() 进行打断
- 通过 Thread.Abort() 进行中止
5.本地 VS 共享状态
本地独立
CLR 为每个线程分配自己的内存栈(stack),以便使本地变量保持独立
Shared 共享
如果多个线程都引用了同一个对象实例,那么他们就共享了数据。
如果更改了线程的等待时间,那么打印 Down 的次数就是不确定的。所以说他是不安全的
为了避免这种情况,可以使用锁定和线程安全
在读取和写入共享数据的时候,可以通过使用互斥锁(exclusive lock),就可以修复前边例子的问题。
C#中使用 lock 语句来加锁。当两个线程同时竞争一个锁的时候(锁可以基于任何引用对象),一个线程会阻塞或等待,直到锁编程可用状态。
class Program
{
static bool down;
static readonly object _locker = new object();
static void Main(string[] args)
{
new Thread(Go).Start();
Go();
}
static void Go()
{
lock (_locker)
{
if (!down)
{
Console.WriteLine("DOWN");
down = true;
} } } }
lock 里面的内容,在同一个时间内,只能被一个线程调用
6.如何往线程传参
往线程里传参,最简单的方法是使用 lambda 表达式
static void Main(string[] args)
{
Thread t = new Thread(()=>PrintAA("你好","世界"));
t.Start();
}
static void PrintAA(string message,string aa)
{
Console.WriteLine(message+aa);
}
当然,也可以直接把所有的逻辑都写在 lambda 里面
7.多线程中的异常处理
在创建线程时,在作用范围内的 try / catch / finally 块,在线程开始执行后就与线程无关了
8.富客户端应用程序的线程
- 在 WPF , UWP , WinForm 等类型的程序中,如果主线程在执行耗时的操作,就会导致整个程序没有响应。
- 针对这种耗时的操作,一种流行的做法就是启动一个 worker 线程。
9.线程池
- 当开始一个线程的时候,将花费几百微妙来组织类似以下的内容: 一个新的局部变量栈。
- 线程池可以节省这种开销: 通过预先创建一个可循环使用线程的池来减少这一开销。
- 线程池对于高效的并行编程和细颗粒度并发是必不可少的
- 线程池允许在不被线程启动的开销淹没的情况下,进行短期操作。(此处的短期操作是指运行时间还没有线程开启的时间长)
使用线程池需要注意的几点
- 不可以设置线程的 Name
- 池线程都是后台线程
- 阻塞线程可能使性能降低
- 可以自由的更改池线程的优先级
- 可以通过 Thread.CurrentThread.IsThreadPoolThread 属性来判断是否执行在池线程上。
显示的在池线程运行代码的方式就是使用 Task.Run
线程池中的整洁
- 线性池能确保临时超出 计算-Bound 的工作不会导致CPU超额定语
- CPU超额订阅:活跃的线程超过 CPU 核数,操作系统就需要对线程进行时间切片
- 超额订阅对性能影响很大,时间切片需要昂贵的上下文切换,并且可能使 CPU 缓存失效,而 CPU 缓存对于现在处理器性能至关重要
10.Task
1.使用 Thread 来处理,和创建并发的一种低级别的工具,他有一些限制:
- 虽然可以很方便的传入数据,但是当 Join 的时候,很难从线程获得返回值(可能需要设置一些共享字段 )
- 如果抛出异常,捕获和传播异常都很麻烦
- 很难使用较小的并发来组建大型的并发
Task 可以很好的解决上述问题,
他是一个相对高级的抽象
开启一个Task 最简单的方法就是使用 Task.Run ( )
代码如下:(传入一个委托即可)
static void Main(string[] args)
{
Task.Run(() => Console.WriteLine("你好"));
}
但是运行,却没有输出,这是为什么呢?
Task 默认使用线程池,也就是后台线程,当主线程结束时,你说创建的所有 task 都会结束
这个Task还没运行完,主线程就已经运行完了,所以这种情况下,Task就默认不再执行了,这种情况下,可以让主线程 “阻塞”
static void Main(string[] args)
{
Task.Run(() => Console.WriteLine("你好"));
Console.ReadLine();
}
- Task.Run 返回一个 Task 对象,可以使用它来监视其过程,在 Task.Run 之后,我们没有调用Start ,因为该方法创建的是 “热” 任务(hot task)
- 可以通过Task的构造函数创建 “冷” 任务(cold task),但是实际上很少这么做
- 可以使用 Task 的 Status 属性来跟踪 task 的执行状态
wait 等待
调用task 的Wait方法会进行阻塞,直到操作完成,相当于调用了 thread 上的Join 方法。
看代码:
static void Main(string[] args)
{
Task task = Task.Run(()=>
{
Thread.Sleep(3000);
Console.WriteLine("FOO");
}
);
Console.WriteLine(task.IsCompleted);
task.Wait();
Console.WriteLine(task.IsCompleted);
}
执行结果:
Task的返回值
- Task 有一个泛型子类叫做 Task<TResult>,他允许发出一个返回值
- 使用Func<TResult> 委托或者兼容的Lambda 表达式来调用 Task.Run 就可以得到Task <TResult>。
- 随后,可以使用Result 属性来获得返回的结果。如果 task 还没完成操作,访问 Result 属性会阻塞该线程,直到该 task 完成操作。
代码:
static void Main(string[] args)
{
Task<int> task = Task.Run(()=>
{
Console.WriteLine("foo");
return 3;
});
int result = task.Result;
// 如果此时 Task 没有完成,那么就会阻塞
Console.WriteLine(result);
}
Task 的异常
与 Thread 不一样, Task 可以很方便的传播异常。
如果你的 task 里面抛出了一个未处理的异常(故障),那么该异常就会重新被抛给
- 调用了 wait() 的地方
- 访问了 Task <TResult> 的 Result 属性的地方
代码例子
注意是哪里抛出了异常
is 是判断是否兼容,是的话就是 true 错的话就是 false
CLR 将异常包包裹在 AggregateException 里,以便在并行编程场景中发挥很好的作用。
static void Main(string[] args)
{
Task task = Task.Run(() => { throw null; }) ;
try
{
//异常会被抛送到有 wait 的地方
task.Wait();
}
catch (AggregateException aex)
{
// 看 A 是否是 B ,如果不是,抛出异常。
if (aex.InnerException is NullReferenceException)
{
Console.WriteLine("null");
}
else
{
throw;
} } }
参考资料
【1】is as 的区别
【2】https://www.bilibili.com/video/BV1Zf4y117fs/?p=16