线程(一)——线程,线程池,Task概念+代码实践

目录
  • 线程(一)——线程,线程池,Task概念+代码实践

    • 12.1 通过TPL进入线程池

    • 12.2 不同过TPL进入线程池

    • 12.3 线程池优化

    • 12.1.1 Task异常捕获

    • 12.2.1 QueueUserWorkItem

    • 12.2.2 异步委托

    • 7.1 lambda表达式传参

    • 7.2 线程start方法传参

    • 7.3 线程创建需要时间

    • 1.1 未出现线程抢占

    • 1.2 线程抢占

    • 1.3 避免线程抢占

    • 摘要

    • 1 线程安全

    • 2 线程阻塞

    • 3 Thread.yield()和Thread.sleep(0)

    • 4 线程如何工作

    • 5 线程与进程

    • 6 线程的使用和滥用

    • 7 线程传参

    • 8 线程命名

    • 9 前台线程与后台线程

    • 10 线程优先级

    • 11 异常处理

    • 12 线程池

    • 13 代码

    • 14 参考文章

摘要

线程中的概念很多,如果没有代码示例来理解,会比较晦涩,而且有些概念落不到实处,因此,本文以一些运行示例代码,结果来阐述线程中的一些基础概念。让自己跟读者一起把线程中的概念理解地更深刻。

1 线程安全

1.1 未出现线程抢占

class ThreadTest2    {        bool done;        static void Main()        {            ThreadTest2 tt = new ThreadTest2();   // Create a common instance            new Thread(tt.Go).Start();            tt.Go();        }                // Note that Go is now an instance method        void Go()        {                if (!done)                {                    done = true;                    Console.WriteLine("Done");                             }                    }    }

运行结果如下:

Done

1.2 线程抢占

class ThreadTest2    {        bool done;        static void Main()        {            ThreadTest2 tt = new ThreadTest2();   // Create a common instance            new Thread(tt.Go).Start();            tt.Go();        }                // Note that Go is now an instance method        void Go()        {                if (!done)                {                                       Console.WriteLine("Done");                            done = true;                     }                    }    }

运行结果如下:

DoneDone

线程抢占例子2:

for (int i = 0; i < 10; i++)  new Thread (() => Console.Write (i)).Start();

运行结果
0223557799

1.3 避免线程抢占

class ThreadTest2    {        static readonly object locker = new object();        bool done;        static void Main()        {            ThreadTest2 tt = new ThreadTest2();   // Create a common instance            new Thread(tt.Go).Start();            tt.Go();        }        // Note that Go is now an instance method        void Go()        {            lock (locker)            {                if (!done)                {                                    Console.WriteLine("Done");                  done = true;                }            }        }    }

运行结果如下:

Done

2 线程阻塞

class Program    {        static void Main()        {            Thread t = new Thread(Go);            t.Start();            t.Join();            Console.WriteLine("Thread t has ended!");        }        static void Go()        {            for (int i = 0; i < 1000; i++) Console.Write("y");        }    }

运行结果:

1000个y打印完毕才输出"Thread t has ended!"。

Thread.Sleep (500);
也会阻塞线程,让渡CPU的执行权给其他线程。

3 Thread.yield()和Thread.sleep(0)

sleep(0)效果相当于yield(),会让当前线程放弃剩余时间片,进入相同优先级线程队列的队尾,只有排在前面的所有同优先级线程完成调度后,它才能再次获执行的机会。

4 线程如何工作

多线痛通过内部的线程调度器(thread scheduler)管理,通过clr委托操作系统。线程调度器会分配适当的执行时间给活动线程,线程等待(锁)或者线程阻塞(用户输入)不会消耗cpu执行时间。
单核处理器电脑上,在Windows,时间片通常会被分配几十毫秒,远大于线程上下文切换还时间几毫秒。
在多处理器计算机上,多线程是通过时间片和真正的并发实现的,其中不同的线程在不同的CPU上同时运行代码。 几乎可以肯定,由于操作系统需要服务自己的线程以及其他应用程序的线程,因此还会有一定的时间片。
当线程的执行由于诸如时间片之类的外部因素而被中断时,该线程被认为是被抢占的。 在大多数情况下,线程无法控制其被抢占的时间和地点。

5 线程与进程

线程与进程有相似之处。 就像进程在计算机上并行运行一样,多个线程在单个进程中并行运行。 进程彼此完全隔离; 线程的隔离度有限。 特别是,线程与在同一应用程序中运行的其他线程共享(堆)内存。 这就是为什么线程有用的原因:例如,一个线程可以在后台获取数据,而另一个线程可以在数据到达时显示数据。

6 线程的使用和滥用

