> 文章列表 > C#的多线程、线程池和Task

C#的多线程、线程池和Task

C#的多线程、线程池和Task

        线程 被定义为程序的执行路径。每个线程都定义了一个独特的控制流。如果您的应用程序涉及到复杂的和耗时的操作,那么设置不同的线程执行路径往往是有益的,每个线程执行特定的工作。

        线程是轻量级进程。一个使用线程的常见实例是现代操作系统中并行编程的实现。使用线程节省了 CPU 周期的浪费,同时提高了应用程序的效率。

一、多线程

1、创建和暂停线程

        当程序运行时,会新建一个线程, 该线程会执行PrintNumbersWithDelay方法中的代码。然后会立即执行PrintNumbers方法。关键之处在于在PrintNumbersWithDelay方法中加入了Thread.Sleep方法调用,这将导致线程执行该代码时,在打印任何数字之前会等待指定的时间(本例中是2秒钟)。然而,PrintNumbers方法的执行是不受新线程的影响的

class Program
{static void Main(string[] args){Thread t = new Thread(PrintNumbersWithDelay);t.Start();PrintNumbers();Console.ReadKey();}static void PrintNumbers(){Console.WriteLine("Starting...");for (int i = 1; i < 5; i++){Console.WriteLine(i);}}static void PrintNumbersWithDelay(){Console.WriteLine("Starting...");for (int i = 1; i < 5; i++){Thread.Sleep(TimeSpan.FromSeconds(2));//暂停2SConsole.WriteLine(i);}}
}

执行结果如下:

2、线程等待

        当程序运行时,启动了一个耗时较长的线程来打印数字,打印每个数字前要等待两秒。但我们在主程序中调用了t.Join方法,该方法允许我们等待直到线程t完成。当线程t完成 "时,主程序会继续运行。借助该技术可以实现在两个线程间同步执行步骤。第一个线程会等待另一个线程完成后再继续执行。第一个线程等待时是处于阻塞状态(正如暂停线程中调用 Thread.Sleep方法一样)

class Program
{static void Main(string[] args){Console.WriteLine("Starting program...");Thread t = new Thread(PrintNumbersWithDelay);t.Start();t.Join();Console.WriteLine("Thread completed");}static void PrintNumbersWithDelay(){Console.WriteLine("Starting...");for (int i = 1; i < 10; i++){Thread.Sleep(TimeSpan.FromSeconds(2));Console.WriteLine(i);}}
}

执行结果如下:

 

3、终止线程

        当主程序和单独的数字打印线程运行时,我们等待6秒后对线程调用了t.Abort方法。这给线程注入了ThreadAbortException方法,导致线程被终结。这非常危险,因为该异常可以在任何时刻发生并可能彻底摧毁应用程序。另外,使用该技术也不一定总能终止线程。目-标线程可以通过处理该异常并调用Thread.ResetAbort方法来拒绝被终止。因此并不推荐使用,Abort方法来关闭线程。可优先使用一些其他方法,比如提供一个CancellationToken方法来,取消线程的执行。

class Program
{static void Main(string[] args){Console.WriteLine("Starting program...");Thread t = new Thread(PrintNumbersWithDelay);t.Start();Thread.Sleep(TimeSpan.FromSeconds(6));t.Abort();Console.WriteLine("A thread has been aborted");}static void PrintNumbersWithDelay(){Console.WriteLine("Starting...");for (int i = 1; i < 10; i++){Thread.Sleep(TimeSpan.FromSeconds(2));Console.WriteLine(i);}}
}

4、线程优先级

        当主程序启动时定义了两个不同的线程。第一个线程优先级为ThreadPriority.Highest,即具有最高优先级。第二个线程优先级为ThreadPriority.Lowest,即具有最低优先级。我们先, ,打印出主线程的优先级值,然后在所有可用的CPU核心上启动这两个线程。如果拥有一个1以上的计算核心,将在两秒钟内得到初步结果。最高优先级的线程通常会计算更多的迭代.但是两个值应该很接近。然而,如果有其他程序占用了所有的CPU核心运行负载,结果则会截然不同。

  为了模拟该情形,我们设置了ProcessorAffinity选项,让操作系统将所有的线程运行在单个CPU核心(第一个核心)上。现在结果完全不同,并且计算耗时将超过2秒钟。 .这是因为CPU核心大部分时间在运行高优先级的线程,只留给剩下的线程很少的时间来,运行。

  请注意这是操作系统使用线程优先级的一个演示。通常你无需使用这种行为编写程序。

class Program
{static void Main(string[] args){Console.WriteLine("Current thread priority: {0}", Thread.CurrentThread.Priority);Console.WriteLine("Running on all cores available");RunThreads();Thread.Sleep(TimeSpan.FromSeconds(2));Console.WriteLine("Running on a single core");Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1);RunThreads();}static void RunThreads(){var sample = new ThreadSample();var threadOne = new Thread(sample.CountNumbers);threadOne.Name = "ThreadOne";var threadTwo = new Thread(sample.CountNumbers);threadTwo.Name = "ThreadTwo";threadOne.Priority = ThreadPriority.Highest;threadTwo.Priority = ThreadPriority.Lowest;threadOne.Start();threadTwo.Start();Thread.Sleep(TimeSpan.FromSeconds(2));sample.Stop();Console.ReadKey();}class ThreadSample{private bool _isStopped = false;public void Stop(){_isStopped = true;}public void CountNumbers(){long counter = 0;while (!_isStopped){counter++;}Console.WriteLine("{0} with {1,11} priority " +"has a count = {2,13}", Thread.CurrentThread.Name,Thread.CurrentThread.Priority,counter.ToString("N0"));}}
}

运行结果:

 

