1. 线程池(ThreadPool)
- 为什么要使用线程池?
主要原因是创建和销毁一个线程的代价是昂贵的,会消耗较多的系统资源; - 线程池原理?
每个CLR只有一个线程池,线程池线程不是在CLR初始化时自动创建的,而是向线程池派发(dispatch)异步操作时,如果线程池中没有线程,则会创建一个新新线程,不同的是,这个线程不会被销毁,执行完后进入空闲状态,等待响应新的异步请求。
当然,如果一个线程池线程不做任何事情,也是一种资源浪费。所以,当一个线程空闲特定一段时间后,会自己醒来终止自己以释放资源。
请注意线程池中的线程都是后台线程,在所有的前台线程运行结束后,所有的后台线程将停止工作。
创建一个线程时,会将当前线程的上下文传递给新建线程,而收集和复制上下文信息会耗费一定的时间和性能,在不需要传递上下文的场景中,可以通过System.Threading.ExecutionContext类型中的SuppressFlow()方法和RestoreFlow()方法分别阻止和恢复上下文的传递或流动。 - 使用线程池时的注意项?
- 线程池不适合需要长时间运行的作业,或者处理需要与其它线程同步的作业;
- 避免在线程池中执行I/O首先的操作,这种任务应该使用TPL模型;
- 不要手动设置线程池的最小和最大线程数,CLR会自动执行线程池的扩张和收缩,手动干预会使性能更差(目前默认是1000个线程);
- 线程池的两种使用方式:
- 通过异步编程模型(Asynchronous Programming Model,简称APM)展示怎样在线程池中异步的执行委托?
下面的方式为异步编程模型(这是.net历史中第一个异步编程模式),这里使用委托的BeginInvoke()方法来来运行该委托,BeginInvoke接收一个回调函数,会在任务执行完后被调用;现在这种APM编程模型使用的越来越少了,更多的是使用任务并行库(Task Parallel Library, 简称TPL)。
- 通过异步编程模型(Asynchronous Programming Model,简称APM)展示怎样在线程池中异步的执行委托?
private delegate string RunOnThreadPool(out int threadId);//委托
static void Main(string[] args)
{
//使用APM方式 进行异步调用 异步调用会使用线程池中的线程
IAsyncResult r = poolDelegate.BeginInvoke(out threadId, Callback, "委托异步调用");
r.AsyncWaitHandle.WaitOne();
// 获取异步调用结果
string result = poolDelegate.EndInvoke(out threadId, r);
}
- 通过ThreadPool.QueueUserWorkItem()向线程池中放入异步操作?
static void Main(string[] args)
{
//直接将方法传递给线程池,AsyncOperation为要异步执行的方法
ThreadPool.QueueUserWorkItem(AsyncOperation);
//直接将方法传递给线程池 并且通过state传递参数
ThreadPool.QueueUserWorkItem(AsyncOperation, "async state");
//使用Lambda表达式将任务传递给线程池 并且通过 state传递参数
ThreadPool.QueueUserWorkItem(state =>
{
WriteLine($"Operation state: {state}");
WriteLine($"工作线程 id: {CurrentThread.ManagedThreadId}");
}, "lambda state");
}
private static void AsyncOperation(object state)
{
WriteLine($"Operation state: {state ?? "(null)"}");
WriteLine($"工作线程 id: {CurrentThread.ManagedThreadId}");
}
- 使用普通创建线程方式和线程池方式有何区别?
分别运行下面两个方法,其中普通线程执行了2s多,但是创建了500个线程,线程池执行了9s多,但是只创建了很少的线程,为操作系统节省了线程和内存空间,但是花费的时间较多;
static void UseThreads()
{
for (int i = 0; i < 500; i++)
{
var thread = new Thread(() =>
{
Sleep(TimeSpan.FromSeconds(0.1));
});
thread.Start();
}
}
static void UseThreadPool()
{
for (int i = 0; i < 500; i++)
{
ThreadPool.QueueUserWorkItem(c =>
{
Sleep(TimeSpan.FromSeconds(0.1));
});
}
}
- 如何通过CancellationTokenSource取消线程:
private CancellationTokenSource cts = new CancellationTokenSource();
private void StartThread(object sender, RoutedEventArgs e)
{
ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 100));
}
private void Count(CancellationToken token, Int32 countTo)
{
for (Int32 count = 0; count < countTo; count++)
{
if (token.IsCancellationRequested)
{
this.Dispatcher.Invoke(() =>
{
this.AutoAddTextBox.Text += '\n' + "Count is canceled" + '\n';
});
break;
}
this.Dispatcher.Invoke(() =>
{
this.AutoAddTextBox.Text += count.ToString() + " ";
});
Thread.Sleep(100);
}
this.Dispatcher.Invoke(() =>
{
this.AutoAddTextBox.Text += "Count is Done!" + '\n';
});
}
private void CancelThread(object sender, RoutedEventArgs e)
{
//执行完Cancel()方法后,会将IsCancellationRequested置为true
cts.Cancel();
}
当运行到第32次时取消线程。
- BackgroundWorker组件介绍
BackgroundWorker是基于事件的异步编程模式( Event-based Asynchronous Pattern,简称EAP),是.net历史上第二种用来构造异步程序的方式,现在推荐使用TPL;该组件被应用于WPF中,通过它实现的代码可以直接与UI控制器交互;
2、任务(Task)
- 什么是任务并行库(TPL)?
为了实现线程的同步、异步、异常传递等问题,需要编写较多的代码,来达到正确性和健壮性,而且最大的问题是没有内建的机制让你知道操作在什么时候完成,也没有机制在操作完成时获取返回值。为此,.NET 4.0引入了一个关于异步操作的API——任务并行库(TPL)。TPL的内部使用了线程池,而且效率更高。在把线程归还到线程池之前,它会在同一个线程中顺序执行多个任务,减少任务上下文切换带来的时间浪费问题。其中,任务(Task)是对象,封装了要异步执行的操作。
TPL被认为是线程池之上的又一抽象层,其对程序员隐藏了与线程池交互的底层代码,并提供了更方便的细粒度API,TPL的核心概念是任务Task。 - Task创建的是线程池任务,Thread默认创建的是前台线程;
- 线程池一般只运行执行时间较短的异步操作;
- 新建Task的方法有3种,示例如下:
其中Task2中使用的是Run()静态方法,Task4中设置了LongRunning,表明需要长时间运行,因此不是线程池线程;注意,Task.Run方法只是Task.Factory.StartNew的一个快捷方式,但是后者有附加的选项;
public Main()
{
var task1 = new Task(() => TaskMethod("Task 1"));
task1.Start();
Task.Run(() => TaskMethod("Task 2"));
Task.Factory.StartNew(() => TaskMethod("Task 3"));
Task.Factory.StartNew(() => TaskMethod("Task 4"), TaskCreationOptions.LongRunning);
}
private void TaskMethod(string name)
{
string str = string.Format("{0} 线程id:{1},线程池中线程:{2}",
name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);
this.Dispatcher.BeginInvoke(new Action(delegate
{
this.textbox.Text += str + "\n";
}));
}
输出结果:
Task 1 线程id:10,线程池中线程:True
Task 2 线程id:12,线程池中线程:True
Task 3 线程id:13,线程池中线程:True
Task 4 线程id:12,线程池中线程:False
- Task的基本操作:
// 主线程直接执行操作
TaskMethod("主线程任务");
// 访问 Result属性,得到运行结果
Task<int> task = CreateTask("Task 1");
task.Start();
task.Wait();//显式等待任务完成后,才执行后面的代码
int result = task.Result;//task.Result是隐式等待任务完成后,才执行后面的代码,即在任务尚未完成时查询任务的Result
//这里Wait和Result是等待单个任务完成,会阻塞当前线程,如果要等待一个Task对象数据,则采用WaitAll()或WaitAny()
WriteLine($"运算结果: {result}");
// 使用当前线程,同步执行任务
task = CreateTask("Task 2");
task.RunSynchronously();//这里是运行在主线程上,非线程池线程
result = task.Result;
WriteLine($"运算结果:{result}");
一个伸缩性好的程序不应该使线程阻塞,当调用Wait,查询任务的Result属性时,极有可能造成线程池创建新线程,增大的资源的消耗。ContinueWith方法可以在任务完成时执行另一个任务,避免线程的阻塞。
private void StartThread(object sender, RoutedEventArgs e)
{
Task<int> t = new Task<int>(n => Sum((int)n), 1000);
t.Start();
Console.WriteLine("before result out");
Task cwt = t.ContinueWith(task => Console.WriteLine("result is : " + t.Result));
//ContinueWith()方法返回一个Task对象,但该对象一般不用,可直接忽略掉
Console.WriteLine("after result out");
}
private int Sum(int countTo)
{
int sum = 0;
for (int i = 0; i < countTo; i++)
{
sum += i;
}
return sum;
}
输出结果:
before result out
after result out
result is : 499500
任务可以启动子任务,且只有当各子任务全部执行结束,父任务才结束:
Task<int[]> parent = new Task<int[]>(() =>
{
var results = new int[3];
new Task(() => results[0] = Sum(500), TaskCreationOptions.AttachedToParent).Start();
new Task(() => results[1] = Sum(1000), TaskCreationOptions.AttachedToParent).Start();
new Task(() => results[2] = Sum(1500), TaskCreationOptions.AttachedToParent).Start();
return results;
});
//parent.ContinueWith(parentTask => Array.ForEach(parentTask.Result, Console.WriteLine));
parent.ContinueWith(parentTask =>
{
foreach (var item in parent.Result)
{
Console.WriteLine(item.ToString());
}
});
parent.Start();
输出结果:
124750
499500
1124250
Task相对于ThreadPool.QueueUserWorkItem具有很多附加属性,如任务状态,父任务的引用,TaskScheduler的引用,回调方法的引用等等,但会增加代价,因为需要为所有这些属性分配内存,所以在不需要这些附加功能时,采用ThreadPool.QueueUserWorkItem能获得更好的资源利用率。
- System.Threading的Timer类
该类使一个线程池线线程定时调用一个方法。在内部,线程池为所有的Timer对象只使用了一个线程,即单独有一个线程控件所有Timer对象中回调方法的调用,当一个Timer对象到期时,该线程会在内部调用ThreadPool.QueueUserWorkItem,将回调任务添加到线程池队列中。当回调方法执行时间很长,而Timer间隔时间又很短时,会存在线程池多个线程同时执行一个回调方法。
所以要在一个线程池线程上执行定期性发生的后台任务时,采用Timer定时器。
System.Windows.Forms的Timer类及System.Windows.Threading的Dispatcher类的功能相同,与上面定时器的区别是:回调方法只由一个线程完成,就是设置定时器的线程,即在一个线程中设置了DispatcherTimer,那么其回调方法也只在该线程中执行。 - Task类中几个常用方法:
public static bool WaitAll(Task[] tasks):判断tasks是否全部执行完毕;
public static bool WaitAny(Task[] tasks):判断tasks中是否存在执行完毕的task;
public static Task WhenAll(Task[] tasks):当所有tasks执行完毕后,创建并返回一个新的Task;
public static Task WhenAll(Task[] tasks):当tasks中存在已执行完毕的task,创建并返回一个新的Task; - TaskScheduler是一个非常重要的抽象,该组件实际上负责如何执行任务,默认的任务调试器将任务放置到线程池的工作线程中,这是TPL的默认选项;
- . C#5.0引入了新的语言特性,称为异步函数(asynchronous function),它是TPL之上更高级别的抽象,真正简化了异步编程,主要依靠async和await关键字实现;