0%

真会C#?——异步编程2

线程池(Thread Pool)

  • 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中

  • 当开始一个线程的时候,将花费几百微秒来组织一个心得局部变量栈,而使用线程池可以节省这种组织开销,即通过预先创建一个可循环使用线程的池来减少这一开销。

  • 线程池对于高效的并行编程和细粒度并发是必不可少的,它允许在不被线程启动的开销淹没的情况下运行短期操作(这种短期操作开销很小)。

使用线程池需要注意的地方

  • 不可以设置线程池的Name
  • 池线程都是后台线程
  • 阻塞池线程可能使性能降级
  • 可以自由地更改池线程的优先级,当线程释放回池时优先级将还原
  • 可以使用Thread.CurrentThread.IsThreadPoolThread属性来判断是否执行在线程池上。

有哪些用了线程池

  • WCF、Asp.Net Core、ASMX等应用服务器
  • System.Timers.Timer、System.Threading.Timer
  • 并行编程结构
  • 。。。

线程池中的整洁

  • 如果项充分利用CPU,那么保持线程池的“整洁”是非常重要的。

超额订阅

  • CPU的超额订阅:活跃的线程超过CPU的核数,操作系统就需要对线程进行时间切片。

  • 线程池提供了另一个功能,即确保临时超出的Compute-Bound工作不会导致CPU的超额订阅。由于超额订阅需要增加时间切片,时间切片又需要昂贵的上下文切换,并且可能使CPU缓存失效(至关重要)。

CLR的策略

  • C#中使用CLR通过对任务排队并对其启动进行节流限制来避免线程池中的超额订阅。它首先运行尽可能对的并发任务,然后通过一些算法调整并发线程的级别,并在特定的方向上不断调整工作负载,具体就是确保并发线程的运行始终追随最佳的性能曲线。
  • 如果下面两点能够满足,则CLR的策略将发挥出最佳的效果:
    1. 工作项大多是短时间(<250毫秒,理想情况下<100毫秒)运行的,因此CLR有更多的机会进行测量和调整。
    2. 大部分时间都被阻塞的工作项不会主宰线程池。

Task

Thread的问题

  • Thread是用来创建concurrency(并发)的一种低级别工具,他有一些限制:
    1. Thread在进行Join的时候,很难从线程获得返回值,解决这个问题可能需要设置一些共享变量。
    2. 如果操作抛出异常,捕获和传播该异常都会很麻烦。
    3. 无法告诉线程在结束时开始做另外的工作,你必须使用Join来完成这个操作,这将会导致其他线程不能执行。
    4. 很难使用较小的并发来组建大型的并发,导致了对手动同步的更大依赖。

Task class

  • Task类是一个相对高级的并发工具,它是在System.Threading.Tasks命名空间下的,它代表了一个并发操作,可以很好地解决上述问题,它有一些操作可能由Thread支持,有可能不由Thread支持。
  • Task是可以组合的,可以使用Continuation把它们串成链。
  • Tasks可以使用线程池来减少启动延迟,它也可以利用回调的方式,在等待IO绑定操作时完全避免线程。

开始一个Task

  • 开始一个Task的最简单方法是使用Task.Run这个静态方法,只需要传入一个委托即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Program
{
static void Main(string[] args)
{
/**
* 创建了一个任务,在创建任务时会自动使用线程池,线程池里面有一个线程(也就是委托实例)
* 主线程执行完后Task中的线程还没有执行结束就被kill了,这时并不会输出线程中的那句话
*/
Task.Run(() => { Console.WriteLine("Hello Task!"); });
//这时要阻塞一下主线程才可以执行线程中的语句
// Console.ReadLine();
Thread.Sleep(1000);
}
}
  • Task默认使用线程池,也就是后台线程:当主线程结束时,你创建的所有tasks都会结束。
  • Task.Run返回了一个Task对象,可以使用它来监视线程的执行过程
  • 使用Task.Run来启动线程的任务是“热”任务,它并不需要调用Start,而使用Task的构造器创建的任务为“冷”任务,需要使用Start来启动线程池中的线程,但很少使用冷任务。
  • 可以通过Task的Status属性来跟踪task的执行状态;

Wait

  • 调用task的Wait方法会进行阻塞直到Task中的并发操作完成,这就相当于调用了thread上的Join方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Program
{
static void Main(string[] args)
{
var task = Task.Run(() =>
{
Thread.Sleep(3000);
Console.WriteLine("Hello Task!");
});
//task的状态:WaitingToRun
Console.WriteLine(task.Status);
//是否完成操作:false
Console.WriteLine(task.IsCompleted);
//task的状态:Running
Console.WriteLine(task.Status);
//阻塞直至task完成操作
task.Wait();
//是否完成操作:true
Console.WriteLine(task.IsCompleted);
//task的状态:RanToCompletion
Console.WriteLine(task.Status);
}
}
  • Wait也可以传入一个超时时间和一个取消指令来提前结束等待。

长时间运行的任务

  • Task比较适合短时间运行的Compute-Bound类的工作,而那种长时间运行的任务可以使用下面方式运行:
1
2
3
4
5
6
7
8
9
10
11
12
class Program
{
static void Main(string[] args)
{
var task = Task.Factory.StartNew(() =>
{
Thread.Sleep(3000);
Console.WriteLine("Hello Task!");
}, TaskCreationOptions.LongRunning);
task.Wait();
}
}

Task的返回值

  • Task有一个泛型子类叫做Task<TResult>,它指定Task的返回值类型,使用Func<TResult>委托或兼容的Lambda表达式来调用Task.Run就可以得到Task<TResult>,随后可以通过Result属性来获得返回的结果。
  • 如果task还没有完成操作,访问Result属性就会阻塞该线程直到task完成操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Program
{
static void Main(string[] args)
{
Task<int> task = Task.Run(() =>
{
Console.WriteLine("Foo");
return 3;
});
//若task还没执行完成,访问Result将会阻塞主线程,直至task执行结束。
var taskResult = task.Result;
Console.WriteLine(taskResult);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
class Program
{
static void Main(string[] args)
{
//2到3000000中的质数的个数
var task = Task.Run(() =>
Enumerable.Range(2, 3000000).Count(n =>
Enumerable.Range(2, (int) Math.Sqrt(n) - 1).All(i => n % i > 0)));
Console.WriteLine("Task running...");
Console.WriteLine("The answer is " + task.Result);
}
}
  • Task<TResult>可以看作是一种所谓的future、promise(未来/许诺),早它里面包裹着一个Result,可以在Task.Run后依旧可以使用。

Task的异常

  • 与Thread不一样,Task可以很方便的传播异常,如果task里面抛出了一个未处理的异常,则该异常就会重新被抛出给:
    1. 调用Wait()的地方
    2. 访问了Task<TResult>的Result属性的地方。
  • 在Task里面抛出的异常会被CLR包装到AggregateException异常里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Program
{
static void Main(string[] args)
{
Task task = Task.Run(() =>
{
throw null;
});
try
{
//当task中抛出异常后,会在这里捕获异常
task.Wait();
}
catch (AggregateException e)
{
if (e.InnerException is NullReferenceException)
{
Console.WriteLine("Null");
}
else
{
throw;
}
}
}
}
  • 可以使用Task的IsFaultedIsCanceled属性来检测Task是否发生了故障。两个属性都返回false,则没有错误发生。
  • 如果ICanceled为true,则说明一个OperationCanceledException为该Task抛出了
  • 如果IsFaulted为true,则说明另一个类型的异常被抛出了,而Exception属性也将指明错误。

学习资料:B站杨旭

-------------本文结束感谢您的阅读-------------