 5、前台线程和后台线程

        当主程序启动时定义了两个不同的线程。默认情况下,显式创建的线程是前台线程。通过手动的设置threadTwo对象的IsBackground属性为ture来创建一个后台线程。通过配置来实现第一个线程会比第二个线程先完成。然后运行程序。

  第一个线程完成后,程序结束并且后台线程被终结。这是前台线程与后台线程的主要区,别:进程会等待所有的前台线程完成后再结束工作,但是如果只剩下后台线程,则会直接结束工作。

  一个重要注意事项是如果程序定义了一个不会完成的前台线程,主程序并不会正常结束。

class Program
{static void Main(string[] args){var sampleForeground = new ThreadSample(10);var sampleBackground = new ThreadSample(20);var threadOne = new Thread(sampleForeground.CountNumbers);threadOne.Name = "ForegroundThread";var threadTwo = new Thread(sampleBackground.CountNumbers);threadTwo.Name = "BackgroundThread";threadTwo.IsBackground = true;threadOne.Start();threadTwo.Start();Console.ReadKey();}class ThreadSample{private readonly int _iterations;public ThreadSample(int iterations){_iterations = iterations;}public void CountNumbers(){for (int i = 0; i < _iterations; i++){Thread.Sleep(TimeSpan.FromSeconds(0.5));Console.WriteLine("{0} prints {1}", Thread.CurrentThread.Name, i);}}}
}

6、向线程传递参数 

        当主程序启动时,首先创建了ThreadSample类的一个对象,并提供了一个迭代次数。然后使用该对象的CountNumbers方法启动线程。该方法运行在另一个线程中,但是使用数 ,字10,该数字是通过ThreadSample对象的构造函数传入的。因此,我们只是使用相同的间接方式将该迭代次数传递给另一个线程。

  另一种传递数据的方式是使用Thread.Start方法。该方法会接收一个对象,并将该对象,传递给线程。为了应用该方法,在线程中启动的方法必须接受object类型的单个参数。在创建threadTwo线程时演示了该方式。我们将8作为一个对象传递给了Count方法,然后 Count方法被转换为整型。

  接下来的方式是使用lambda表达式。lambda表达式定义了一个不属于任何类的方法。我们创建了一个方法,该方法使用需要的参数调用了另一个方法,并在另一个线程中运行该 ,方法。当启动threadThree线程时,打印出了12个数字,这正是我们通过lambda表达式传递,的数字。

  使用lambda表达式引用另一个C#对象的方式被称为闭包。当在lambda表达式中使用任何局部变量时, C#会生成一个类,并将该变量作为该类的一个属性。所以实际上该方式与 threadOne线程中使用的一样,但是我们无须定义该类, C#编译器会自动帮我们实现。