  • 利于响应式用户界面
    在同时并行运行的“worker”线程上运行耗时的任务,主UI线程可以自由继续处理键盘和鼠标事件。

  • 有效利用原本被阻塞的CPU
    当线程正在等待来自另一台计算机或硬件的响应时,多线程很有用。 当一个线程在执行任务时被阻塞时,其他线程可以利用本来没有负担的计算机的其他线程来响应任务。

  • 并行编程
    如果以``分而治之''策略在多个线程之间共享工作负载,则执行密集计算的代码可以在多核或多处理器计算机上更快地执行(请参阅第5部分)。

  • 随机执行
    在多核计算机上,有时可以通过预测可能需要完成的事情然后提前进行来提高性能。 LINQPad使用此技术来加速新查询的创建。 一种变化是并行运行许多不同的算法,这些算法都可以解决同一任务。 谁先获得“胜利”,当您不知道哪种算法执行速度最快时,此方法将非常有效。

  • 允许服务同时处理请求
    在服务器上,客户端请求可以同时到达,因此需要并行处理(如果使用ASP.NET,WCF,Web服务或远程处理,.NET Framework会为此自动创建线程)。 这在客户端上也很有用(例如,处理对等网络-甚至来自用户的多个请求)。

使用ASP.NET和WCF之类的技术,您如果不知道多线程正在发生-除非您在没有适当锁定的情况下访问共享数据(可能通过静态字段),会破坏线程安全性。

线程之间的交互(通常是通过共享数据),会带来很多复杂性,但却不可避免,因此,有必要将交互保持在最低限度,并尽可能地坚持简单可靠的设计。

好的策略是将多线程逻辑封装到可重用的类中,这些类可以独立检查和测试。 框架本身提供了许多更高级别的线程结构,我们将在后面介绍。

线程化还会在调度和切换线程时(如果活动线程多于CPU内核)会导致资源和CPU的浪费,并且还会产生创建/释放成本。 多线程并不总是可以加快您的应用程序的速度-如果使用过多或使用不当,它甚至可能减慢其速度。 例如,当涉及大量磁盘I / O时,让几个工作线程按顺序运行任务比一次执行10个线程快得多。

7 线程传参

7.1 lambda表达式传参

最方便的方法就是通过lambda表达式调用匿名方法,传参数。

static void Main()        {            Thread t = new Thread(() =>Print("Hello from t!"));            t.Start();        }        static void Print(string message)        {            Console.WriteLine(message);        }

7.2 线程start方法传参

static void Main()        {            Thread t = new Thread(Print);            t.Start("Hello from t!");        }        static void Print(object messageObj)        {            string message = (string)messageObj;   // We need to cast here            Console.WriteLine(message);        }

7.3 线程创建需要时间

string text = "t1";Thread t1 = new Thread ( () => Console.WriteLine (text) ); text = "t2";Thread t2 = new Thread ( () => Console.WriteLine (text) ); t1.Start();t2.Start();

运行结果:

t2t2

以上运行结果说明,在t1线程创建之前text被修改成了t2。

8 线程命名

每个线程都有名称属性,目的是为了更方便调试。

static void Main()            {                Thread.CurrentThread.Name = "main";                Thread worker = new Thread(Go);                worker.Name = "worker";                worker.Start();                Go();            }            static void Go()            {                Console.WriteLine("Hello from " + Thread.CurrentThread.Name);            }

运行结果:

Hello from mainHello from worker

9 前台线程与后台线程

Thread worker = new Thread(() => Console.ReadLine());            if (args.Length > 0) worker.IsBackground = true;            worker.Name = "backThread";            worker.Start();            Console.WriteLine("finish!");

前台线程会随着主线程窗口关闭而停止,后台线程及时主线程窗口关闭自己独立运行。

10 线程优先级

线程优先级决定了操作系统执行活动线程时间的长短。

enum ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }

有时候提高了线程的优先级,但却仍然无法满足一些实时的应用需求,这时候就需要提高进程的优先级,System.Diagnostics命名空间中的process进程类.

using (Process p = Process.GetCurrentProcess())  p.PriorityClass = ProcessPriorityClass.High;

实际上,ProcessPriorityClass.High比最高优先级低1个级别:Realtime。 将进程优先级设置为Realtime,可指示OS,您永远不希望该进程将CPU时间浪费在另一个进程上。 如果您的程序进入意外的无限循环,您甚至可能会发现操作系统已锁定,只剩下电源按钮可以拯救您! 因此,高通常是实时应用程序的最佳选择。

如果您的实时应用程序具有用户界面,则提高处理优先级将给屏幕更新带来过多的CPU时间,从而减慢整个计算机的速度(尤其是在UI复杂的情况下)。 降低主线程的优先级并提高进程的优先级可确保实时线程不会因屏幕重绘而被抢占,但不会解决使其他应用程序耗尽CPU时间的问题,因为操作系统仍会分配 整个过程的资源不成比例。 理想的解决方案是使实时工作程序和用户界面作为具有不同进程优先级的单独应用程序运行,并通过远程处理或内存映射文件进行通信。 内存映射文件非常适合此任务。 我们将在C#4.0的第14和25章中简要介绍它们的工作原理。

11 异常处理

Go无法补捉异常,GoCatch能捕获当前线程的异常,输出Console.WriteLine("exception.");由此可见,线程创建之后,异常只能由本线程捕获,如果其调用方需要捕获,则得用共享内存方式往上传,Task帮我们做了这件事,调用方可在task.result里捕获到其他线程的异常。

public static void Main()        {            try            {                new Thread(Go).Start();                Console.ReadKey();            }            catch (Exception ex)            {                // We'll never get here!                Console.WriteLine("Exception!");            }        }        static void Go() { throw null; }   // Throws a NullReferenceException        static void GoCatch()        {            try            {                // ...                throw null;    // The NullReferenceException will get caught below                               // ...            }            catch (Exception ex)            {                // Typically log the exception, and/or signal another thread                // that we've come unstuck                // ...                Console.WriteLine("exception.");            }        }

12 线程池

当你创建一个线程,几百毫秒会被花费在例如创建本地私有变量堆栈。每个线程都会默认消耗1MB内存,从而允许在非常精细的级别上应用多线程而不会影响性能。当利用多核处理器以“分而治之”的方式并行执行计算密集型代码时,这很有用。
线程池还限制了将同时运行的工作线程总数。活动线程过多会限制操作系统的管理负担,并使CPU缓存无效。一旦达到限制,作业将排队并仅在另一个作业完成时才开始。这使任意并发的应用程序成为可能,例如Web服务器。 (异步方法模式是一种先进的技术,通过高效利用池线程来进一步实现这一点;我们在C#4.0的第23章中简要介绍了这一点)。
有多种进入线程池的方法:
·通过Task Parallel Library(来自Framework 4.0)
·通过调用ThreadPool.QueueUserWorkItem
·通过异步委托(await)
·通过BackgroundWorker

以下方法间接使用线程池:
·WCF,远程,ASP.NET和ASMX Web服务应用程序服务器
·System.Timers.Timer和System.Threading.Timer
·以Async结尾的框架方法,例如WebClient(基于事件的异步模式)上的框架方法和大多数BeginXXX方法(异步编程模型模式)
·PLINQ

使用池线程时,需要注意以下几点:
·您无法设置池线程的名称,这会使调试更加困难(尽管您可以在Visual Studio的“线程”窗口中进行调试时附加说明)。
·池线程始终是后台线程(这通常不是问题)。
·除非您调用ThreadPool.SetMinThreads(请参阅优化线程池),否则阻塞线程池可能会在应用程序的早期阶段触发额外的延迟。
您可以自由更改池线程的优先级-将其释放回池后将恢复为正常状态。

您可以通过Thread.CurrentThread.IsThreadPoolThread属性查询当前是否在线程池上执行。

12.1 通过TPL进入线程池

通过Task Parallel Library库中的Task类可轻松使用线程池,Task类由Framework 4.0引入,如果你熟悉老的结构,考虑用不带泛型Task类来替代ThreadPool.QueueUserWorkItem,而泛型Task代表的是一个异步委托。 新的结构更快,更方便,比老的更灵活。

使用不带泛型例子的Task类,调用Task.Factory.StartNew,传递一个目标方法的委托;

static void Main()    // The Task class is in System.Threading.Tasks        {            var task=Task.Factory.StartNew(Go);            Console.WriteLine("main");            task.Wait() ;            Console.WriteLine(task.Result);            Console.ReadLine();        }        static string Go()        {            if (Thread.CurrentThread.IsThreadPoolThread)            { Console.WriteLine("Hello from the thread pool!"); }            else { Console.WriteLine("Hello just from the thread!"); }            return "task complete!";        }

输出结果:

mainHello from the thread pool!task complete!

12.1.1 Task异常捕获

static void Main()    // The Task class is in System.Threading.Tasks        {            var task=Task.Factory.StartNew(Go);            Console.WriteLine("main");            try            { task.Wait(); }                                                catch (Exception e)            {                Console.WriteLine("exception!");            }            Console.WriteLine(task.Result);            Console.ReadLine();        }        static string Go()        {            if (Thread.CurrentThread.IsThreadPoolThread)            { Console.WriteLine("Hello from the thread pool!"); }            else { Console.WriteLine("Hello just from the thread!"); }            throw null;            return "task complete!";        }

运行结果,在主线程中捕获到了其他线程的异常:

static void Main(){  // Start the task executing:  Task<string> task = Task.Factory.StartNew<string>    ( () => DownloadString ("http://www.linqpad.net") );   // We can do other work here and it will execute in parallel:  RunSomeOtherMethod();   // When we need the task's return value, we query its Result property:  // If it's still executing, the current thread will now block (wait)  // until the task finishes:  string result = task.Result;} static string DownloadString (string uri){  using (var wc = new System.Net.WebClient())    return wc.DownloadString (uri);}

Task<string> 就是一个返回值为string的异步委托。

12.2 不同过TPL进入线程池

如果你的框架是.Net 4.0之前的,你可以不通过Task Parallel Library 进入线程池。

12.2.1 QueueUserWorkItem

static void Main()        {            ThreadPool.QueueUserWorkItem(Go);            ThreadPool.QueueUserWorkItem(Go, 123);            Console.ReadLine();        }        static void Go(object data)   // data will be null with the first call.        {            Console.WriteLine("Hello from the thread pool! " + data);        }

运行结果:

Hello from the thread pool!Hello from the thread pool! 123

与Task不同:

  • 后续执行中无法返回执行结果;

  • 无法返回异常给调用者;

12.2.2 异步委托

即鄙人写的这篇文章深入理解C#中的异步(一)——APM模式EAP模式里的2.1APM异步编程模式。
需要补充说明的是:
委托的EndInvoke 做了3件事:

  • 阻塞等待;

  • 返回结果;

  • 向调用者跑出异常;

2.1.3 为异步委托的回调例子

12.3 线程池优化

线程池从其池中的一个线程开始。 分配任务后,池管理器会“注入”新线程以应对额外的并发工作负载(最大限制)。 在足够长时间的不活动之后,如果池管理器怀疑这样做会导致更好的吞吐量,则可以“退出”线程。
您可以通过调用ThreadPool.SetMaxThreads;来设置池将创建的线程的上限; 默认值为:
·32位环境中的Framework 4.0中的1023
·64位环境中的Framework 4.0中的32768
·Framework 3.5中的每个内核250个
·Framework 2.0中每个内核25个

您还可以通过调用ThreadPool.SetMinThreads设置下限。 下限的作用是微妙的:这是一种高级优化技术,它指示池管理器在达到下限之前不要延迟线程的分配。 当存在阻塞的线程时,提高最小线程数可提高并发性。
默认的下限是每个处理器内核一个线程-允许全部CPU利用率的最小值。 但是,在服务器环境(例如IIS下的ASP .NET)上,下限通常要高得多-多达50个或更多。
设置线程池最小线程数量。

ThreadPool.SetMinThreads (50, 50);

13 代码

本文代码git下载

14 参考文章

Threading in C#


版权声明:本文为博主翻译文章+自己理解,部分代码自己写,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.cnblogs.com/JerryMouseLi/p/14135600.html

(0)

相关推荐

  • C#多线程编程(二)线程池与TPL

    一.直接使用线程的问题 每次都要创建Thread对象,并向操作系统申请创建一个线程,这是需要耗费CPU时间和内存资源的. 无法直接获取线程函数返回值 无法直接捕捉线程函数内发生的异常 使用线程池可以解 ...

  • Task 类 (System.Threading.Tasks) | Microsoft Docs

    Task 类 定义 命名空间: System.Threading.Tasks 程序集: System.Runtime.dll 表示一个异步操作. C# public class Task : IAsy ...

  • 多线程之旅(Thread)

    在上篇文章中我们已经知道了多线程是什么了,那么它到底可以干嘛呢?这里特别声明一个前面的委托没看的同学可以到上上上篇博文查看,因为多线程要经常使用到委托.源码一.异步.同步1.同步(在计算的理解总是要你 ...

  • 一文说通C#中的异步编程

    天天写,不一定就明白. 又及,前两天看了一个关于同步方法中调用异步方法的文章,里面有些概念不太正确,所以整理了这个文章.   一.同步和异步. 先说同步. 同步概念大家都很熟悉.在异步概念出来之前,我 ...

  • C#线程学习笔记七:Task详细用法

    一.Task类简介: Task类是在.NET Framework 4.0中提供的新功能,主要用于异步操作的控制.它比Thread和ThreadPool提供了更为强大的功能,并且更方便使用. Task和 ...

  • Android之HandlerThread源码分析和简单使用(主线程和子线程通信、子线程和子线程通信)

    Android之HandlerThread源码分析和简单使用(主线程和子线程通信、子线程和子线程通信)

  • Java线程安全以及线程安全的实现方式和内存模型(JMM)

    一.了解几个概念 1)临界区: 临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性.当有线程进入临界区段时,其他线程或是进程必须等待, ...

  • 堆核到极致就堆线程:4线程SMT可能现身Zen 3霄龙EPYC处理器

    据AMD的路线图,Zen 3架构已经设计完毕,而Zen 4正在设计当中,使用这两种架构的霄龙EPYC 服务器处理器则分別名为Milan及Genoa.而随之而来,则是各种有关Zen 3的传言.外媒Har ...

  • 股票池观察界面代码更新

    之前视频版得分那个地方写的有点逻辑上的问题,我修改了更新一下 得分部分修改标红色: 增加大量,当日成交量/30日成交量均值,大于2认为是出现大量: 相对强度希望是对比中证800,需要先载一下扩展数据, ...

  • 好代码实践:基于 Redis 的轻量级分布式均衡消费队列

    一 我对好代码的看法 1 什么是好代码 如果你读过<设计模式之美>,你可能会觉得玩转各种设计模式,符合设计模式的6大基本原则的代码就是好代码:如果读过<clean code>, ...

  • 数字守门人:从概念到实践

    随着平台经济的迅速发展,如何监管平台,让平台更好.更规范地发展,已经成为了各国共同面对的一个难题.相比于传统的企业,平台具有很多独有的特点,因此很多传统的规制措施很难被简单照搬到平台环境.在这种背景下 ...

  • 如何合理地估算线程池大小?

    这个问题虽然看起来很小,却并不那么容易回答. 大家如果有更好的方法欢迎赐教,先来一个天真的估算方法: 假设要求一个系统的TPS(Transaction Per Second或者Task Per Sec ...

  • C#线程学习笔记三:线程池中的I/O线程

    本笔记摘抄自:https://www.cnblogs.com/zhili/archive/2012/07/20/MultiThreads.html,记录一下学习过程以备后续查用.     一.I/O线 ...