CLR 线程池
CLR 线程池: CLR初始化时线程池是怎样的,在什么情况下创建,什么情况下收回,销毁。
-
线程池在CLR中,CLR中的多个AppDomain共享这个线程池。
-
CLR在初始化时线程池中没有线程。
-
线程池内部维护了一个操作请求队列。
-
调用线程池某个方法,将一个记录项(entry)追加到线程池队列中。
-
线程池的内部代码将记录项派发给一个线程池线程。
-
如果线程池中没有就创建一个,线程执行完后,再收回到线程池中。
-
如果应用程序向线程池发了很多请求,一个线程忙不过来就会再创建更多的线程。
-
当一个线程池线程闲着没事一段时间后,线程会自己醒来终止自己以释放资源。
ThreadPool 的使用:
using System;
using System.Threading;
public static class Program {
public static void Main() {
Console.WriteLine("Main thread: queuing an asynchronous operation");
ThreadPool.QueueUserWorkItem(ComputeBoundOp, 5);
Console.WriteLine("Main thread: Doing other work here...");
Thread.Sleep(10000); // Simulating other work (10 seconds)
Console.WriteLine("Hit <Enter> to end this program...");
Console.ReadLine();
}
// This method's signature must match the WaitCallback delegate
private static void ComputeBoundOp(Object state) {
// This method is executed by a thread pool thread
Console.WriteLine("In ComputeBoundOp: state={0}", state);
Thread.Sleep(1000); // Simulates other work (1 second)
// When this method returns, the thread goes back
// to the pool and waits for another task
}
}
执行上下文
执行上下文 是每个线程都有的,它包含了:安全设置、宿主设置、 以及逻辑调用上下文数据。执行上下文设置 会影响线程执行它的代码。那是怎么影响的呢?
当一个线程(主)使用另一个线程(辅)执行任务时,前者的执行上下文应该流向(复制)辅助线程。
确保辅助线程的操作 和主线程是相同的安全设置和宿主设置。但是主线程将 上下文流向 辅助线程 这个操作很耗时间。
System.Threading 命名空间中的ExecutionContext 类,可以控制主线程 如何将执行上下文流向另一个辅助线程。
public sealed class ExecutionContext : IDisposable, ISerializable {
[SecurityCritical] public static AsyncFlowControl SuppressFlow();
public static void RestoreFlow();
public static Boolean IsFlowSuppressed();
// Less commonly used methods are not shown
}
它可以阻止执行上下文流动以提升应用程序的性能。
public static void Main() {
// Put some data into the Main thread's logical call context
CallContext.LogicalSetData("Name", "Jeffrey");
// Initiate some work to be done by a thread pool thread
// The thread pool thread can access the logical call context data
ThreadPool.QueueUserWorkItem(state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name")));
// Now, suppress the flowing of the Main thread's execution context
ExecutionContext.SuppressFlow();
// Initiate some work to be done by a thread pool thread
// The thread pool thread CANNOT access the logical call context data
ThreadPool.QueueUserWorkItem(state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name")));
// Restore the flowing of the Main thread's execution context in case
// it employs more thread pool threads in the future
ExecutionContext.RestoreFlow();
...
Console.ReadLine();
}
当编译完成后运行的结果如下:
Name=Jeffrey
Name=
协作式取消和超时
-
取消操作首先要创建一个 System.Threading.CancellationTokenSource 对象。这个对象包含了和管理取消有关的所有状态。
-
CancellationTokenSource 对象的 Token 属性 获得 一个或多个 CancellationToken 实例,传给你的操作就可以取消。
对一个任务的取消操作的例子如下:
public struct CancellationToken { // A value type
public static CancellationToken None { get; } // Very convenient
public Boolean IsCancellationRequested { get; } // Called by non•Task invoked operations
public void ThrowIfCancellationRequested(); // Called by Task•invoked operations
// WaitHandle is signaled when the CancellationTokenSource is canceled
public WaitHandle WaitHandle { get; }
// GetHashCode, Equals, operator== and operator!= members are not shown
public Boolean CanBeCanceled { get; } // Rarely used
public CancellationTokenRegistration Register(Action<Object> callback, Object state, Boolean useSynchronizationContext); // Simpler overloads not shown
}
任务
ThreadPool 的 QueueUserWorkItem 有许多限制,没有内建的机制让你知道操作在什么时候完成,也没有机制在操作完成时获得返回值。task 任务可以替代ThreadPool。
ThreadPool.QueueUserWorkItem(ComputeBoundOp, 5); // Calling QueueUserWorkItem
new Task(ComputeBoundOp, 5).Start(); // Equivalent of preceding using Task
Task.Run(() => ComputeBoundOp(5)); // Another equivalent
为了创建一个Task,需要调用构造器并传递一个Action或Action<Object>委托。这个委托就是你想要执行的操作。
可以向构造器传递一些TaskCreationOptions 标志类控制Task 的执行方式。TaskCreationOptions 枚举类型定义了一组可按位OR 的标志,定义如下:
[Flags, Serializable]
public enum TaskCreationOptions {
None = 0x0000,// The default
// Hints to the TaskScheduler that you want this task to run sooner than later.
PreferFairness = 0x0001,
// Hints to the TaskScheduler that it should more aggressively create thread pool threads.
LongRunning = 0x0002,
// Always honored: Associates a Task with its parent Task (discussed shortly)
AttachedToParent = 0x0004,
// If a task attempts to attach to this parent task, it is a normal task, not a child task.
DenyChildAttach = 0x0008,
// Forces child tasks to use the default scheduler as opposed to the parent’s scheduler.
HideScheduler = 0x0010
}
等待任务完成并获取结果
private static Int32 Sum(Int32 n) {
Int32 sum = 0;
for (; n > 0; n--)
checked { sum += n; } // if n is large, this will throw System.OverflowException
return sum;
}
现在构造一个Task<TResult> 对象,并为泛型TResult 参数传递计算限制操作的返回类型。开始任务之后,可等待它完成并获得结果。
// Create a Task (it does not start running now)
Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 1000000000);
// You can start the task sometime later
t.Start();
// Optionally, you can explicitly wait for the task to complete
t.Wait(); // FYI: Overloads exist accepting timeout/CancellationToken
// You can get the result (the Result property internally calls Wait)
Console.WriteLine("The Sum is: " + t.Result); // An Int32 value
-
调用 Wait 方法 或者 Result属性时,这些成员会抛出一个System.AggergateException 对象。
-
AggregateException 类型封装了异常对象的一个集合,该类的InnerExceptions属性包含一个异常列表。
-
如果一直不调用Wait 或 Result ,或者一直不查询Task 的 Exception 属性,代码就一直注意不到这个异常的发生。
-
为了帮你检测到该异常,可以向 TaskerScheduler 的静态 UnobservedTaskException 事件登记一个回调方法。
-
每次当一个Task 被垃圾回收时,如果存在一个没有被注意到的异常,CLR 的终结器线程就会引发这个事件。
-
一旦引发就会向你注册的事件处理方法中传递一个UnobservedTaskExceptionEventArgs对象,这其中包含你没有注意到的AggregateException。
-
Task 的两个静态方法 WaitAny 和 WaitALl ,它们都会阻塞调用线程,直到数组中所有的Task 对象完成。都可以通过CancellationToken 取消,并抛出一个 OperationCanceledException。
任务完成时自动启动新任务
伸缩性好的软件不应该使线程阻塞。调用Wait ,或者在任务还没有完成时查询任务的Result 属性。这样操作可能会造成线程池创建新线程,这增大了资源的消耗,也不利于性能和伸缩性。幸好还有更好的办法知道线程什么时候结束,还能在结束时启动新的 Task。
// Create and start a Task, continue with another task
Task<Int32> t = Task.Run(() => Sum(CancellationToken.None, 10000));
// ContinueWith returns a Task but you usually don't care
Task cwt = t.ContinueWith(task => Console.WriteLine("The sum is: " + task.Result));
ContinueWith 方法,它返回的是对新 Task 对象的引用。可以用这个对象调用各种成员。还有 Task 对象内部包含了ContinueWith 任务的一个集合。所以可用一个Task 对象来多次调用COntinueWith。任务完成时,所有ContinueWith 任务都会进入线程池的队列中。
TaskContinueationOptions 枚举值 可在调用ContinueWith 时传进去。
[Flags, Serializable]
public enum TaskContinuationOptions {
None = 0x0000,// The default
// Hints to the TaskScheduler that you want this task to run sooner than later.
PreferFairness = 0x0001,
// Hints to the TaskScheduler that it should more aggressively create thread pool threads.
LongRunning = 0x0002,
// Always honored: Associates a Task with its parent Task (discussed shortly)
AttachedToParent = 0x0004,
// If a task attempts to attach to this parent task, an InvalidOperationException is thrown.
DenyChildAttach = 0x0008,
// Forces child tasks to use the default scheduler as opposed to the parent’s scheduler.
HideScheduler = 0x0010,
// Prevents completion of the continuation until the antecedent has completed.
LazyCancellation = 0x0020,
// This flag indicates that you want the thread that executed the first task to also
// execute the ContinueWith task. If the first task has already completed, then the
// thread calling ContinueWith will execute the ContinueWith task.
ExecuteSynchronously = 0x80000,
// These flags indicate under what circumstances to run the ContinueWith task
NotOnRanToCompletion = 0x10000,
NotOnFaulted = 0x20000,
NotOnCanceled = 0x40000,
// These flags are convenient combinations of the above three flags
OnlyOnCanceled = NotOnRanToCompletion | NotOnFaulted,
OnlyOnFaulted = NotOnRanToCompletion | NotOnCanceled,
OnlyOnRanToCompletion = NotOnFaulted | NotOnCanceled,
}
调用 ContinueWith 时,可用 TaskContinuationOptions.OnlyOnCanceled 标志指定新任务只有在第一个任务被取消时才执行。类似地 OnlyOnFaulted 只有在第一个任务抛出未处理的异常时才执行。OnlyOnRanToCompletion 只有在第一个任务顺利执行完成时才执行。
默认情况下,如果不指定上述任何标志,则新任务无论如何都会运行,不管第一任务如何完成。
任务可以启动子任务
任务支持父/子关系,如下代码所示:
Task<Int32[]> parent = new Task<Int32[]>(() => {
var results = new Int32[3]; // Create an array for the results
// This tasks creates and starts 3 child tasks
new Task(() => results[0] = Sum(10000), TaskCreationOptions.AttachedToParent).Start();
new Task(() => results[1] = Sum(20000), TaskCreationOptions.AttachedToParent).Start();
new Task(() => results[2] = Sum(30000), TaskCreationOptions.AttachedToParent).Start();
// Returns a reference to the array (even though the elements may not be initialized yet)
return results;
});
// When the parent and its children have run to completion, display the results
var cwt = parent.ContinueWith(
parentTask => Array.ForEach(parentTask.Result, Console.WriteLine));
// Start the parent Task so it can start its children
parent.Start();
一个任务创建的一个或多个 Task 对象默认是顶级任务,他们与创建它们的任务无关。但 TaskCreationOptions.AttachedToParent 标志将一个Task 和创建它的 Task关联,结果是除非所有子任务(以及子任务的子任务)结束运行,否则创建(父任务)不认为已经结束。
Task内部揭秘
每个Task 对象都有一组字段,这些字段构成了任务的状态。其中包括 :
-
一个Int32 ID;(代表Task 唯一ID 的 Int32 字段。从 1 开始,每分配一个ID都递增1。系统分配的代表 Task 执行状态的一个 Int32 )
-
对父任务的引用、
-
对Task创建时指定的 TaskScheduler 的引用、
-
对回调方法的引用、
-
对要传给回调方法对象的引用、
-
对 ExecutionContext 的引用以及对 ManualResetEventSlim 对象的引用。
另外,每个 Task 对象都有对根据需要创建的补充状态的引用。补充状态包含:
-
一个CancellationToken、
-
一个 ContinueWithTask 对象集合、
-
为抛出未处理异常的子任务而准备的一个Task 对象集合等。
Task 很好用,但也是有代价的。必须为所有这些状态分配内存。如果不需要任务的附加功能,那么使用 ThreadPool.QueueUserWorkItem 能获得更好的资源利用率。
Task 和 Task<TResult> 类实现了 IDisposable 接口。如今 ,所有Dispose 方法所做的都是关闭 ManualResetEventSlim 对象。 但可以从 Task 和 Task<TResult> 派生的类,在这些类中分配它们自己的资源,并在它们重写的 Dispose 方法中释放这些资源。但不建议为Task对象显示调用 Dispose。
在一个 Task 对象的存在期间,可查询 Task 的只读 Status 属性了解 它在其生存期的什么位置。该属性返回一个 TaskStatus 值。
public enum TaskStatus {
// These flags indicate the state of a Task during its lifetime:
Created, // Task created explicitly; you can manually Start() this task
WaitingForActivation,// Task created implicitly; it starts automatically
WaitingToRun, // The task was scheduled but isn’t running yet
Running, // The task is actually running
// The task is waiting for children to complete before it considers itself complete
WaitingForChildrenToComplete,
// A task's final state is one of these:
RanToCompletion,
Canceled,
Faulted
}
当任务完成时,状态变成 以下状态之一:RanToCompletion、Canceled 或 Faulted。如果任务完成,可通过Task<TResult> 的Result 属性来查询任务结果。Task 或 Task<TResult>出错时,可查询 Task 的 Exception 属性获取异常,该属性总是返回一个AggregateException 对象,对象的 InnerExceptions 集合包含了所有未处理的异常。
为了简化代码,Task 提供了几个只读 Boolean 属性,包括IsCanceled 、 IsFaulted和 Iscompleted。注意当 Task 处于 RanToCompletion ,Canceled 或 Faulted 状态时,IsCompleted 返回 true。判断一个 Task 是否成功完成 最简单的办法是使用如下代码:
if(task.Status == TaskStatus.RanToCompletion) ...
任务工厂 - TaskFactory
有时需要创建一组共享相同配置的 Task 对象。为避免机械地将相同的参数传给每个Task 的构造器,可以创建一个任务工厂来封装通用的配置。System.Threading.Tasks 命名空间定义了一个 TaskFactory 类型和一个 TaskFactory<TResult> 类型。
以下代码延时了如何使用一个TaskFactory:
Task parent = new Task(() => {
var cts = new CancellationTokenSource();
var tf = new TaskFactory<Int32>(cts.Token, TaskCreationOptions.AttachedToParent, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
// This task creates and starts 3 child tasks
var childTasks = new[] {
tf.StartNew(() => Sum(cts.Token, 10000)),
tf.StartNew(() => Sum(cts.Token, 20000)),
tf.StartNew(() => Sum(cts.Token, Int32.MaxValue)) // Too big, throws OverflowException
};
// If any of the child tasks throw, cancel the rest of them
for (Int32 task = 0; task < childTasks.Length; task++)
childTasks[task].ContinueWith(t => cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted);
// When all children are done, get the maximum value returned from the
// non-faulting/canceled tasks. Then pass the maximum value to another
// task that displays the maximum result
tf.ContinueWhenAll(
childTasks,
completedTasks =>
completedTasks.Where(t => t.Status == TaskStatus.RanToCompletion).Max(t => t.Result),
CancellationToken.None).ContinueWith(t=>Console.WriteLine("The maximum is: " + t.Result),
TaskContinuationOptions.ExecuteSynchronously);
});
// When the children are done, show any unhandled exceptions too
parent.ContinueWith(p => {
// I put all this text in a StringBuilder and call Console.WriteLine just once
// because this task could execute concurrently with the task above & I don't
// want the tasks' output interspersed
StringBuilder sb = new StringBuilder(
"The following exception(s) occurred:" + Environment.NewLine);
foreach (var e in p.Exception.Flatten().InnerExceptions)
sb.AppendLine(" "+ e.GetType().ToString());
Console.WriteLine(sb.ToString());
}, TaskContinuationOptions.OnlyOnFaulted);
// Start the parent Task so it can start its children
parent.Start();
任务调度器
任务基础结构非常灵活,其中TaskScheduler 对象功不可没。TaskScheduler 对象负责执行被调度的任务。FCL 提供了 两个派生自TaskScheduler 的类型:
-
线程池任务调度器 ( thread pool task scheduler)
-
同步上下文任务调度器 (synchronization context task scheduler)
默认情况下是线程池任务调度器,这个任务调度器将任务调度给线程池的工作者线程。
同步上下文任务调度器适合提供了图形用户界面的应用程序,例如:windows 窗体、windows Presentation Foundation(WPF)、Silverlight、Windows Store 应用程序。它将所有任务都调度给应用程序的GUI 线程,使所有任务代码都能成功更新UI 组件。该任务调度器也不适用线程池。
下面的代码演示如何使用 同步上下文任务调度器:
internal sealed class MyForm : Form {
private readonly TaskScheduler m_syncContextTaskScheduler;
public MyForm() {
// Get a reference to a synchronization context task scheduler
m_syncContextTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Text = "Synchronization Context Task Scheduler Demo";
Visible = true; Width = 600; Height = 100;
}
private CancellationTokenSource m_cts;
protected override void OnMouseClick(MouseEventArgs e) {
if (m_cts != null) { // An operation is in flight, cancel it
m_cts.Cancel();
m_cts = null;
} else { // An operation is not in flight, start it
Text = "Operation running";
m_cts = new CancellationTokenSource();
// 这个任务使用默认的任务调度器,在一个线程池线程上执行
Task<Int32> t = Task.Run(() => Sum(m_cts.Token, 20000), m_cts.Token);
// 这些任务使用同步上下文任务调度器,在GUI 线程中执行
t.ContinueWith(task => Text = "Result: " + task.Result,
CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion,
m_syncContextTaskScheduler);
t.ContinueWith(task => Text = "Operation canceled",
CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled,
m_syncContextTaskScheduler);
t.ContinueWith(task => Text = "Operation faulted",
CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted,
m_syncContextTaskScheduler);
}
base.OnMouseClick(e);
}
}
如果有特殊的任务需求,完全可以定义自己的TaskScheduler 派生类。
Parallel 的静态 For,ForEach,和 Invoke 方法
一些常见的编程情形可通过任务提升性能。为简化编程,静态 System.Threading.Tasks.Parallel 类封装了这些情形,它内部使用Task 对象。
不要像下面这样处理集合中的所有项,例如:
//一个线程顺序执行这个工作(每次迭代调用一次 Dowork)
for (Int32 i=0; i< 1000; i++) DoWork(i);
使用Parallel 类的For方法,用多个线程池线程辅助完成工作:
//线程池的线程并行处理工作
Parallel.For(0, 1000, i=>DoWork(i));
如果能用foreach 的地方可以这样写:
//线程池的线程并行处理工作
Parallel.ForEach(collection , item => DoWork(item));
能用For的地方尽量用for,因为它更快。
如果要执行多个方法,可以像下面这样执行:
Parallel.Invoke(
()=>method1();
()=>method2();
()=>method3();)
如果只为区区几个工作项使用Parallel的方法,或者为处理得非常快的工作项使用Parallel 的方法,就会得不偿失,反而降低性能。因为Parallel 的方法本身也有开销。
定时器的使用
System.Threading 命名空间定义了一个 Timer 类,可用它让一个线程池线程定时调用一个方法。
在内部,线程池为所有 Timer 对象只使用一个线程。这个线程知道下一个Timer 对象在什么时候到期(计时器还有多久触发)。下一个Timer 对象到期时,线程就会唤醒,在内部调用 ThreadPool 的 QueueUserWorkItem,将一个工作项添加到线程池的队列中,使你的回调方法得到调用。
如果回调方法的执行时间很长,计时器可能在上个回调还没完成的时候再次触发,这可能造成多个线程池线程同时执行你的回调方法。为解决这个问题,构造Timer 时,为period 参数指定 Timeout.Infinite。这样,计时器就只触发一次,然后,在你的回调方法中,调用Change 方法来指定一个新的dueTime,并再次为period 参数指定Timeout.Infinite。
Timer 类提供了一个 Dispose 方法,允许完全取消计时器,并可在当时出于pending 状态的所有回调完成之后,向notifyObject 参数标识的内核对象发出信号。
以下代码演示了如何让一个线程池线程立即回调方法,以后每2 秒调用一次:
internal static class TimerDemo {
private static Timer s_timer;
public static void Main() {
Console.WriteLine("Checking status every 2 seconds");
// Create the Timer ensuring that it never fires. This ensures that
// s_timer refers to it BEFORE Status is invoked by a thread pool thread
s_timer = new Timer(Status, null, Timeout.Infinite, Timeout.Infinite);
// Now that s_timer is assigned to, we can let the timer fire knowing
// that calling Change in Status will not throw a NullReferenceException
s_timer.Change(0, Timeout.Infinite);
Console.ReadLine(); // Prevent the process from terminating
}
// This method's signature must match the TimerCallback delegate
private static void Status(Object state) {
// This method is executed by a thread pool thread
Console.WriteLine("In Status at {0}", DateTime.Now);
Thread.Sleep(1000); // Simulates other work (1 second)
// Just before returning, have the Timer fire again in 2 seconds
s_timer.Change(2000, Timeout.Infinite);
// When this method returns, the thread goes back
// to the pool and waits for another work item
}
}
如果有需要定时执行的操作,可利用Task 的静态Delay 方法和 C# 的async 和 await 关键字来编码。
internal static class DelayDemo {
public static void Main() {
Console.WriteLine("Checking status every 2 seconds");
Status();
Console.ReadLine(); // Prevent the process from terminating
}
// This method can take whatever parameters you desire
private static async void Status() {
while (true) {
Console.WriteLine("Checking status at {0}", DateTime.Now);
// Put code to check status here...
// At end of loop, delay 2 seconds without blocking a thread
await Task.Delay(2000); // await allows thread to return
// After 2 seconds, some thread will continue after await to loop around
}
}
}
FCL 只提供几个计时器,下面介绍这几个计时器的特点:
-
System.Threading 的 Timer 类
要在一个线程池线程上执行定时的后台任务,最好用它。
-
System.WIndows.Forms 的 Timer 类
构造这个类的实例,相当于告诉WIndows 将一个计时器和调用线程关联。当这个计时器触发时,Windows 将一条计时器消息(WM_TIMER) 注入线程的消息队列。线程必须执行一个消息泵来提取这些消息,并把它们派发给需要的回调方法。注意,所有这些工作都只由一个线程完成 (设置计时器的线程保证就是执行回调方法的线程)。还意味着计时器方法不会由多个线程并发执行。
-
System.Windows.Threading 的 DispatcherTimer 类
这个类是 System.Windows.Forms 的 Timer 类在 Silverlight 和 WPF 应用程序中的等价物。
-
Windows.UI.Xaml 的 DispatcherTimer 类
这个类是System.Windows.Forms’s Timer 类 在Windows Store 应用中的等价物。
-
System.Timers’s Timer 类
它本质上是 System.Threading 的 Timer 类的包装类。
线程池如何管理线程
CLR 团队将线程池默认 大约 1000 个线程。这基本上是不设限制。因为一个32位 进程最大有 2 GB 的可用空间。加载了一组 Win32 和CLR Dlls,并分配了本地堆和托管堆之后,剩余约 1.5 GB 的地址空间。由于每个线程都要为其用户模式栈 和 线程环境变量块(TEB)主备超过1 MB 的内存,所以在一个 32 位进程中,最多能有大约 1360 线程。
System.Threading.ThreadPool 类提供了几个静态方法,可调用它们设置和查询线程池线程的线程数:
GetMaxThreads、SetMaxThreads、GetMinThreads、SetMinThreads 和 GetAvailableThreads
Jeffrey Richter 强烈建议不要调用上述任何方法。Jeffrey 说设置线程池的线程数量会让程序的性能变得更差。我个人觉得线程池的数量不是应该保持在和CPU 数量的2倍上吗?
如何管理工作者线程
ThreadPool.QueueUserWorkItem 方法和 Timer 类总是将工作项放到全局队列中。工作者线程采用一个先入先出FIFO 算法将工作项从这个队列中取出,并处理它们。
由于是多个工作者线程在全局队列中拿走工作项,这就会形成并发情形,要有一个线程同步锁,保证两个或多个线程不会获取同一个工作项。这个线程同步锁在某些应用程序中可能成为瓶颈。
每个工作者线程都有自己的本地队列,工作者线程调度一个Task 时,该Task 被添加到调用线程的本地队列. 工作者线程采用先入后出 (LIFO)算法将任务从本地队列取出.
工作者线程发现它的本地线程队列变空了 , 会尝试从另一个工作者线程的本地队列"偷" 一个Task , 这个Task 从本地队列的尾部 "偷走" , 并要求获取一个线程同步锁 .
如果所有本地队列都变空 , 工作者线程会从全局队列中取一个工作项 .
如果全局队列也为空 , 工作者线程会进入睡眠状态 , 等待事情的发生 .