  这可能会导致几个问题。例如,如果在多个lambda表达式中使用相同的变量,它们会共享该变量值。在前一个例子中演示了这种情况。当启动threadFour和threadFive线程时,.它们都会打印20,因为在这两个线程启动之前变量被修改为20。

class Program
{static void Main(string[] args){var sample = new ThreadSample(10);var threadOne = new Thread(sample.CountNumbers);threadOne.Name = "ThreadOne";threadOne.Start();threadOne.Join();Console.WriteLine("--------------------------");var threadTwo = new Thread(Count);threadTwo.Name = "ThreadTwo";threadTwo.Start(8);threadTwo.Join();Console.WriteLine("--------------------------");var threadThree = new Thread(() => CountNumbers(12));threadThree.Name = "ThreadThree";threadThree.Start();threadThree.Join();Console.WriteLine("--------------------------");int i = 10;var threadFour = new Thread(() => PrintNumber(i));i = 20;var threadFive = new Thread(() => PrintNumber(i));threadFour.Start(); threadFive.Start();}static void Count(object iterations){CountNumbers((int)iterations);}static void CountNumbers(int iterations){for (int i = 1; i <= iterations; i++){Thread.Sleep(TimeSpan.FromSeconds(0.5));Console.WriteLine("{0} prints {1}", Thread.CurrentThread.Name, i);}}static void PrintNumber(int number){Console.WriteLine(number);}class ThreadSample{private readonly int _iterations;public ThreadSample(int iterations){_iterations = iterations;}public void CountNumbers(){for (int i = 1; i <= _iterations; i++){Thread.Sleep(TimeSpan.FromSeconds(0.5));Console.WriteLine("{0} prints {1}", Thread.CurrentThread.Name, i);}}}
}

二、线程池的使用

        在之前部分我们讨论了创建线程和线程协作的几种方式。现在考虑另一种情况,即只花费极少的时间来完成创建很多异步操作。创建线程是昂贵的操作,所以为每个短暂的异步操作创建线程会产生显著的开销。

  为了解决该问题,有一个常用的方式叫做池( pooling),线程池可以成功地适应于任何需要大量短暂的开销大的资源的情形。我们事先分配一定的资源,将这些资源放入到资源池。每次需要新的资源,只需从池中获取一个,而不用创建一个新的。当该资源不再被使用,时,就将其返回到池中。

1、在线程池中调用委托

        当程序运行时,使用旧的方式创建了一个线程,然后启动它并等待完成。由于线程的构造函数只接受一个无任何返回结果的方法,我们使用了lambda表达式来将对Test方法的调用包起来。我们通过打印出Thread. CurrentThread.IsThreadPoolThread属性值来确,保该线程不是来自线程池。我们也打印出了受管理的线程ID来识别代码是被哪个线程执行的。

  然后定义了一个委托并调用Beginlnvoke方法来运行该委托。BeginInvoke方法接受一个回调函数。该回调函数会在异步操作完成后会被调用,并且一个用户自定义的状态会传给该回调函数。该状态通常用于区分异步调用。结果,我们得到了一个实现了IAsyncResult接口的result对象。BeginInvoke立即返回了结果,当线程池中的工作线程在执行异步操作时,仍允许我们继续其他工作。当需要异步操作的结果时,可以使用BeginInvoke方法调用返回的result对象。我们可以使用result对象的IsCompleted属性轮询结果。但是在本例子中,使用的是AsyncWaitHandle属性来等待直到操作完成。当操作完成后,会得到一个结果,可以通过委托调用EndInvoke方法,将IAsyncResult对象传递给委托参数。

  事实上使用AsyncWaitHandle并不是必要的。如果注释掉r.AsyncWaitHandle.WaitOne,代码照样可以成功运行, 因为EndInvoke方法事实上会等待异步操作完成。调用 "EndInvoke方法(或者针对其他异步API的EndOperationName方法)是非常重要的, '因为该方法会将任何未处理的异常抛回到调用线程中。当使用这种异步API时,请确保始终调用了Begin和End方法。

