同步操作(synchornous operation):先完成其工作再返回调用者。
异步操作(asynchornous operation):大部分工作是在返回给调用者之后才完成的,但是线程异步必须对资源进行协调,否则两个或者更多线程可能在同一时间访问相同的资源,二每个线程都不知道其他线程的操作,结果将产生不可预知的数据损坏。异步需要并发的创建,我们学习的异步方法通常有:Thread.Start和Task.Run还有Dispatcher.BeginInvoke,SynchronizationContext.Post。
富客户端应用程序的线程
在WPF,UWP,Windows Forms应用程序中,在主线程上执行长时间的操作将导致应用程序失去响应。一个常用的方法是创建一个工作线程来执行耗时的操作(通常用异步),并在完成之时更新UI线程,这种方法就要采用封送技术,实现这个操作的底层模式有:
1.WPF中,调用元素上的Dispatcher对象的BeginInvoke或者Invoke方法,这两者的区别看博客
2.在Windows Forms中:调用控件的BeginInvoke或者Invoke方法。
3.在UWP中:可以调用Dispatcher对象的RunAsync或者Invoke方法。
Invoke 或者 BeginInvoke 去调用,分别表示同步和异步,两者的区别就是一个导致工作线程等待,而另外一个则不会,线程同步时锁的使用可以参考这篇博客。
UI线程也可以有多个,但每一个线程要对应不同的窗口。这样使每一个窗口指定独立的UI线程,可以让每一个窗口都具有更好独立响应的能力。
线程Thread
关于的Thread的使用可以看以前的博客点击这里
对线程的操作有创建,暂停(挂起),恢复,休眠和终止以及设置优先权。
这里讲述它的缺点:
1.线程完成后,就无法通过再次使用Start方法来重新启动它,相反只能将其Join(它告诉操作系统暂停执行当前线程,直到另一个线程终止)
2.直接使用线程会对性能产生影响,线程是昂贵的资源,上下文的切换不是免费的,如果需要运行大量并发的I/O密集型操作,线程本身的开销非常巨大。
3.线程除非创建共享字段,不然在Join之后难以得到“返回值”,此外捕获和处理线程中的操作抛出的异常也非常麻烦。
注意:如果线程里有while循环,会照成主界面卡顿
线程池
线程池是多个线程的集合,通过一定的逻辑决定如何为线程分配工作,有任务要执行时,它分配池中的一个工作者线程执行任务,线程池中的线程执行完指定的方法后并不会自动消除,而是以挂起状态返回线程池,如果应用程序再次向线程池发出请求,那么处以挂起状态的线程就会被激活并执行任务,而不会创建新线程,这就节约了很多开销。只有当线程数达到最大线程数量,任务会排队,等待其他任务释放线程后再执行。因此,使用线程池可以避免大量的创建和销毁的开支,具有更好的性能和稳定性,但是ThreadPool不能控制线程的执行顺序,我们不能有效监控和控制线程池中的线程。
所以它的效率是通过重用线程获得的。
但是线程池不包括需要长时间运行的工作。这种情况下使用任务并行库(Task Parallel Library,TPL),以后博客介绍。
public const int Repetition = 800;
public static void Main()
{
ThreadPool.QueueUserWorkItem(DoWork, '+');//将方法排入队列以便执行,并指定包含该方法所用数据的对象。 此方法在有线程池线程变得可用时执行。
for (int count = 0; count < Repetition; count++)
{
Console.Write('-');
}
}
public static void DoWork(object state)
{
for (int count = 0; count < Repetition; count++)
{
Console.Write(state);
}
}
注意:避免将线程池中的工作者线程分配给I/O受限(是从外部来源获取数据如磁盘驱动器,web服务器所产生的延迟)或长时间运行的任务,这时可以改用任务并行库(TPL)
需要注意:
- 线程池中的所有线程都是后台线程。如果进程的所有前台线程都结束了,所有的后台线程也都会停止。
- 不能将入池的线程改为前台线程。
- 不能给入池的线程设置优先级或名称。
- 入池的线程只能用于时间较短的任务。如果线程要一直运行(比如Word的拼写检查线程),就应该使用Thread类创建一个线程。
任务Task
Task类可以解决所有Thread的问题,通过Task“任务并行库”中的类轻松输入线程池
**任务是对象,其中封装了以异步方式执行的工作(委托也是封装了代码的对象,但是委托是同步的,而任务是异步的),所以不会阻塞线程。
以下实例描述任务如何获取一个Action并以异步方式运行它:
const int Rept = 800;
Task task = Task.Run(()=>
{
for (int count = 0; count < Rept; count++)
{
Console.Write('-');
}
});
for(int count=0;count<Rept;count++)
{
Console.Write('+');
}
Task.Wait();//强迫主线程等待分配给任务的所有工作完成。相当于在工作者线程(自己创建的那个)上调用Join()
以下实例描述执行的任务返回结果:
static void Main(string[] args)
{
Task<string> task2 = Task.Run<string>(() => TempClass.TempWay(10));
Console.WriteLine(task2.Result);//读取Result属性自动造成当前线程阻塞,所以不用wait()
System.Diagnostics.Trace.Assert(task2.IsCompleted);
}
class TempClass
{
public static string TempWay(int data)
{
string tempstring = data.ToString();
return tempstring;
}
}
启动任务将从线程池获取一个新线程,创建第二个“控制点”,并在线程上执行委托(理解为任务是通过线程池来工作的)。
Task的背后的实现也是使用了线程池线程,但它的性能优于ThreadPool,因为它使用的不是线程池的全局队列,而是使用的本地队列,使线程之间的资源竞争减少。同时Task提供了丰富的API来管理线程。但是相对前面的两种耗内存,Task对于多核的CPU性能远超前两者,单核的CPU三者的性能没什么差别。
术语
为解决CPU核少线程多的矛盾,操作系统通过时间分片的机制来模拟多个线程并发运行。
Task某认使用线程池中的线程,他们都是后台线程。这意味着当主线程结束时,所有的任务也会随之停止。
调用Task的wait方法可以阻塞当前方法,直到任务完成,类似于线程对象的Join方法。
某认情况下,CLR会将任务运行在线程池上,这种线程非常适合执行短小的计算密集任务。如果要执行长时间的阻塞操作,则可以按照以下方式避免使用线程池中的线程。
Task myTask = Task.Factory.StartNew(() =>。。。。,TaskCreationOptions.LongRunning)
在某个核心上更改执行线程的行动称为上下文切换。
同步上下文
它适用于所用富客户端用户界面的API
using System.ComponentModel;
SynchronizationContext _usingSync;
public MainWindow()
{
InitializeComponent();
_usingSync = SynchronizationContext.Current;
new Thread(Work).Start();
}
void Work()
{
_usingSync.Post(_ => TextBox1.Text="111",null);
}
Post的效果和BeginInvoke是相同的。
前台线程和后台线程
显式创建的线程是前台线程。只要前台线程中的任何一个正在运行,它就可以使应用程序保持活动状态,而后台线程则不会。一旦所有前台线程完成,应用程序结束,所有仍在运行的后台线程终止。
IsBackground = true