  当操作完成后,传递给BeginInvoke方法的回调函数将被放置到线程池中,确切地说是,一个工作线程中。如果在Main方法定义的结尾注释掉Thread.Sleep方法调用,回调函数将不,会被执行。这是因为当主线程完成后,所有的后台线程会被停止,包括该回调函数。对委托和回调函数的异步调用很可能会被同一个工作线程执行。通过工作线程ID可以容易地看出。使用BeginOperationName/EndOperationName方法和.NET中的IAsyncResult对象等方 ,式被称为异步编程模型(或APM模式),这样的方法对被称为异步方法。该模式也被应用于多个,NET类库的API中,但在现代编程中,更推荐使用任务并行库( Task Parallel Library,简称TPL)来组织异步API

using System;
using System.Diagnostics;
using System.Threading;class Program
{static void Main(string[] args){int threadId = 0;RunOnThreadPool poolDelegate = Test;var t = new Thread(() => Test(out threadId));t.Start();t.Join();Console.WriteLine("Thread id: {0}", threadId);IAsyncResult r = poolDelegate.BeginInvoke(out threadId, Callback, "a delegate asynchronous call");r.AsyncWaitHandle.WaitOne();string result = poolDelegate.EndInvoke(out threadId, r);Console.WriteLine("Thread pool worker thread id: {0}", threadId);Console.WriteLine(result);Thread.Sleep(TimeSpan.FromSeconds(2));Console.ReadKey();}private delegate string RunOnThreadPool(out int threadId);private static void Callback(IAsyncResult ar){Console.WriteLine("Starting a callback...");Console.WriteLine("State passed to a callbak: {0}", ar.AsyncState);Console.WriteLine("Is thread pool thread: {0}", Thread.CurrentThread.IsThreadPoolThread);Console.WriteLine("Thread pool worker thread id: {0}", Thread.CurrentThread.ManagedThreadId);}private static string Test(out int threadId){Console.WriteLine("Starting...");Console.WriteLine("Is thread pool thread: {0}", Thread.CurrentThread.IsThreadPoolThread);Thread.Sleep(TimeSpan.FromSeconds(2));threadId = Thread.CurrentThread.ManagedThreadId;return string.Format("Thread pool worker thread id was: {0}", threadId);}
}

运行结果:

 2、向线程池中放入异步操作

        首先定义了AsyncOperation方法,其接受单个object类型的参数。然后使用QueueUser WorkItem方法将该方法放到线程池中。接着再次放入该方法,但是这次给方法调用传入了一个状态对象。该对象将作为状态参数传递给AsynchronousOperation方法。

  在操作完成后让线程睡眠一秒钟,从而让线程池拥有为新操作重用线程的可能性。如果注释掉所有的Thread.Sleep调用,那么所有打印出的线程ID多半是不一样的。如果ID是一样的,那很可能是前两个线程被重用来运行接下来的两个操作。

  首先将一个lambda表达式放置到线程池中。这里没什么特别的。我们使用了labmbda表达式语法,从而无须定义一个单独的方法。

  然后,我们使用闭包机制,从而无须传递lambda表达式的状态。闭包更灵活,允许我,们向异步操作传递一个以上的对象而且这些对象具有静态类型。所以之前介绍的传递对象给,方法回调的机制既冗余又过时。在C#中有了闭包后就不再需要使用它了。

class Program
{static void Main(string[] args){const int x = 1;const int y = 2;const string lambdaState = "lambda state 2";ThreadPool.QueueUserWorkItem(AsyncOperation);Thread.Sleep(TimeSpan.FromSeconds(1));ThreadPool.QueueUserWorkItem(AsyncOperation, "async state");Thread.Sleep(TimeSpan.FromSeconds(1));ThreadPool.QueueUserWorkItem( state => {Console.WriteLine("Operation state: {0}", state);Console.WriteLine("Worker thread id: {0}", Thread.CurrentThread.ManagedThreadId);Thread.Sleep(TimeSpan.FromSeconds(2));}, "lambda state");ThreadPool.QueueUserWorkItem( _ =>{Console.WriteLine("Operation state: {0}, {1}", x+y, lambdaState);Console.WriteLine("Worker thread id: {0}", Thread.CurrentThread.ManagedThreadId);Thread.Sleep(TimeSpan.FromSeconds(2));}, "lambda state");Thread.Sleep(TimeSpan.FromSeconds(2));Console.ReadKey();}private static void AsyncOperation(object state){Console.WriteLine("Operation state: {0}", state ?? "(null)");Console.WriteLine("Worker thread id: {0}", Thread.CurrentThread.ManagedThreadId);Thread.Sleep(TimeSpan.FromSeconds(2));}
}

三、Task的使用

        Net Framework4.0引入了一个新的关于异步操作的API,它叫做.任务并行库( Task Parallel Library,简称TPL), .Net Framework 4.5版对该API进行了轻微的改进,使用更简单。在本书的项目中将使用最新版的TPL,即.Net Framework 4.5版中的 API, TPL可被认为是线程池之上的又一个抽象层,其对程序员隐藏了与线程池交互的底层代码,并提供了更方便的细粒度的APL, TPL的核心概念是任务。一个任务代表了一个异步操作,该操作可以通过多种方式运行,可以使用或不使用独立线程运行。在本章中将探究任务的所有使用细节。

1、创建任务

        当程序运行时,我们使用Task的构造函数创建了两个任务。我们传入一个lambda表达式作为Action委托。这可以使我们给TaskMethod提供一个string参数。然后使用Start方法运行这些任务。

  请注意只有调用了这些任务的Start方法,才会执行任务。很容易忘记真正启动任务。

  然后使用Task.Run和Task.Factory.StartNew方法来运行了另外两个任务。与使用Task构造函数的不同之处在于这两个被创建的任务会立即开始工作,所以无需显式地调用这些任务的Start方法。从Task 1到Task 4的所有任务都被放置在线程池的工作线程中并以未指定,的顺序运行。如果多次运行该程序,就会发现任务的执行顺序是不确定的。

  Task.Run方法只是Task.Factory.StartNew的一个快捷方式,但是后者有附加的选项。通!常如果无特殊需求,则可使用前一个方法,如Task 5所示。我们标记该任务为长时间运行,结果该任务将不会使用线程池,而在单独的线程中运行。然而,根据运行该任务的当前的任务调度程序( task scheduler)运行方式有可能不同。

class Program
{static void Main(string[] args){var t1 = new Task(() => TaskMethod("Task 1"));var t2 = new Task(() => TaskMethod("Task 2"));t2.Start();t1.Start();Task.Run(() => TaskMethod("Task 3"));Task.Factory.StartNew(() => TaskMethod("Task 4"));Task.Factory.StartNew(() => TaskMethod("Task 5"), TaskCreationOptions.LongRunning);Thread.Sleep(TimeSpan.FromSeconds(1));Console.ReadKey();}static void TaskMethod(string name){Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);}
}

2、使用任务执行基本的操作

        首先直接运行TaskMethod方法,这里并没有把它封装到一个任务中。结果根据它提供给我们的主线程的信息可以得知该方法是被同步执行的。很显然它不是线程池中的线程。

  然后我们运行了Task 1,使用Start方法启动该任务并等待结果。该任务会被放置在线程池中,并且主线程会等待,直到任务返回前一直处于阻塞状态。

  Task 2和Task 1类似,除了Task 2是通过RunSynchronously()方法运行的。该任务会运行在主线程中,该任务的输出与第一个例子中直接同步调用TaskMethod的输出完全一样。这是个非常好的优化,可以避免使用线程池来执行非常短暂的操作。

  我们用以运行Task 1相同的方式来运行Task 3,但这次没有阻塞主线程,只是在该任务完成前循环打印出任务状态。结果展示了多种任务状态,分别是Creatd, Running和 RanToCompletion.

class Program
{static void Main(string[] args){TaskMethod("Main Thread Task");Task<int> task = CreateTask("Task 1");task.Start();int result = task.Result;Console.WriteLine("Result is: {0}", result);task = CreateTask("Task 2");task.RunSynchronously();result = task.Result;Console.WriteLine("Result is: {0}", result);task = CreateTask("Task 3");Console.WriteLine(task.Status);task.Start();while (!task.IsCompleted){Console.WriteLine(task.Status);Thread.Sleep(TimeSpan.FromSeconds(0.5));} Console.WriteLine(task.Status);result = task.Result;Console.WriteLine("Result is: {0}", result);Console.ReadKey();}static Task<int> CreateTask(string name){return new Task<int>(() => TaskMethod(name));}static int TaskMethod(string name){Console.WriteLine("Task {0} is running on a thread id {1}. Is thread pool thread: {2}",name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread);Thread.Sleep(TimeSpan.FromSeconds(2));return 42;}
}

参考文献地址:https://www.cnblogs.com/wyt007/p/9486752.html