Advertisement

C# 多线程(菜鸟教程及爱整理)

阅读量:

C# 多线程–菜鸟教程

线程 代表程序的执行路径。
所有线程都拥有一个独特的控制流。
如果您在应用中遇到一些难度较大的操作以及需要较长的时间完成的任务,则建议设置不同进程运行路线。
建议根据需求设定不同进程运行路线。
每一个进程都有其独立负责的任务。

该术语指的是程序的具体运作轨迹。
所有进程均具备各自独有的指令流程图示。
如果您在应用中遇到一些难度较大的操作以及需要较长的时间完成的任务,则建议设置不同进程运行路线。
此方法有助于提高整体效率并确保各环节顺利衔接。
每一个进程都有其独立负责的任务。

线程生命周期

System.Threading.Thread 类对象被创建那一刻起即构成其生命周期之始点至当该线程被终止或者其执行完毕之时则标志着周期性的终点

下面列出了线程生命周期中的各种状态:

未启动状态 :当线程实例被创建但 Start 方法未被调用时的状况。

就绪状态 :当线程准备好运行并等待 CPU 周期时的状况。

不可运行状态 :下面的几种情况下线程是不可运行的:

复制代码
* 已经调用 Sleep 方法
* 已经调用 Wait 方法
* 通过 I/O 操作阻塞

死亡状态 :当线程已完成执行或已中止时的状况。

主线程

进程中第一个被执行的线程称为主线程

一旦 C# 程序启动运行时,主线程必然自动创建。由主线程的子线程所使用的 Thread 实例负责调度相关操作。建议您通过 Thread 的 CurrentThread 属性来获取当前运行中的线程。

下面的程序演示了主线程的执行:

实例

using System ;
using System.Threading ;

namespace MultithreadingApplication
{
class MainThreadProgram
{
static void Main(string[] args)
{
var currentThrd = Thread.GetCurrentThread();
currentThrd.Name = "MainThread";
Console.WriteLine($"This is {currentThrd.Name}");
Console.ReadKey();
}
}
}

当上面的代码被编译和执行时,它会产生下列结果:

This is MainThread

创建线程

线程是基于Thread类创建的。Thread类会启动Start()方法以开始子线程的执行。

下面的程序演示了这个概念:

复制代码
    using System;
    using System.Threading;
    
    namespace MultithreadingApplication
    {
    class ThreadCreationProgram
    {
        public static void CallToChildThread()
        {
            Console.WriteLine("Child thread starts");
        }
       
        static void Main(string[] args)
        {
            ThreadStart childref = new ThreadStart(CallToChildThread);
            Console.WriteLine("In Main: Creating the Child thread");
            Thread childThread = new Thread(childref);
            childThread.Start();
            Console.ReadKey();
        }
    }
    }

当上面的代码被编译和执行时,它会产生下列结果:

In Main: Creating the Child thread
Child thread starts

管理线程

Thread 类提供了各种管理线程的方法。

下面的实例演示了 sleep() 方法的使用,用于在一个特定的时间暂停线程。

复制代码
    using System;
    using System.Threading;
    
    namespace MultithreadingApplication
    {
    class ThreadCreationProgram
    {
        public static void CallToChildThread()
        {
            Console.WriteLine("Child thread starts");
            // 线程暂停 5000 毫秒
            int sleepfor = 5000;
            Console.WriteLine("Child Thread Paused for {0} seconds",
                              sleepfor / 1000);
            Thread.Sleep(sleepfor);
            Console.WriteLine("Child thread resumes");
        }
       
        static void Main(string[] args)
        {
            ThreadStart childref = new ThreadStart(CallToChildThread);
            Console.WriteLine("In Main: Creating the Child thread");
            Thread childThread = new Thread(childref);
            childThread.Start();
            Console.ReadKey();
        }
    }
    }

当上面的代码被编译和执行时,它会产生下列结果:

In Main, 启动 子线程. Child thread开始运行. 子线程被暂停了五秒. 子线程恢复了.

销毁线程

Abort() 方法用于销毁线程。

通过触发 threadabortexception 来终止该线程的执行。这个异常无法被捕获,在存在 finally 块结构的情况下,会落入 finally 块。

这个异常不能被捕获是什么鬼,可以被捕获呀

下面的程序说明了这点:

复制代码
    using System;
    using System.Threading;
    
    namespace MultithreadingApplication
    {
    class ThreadCreationProgram
    {
        public static void CallToChildThread()
        {
            try
            {
    
                Console.WriteLine("Child thread starts");
                // 计数到 10
                for (int counter = 0; counter <= 10; counter++)
                {
                    Thread.Sleep(500);
                    Console.WriteLine(counter);
                }
                Console.WriteLine("Child Thread Completed");
    
            }
            catch (ThreadAbortException e)
            {
                Console.WriteLine("Thread Abort Exception");
            }
            finally
            {
                Console.WriteLine("Couldn't catch the Thread Exception");
            }
    
        }
       
        static void Main(string[] args)
        {
            ThreadStart childref = new ThreadStart(CallToChildThread);
            Console.WriteLine("In Main: Creating the Child thread");
            Thread childThread = new Thread(childref);
            childThread.Start();
            // 停止主线程一段时间
            Thread.Sleep(2000);
            // 现在中止子线程
            Console.WriteLine("In Main: Aborting the Child thread");
            childThread.Abort();
            Console.ReadKey();
        }
    }
    }

当上面的代码被编译和执行时,它会产生下列结果:

In Main, creating a child thread. The child thread initiates at 0, followed by 1 and then 2. In Main, the child thread is aborted. The system fails to capture a thread exception.

C# 多线程–爱整理

一、利用多线程提高程序性能

本节导读:

随着硬件和网络快速地发展,在多线程(Multithreading)处理并行任务方面构成了有利的基础

其实在每一刻我们都都在享受到由多线程技术带来的便利。包括当前使用的多核处理器以及Windows操作系统和Web服务器在内的各种设备和系统都采用了多线程技术来提升性能和效率。

使用多线程能够明显提升程序的执行效率,并且学习多线程对于提升程序运行能力具有重要意义。本节将详细介绍多线程的基本原理及其在.NET面向对象程序设计中的实际应用。

1. 关于多线程

在介绍多线程之前,先了解一下进程。

进程:单独运行的程序被称为进程。(例如Windows系统的后台应用程序也可以被称作后台进程)

线程:对于同一个程序分为多个执行流,称为线程。

多线程:使用多个线程进行多任务处理,称为多线程。

并行计算主要针对单核处理器设计,在实际应用中发现其局限性

2. 如何合理使用多线程?

A.对于用户等待程序处理时,可以使用多线程处理耗时任务;

B.对于一些不需要即时完成的任务,可以使用后台任务线程处理;

C.对于多并发任务,可以使用多线程同时处理;

这句话的意思是将并发线程转换为并行线程吗?即让单核处理多个线程的方式转变为多核同时处理多个独立线程的方式(每个核心分配一个独立的线程)。

D.对于通讯类,比如对线程阻塞,可以使用多线程。

除过上面的几个常用的情况,还有很多情况下可以使用多线程。

3. 多线程的缺点

线程自然也有缺点,以下列出了一些:

A.如果有大量的线程,会影响性能,因为操作系统需要在他们之间切换;

B.更多的线程需要更多的内存空间;

C. 使用线程可能给程序带来额外的bug需要注意谨慎地使用。例如,在完成线程任务后,必须及时释放内存资源。

D.线程的中止需要考虑其对程序运行的影响。

4. .NET中的两种多线程

.NET本身就是一个多线程的的环境。

在.NET中有两种多线程的:

一种是使用Thread类进行线程的创建、启动,终止等操作。

一种是使用ThreadPool类用于管理线程池.

5 .NET中使用Thread进行多线程处理

一种多核并行处理机制称为线性池。它通常是由一批预先启动好的子进程构成。当出现新任务时:首先提取闲置的子进程来执行当前的任务;一旦当前的任务完成:将其结果存入队列等待下一步调用。这种机制通过减少短时间内频繁启动和终止子进程的成本而特别适合于高负载环境下持续产生并行作业的需求

5.1 Thread类常用方法

.NET基础库中的System.Threading命名空间包含了丰富的功能与接口以支持多线 thread 操作。
.NET中的Thread类型用于创建与管理线 thread 并设定它们的优先级以及获取相关信息。

以下是一些关键的方法

suspend 挂起、暂停 resume 继续、重新开始 interrupt 中断、打断

5.2 Thread类常用属性

Thread的属性有很多,我们先看最常用的几个:

CurrentThread :用于获取当前线程;

ThreadState 当前线程的状态(5.4介绍);

Name:获取或设置线程名称;

Priority:获取或设置线程的优先级(5.5介绍)

ManagedThreadId:获取当前线程的唯一标识

IsBackground:获取或设置线程是前台线程还是后台线程(5.6介绍)

IsThreadPoolThread:获取当前线程是否是托管线程池(后面章节会介绍)

下面创建一个线程示例,来说明这几个属性:

复制代码
    Thread myThreadTest = new Thread(() =>//这里的new Thread(ThreadStart start)里面ThreadStart是一个委托,而(()=>{代码块...})是lambda表达式,所以可以说lambda表达式是基于委托的
    {
    Thread.Sleep(1000);
    Thread t = Thread.CurrentThread;
    Console.WriteLine("Name: " + t.Name);
    Console.WriteLine("ManagedThreadId: " + t.ManagedThreadId);
    Console.WriteLine("State: " + t.ThreadState);
    Console.WriteLine("Priority: " + t.Priority);
    Console.WriteLine("IsBackground: " + t.IsBackground);
    Console.WriteLine("IsThreadPoolThread: " + t.IsThreadPoolThread);//process 进程
    })
    {
    Name = "线程测试",
    Priority = ThreadPriority.Highest
    };
    myThreadTest.Start();
    Console.WriteLine("关联进程的运行的线程数量:"+System.Diagnostics.Process.GetCurrentProcess().Threads.Count);

运行结果如下:

在这里插入图片描述

我的天竟然有6个线程,其他四个线程是

下面的代码是一个小插曲,有助于强化理解线程委托

复制代码
    using System;
    using System.Threading;
    
    namespace MultithreadingApplication
    {
    delegate void P1(object n);
    delegate void P2(int n1, int n2);
    class ThreadCreationProgram
    {
        public static void MyThreadStart0()
        {
            Console.WriteLine("我的线程:0");
        }
        public static void MyThreadStart11(object n)
        {
            for (int i = 0; i < (int)n; i++)
            {
                Console.WriteLine("我的线程:" + i);
            }
        }
        public static void MyThreadStart12(int n)
        {
            for (int i = 0; i < n; i++)
            {
                Console.WriteLine("我的线程:" + i);
            }
        }
        public static void MyThreadStart2(int n1, int n2)
        {
            for (int i = n1; i < n2; i++)
            {
                Console.WriteLine("我的线程:" + i);
            }
        }
      
        static void Main(string[] args)
        {
            //平常情况(自定义委托(参数,返回值情况任意))
            P1 p1 = new P1(MyThreadStart11);
            p1(5);
            P2 p2 = new P2(MyThreadStart2);
            p2(2, 5);
    
            //线程情况(系统线程定义委托(总共两个:1无参无返回值 2有一个参无返回值)
            ThreadStart ts = new ThreadStart(MyThreadStart0);
            ts();
            ParameterizedThreadStart pts = new ParameterizedThreadStart(MyThreadStart11);//实例委托pts就相当于一个方法指针,指向一个方法
            pts(5);
    
            //我的天在线程中委托的参数居然是从start()里面传进去的
            new Thread(pts).Start(5);
    
            //注意下面两个的比较
            new Thread((n) => MyThreadStart11(n)).Start(5);//lambda表达式:(n) => MyThreadStart11(n)就像当于委托public delegate void ParameterizedThreadStart(object obj)的一个实例,所以传进的参数n就是object类型,下面的语句需要(int)n进行强制转换.
            new Thread((n) => MyThreadStart12((int)n)).Start(5);//委托的方法参数类型与委托的参数数据类型不一致,此处却可以这样搞,就当是封装的原因吧
           
           new Thread(MyThreadStart0);//甚至可以直接跟个方法名里面的括号都省略了
            Console.ReadKey();
        }
    }
    }
5.3 带参数的线程方法

首先我们写“简单线程”中无参数的方法,如下:

注意看注释

复制代码
    namespace MultithreadingApplication
    {
    class ThreadCreationProgram
    {
        static void MyThreadStart()
        {
            Console.WriteLine("我是一个简单线程");
        }
        static void Main(string[] args)
        {
            //简单的线程
            Thread myThread = new Thread(MyThreadStart);//此时的Thread(MyThreadStart)等于Thread(() =>MyThreadStart()),可能是lambda express的简写形式
            //那么也就是说在无参方法调用委托时可以不用先实例化一个委托ThreadStart ts = new ThreadStart(MyThreadStart)然后再将ts传进线程Thread myThread = new Thread(ts),而可以直接在线程里传此方法Thread myThread = new Thread(MyThreadStart),这样的话就简单一些了
            myThread.Start();
        }     
    }
    }

我们使用Lambda表达式来改写前面“简单线程”中无参数的方法,如下:

复制代码
    namespace MultithreadingApplication
    {
    class ThreadCreationProgram
    {
        static void Main(string[] args)
        {
            new Thread(() => {  //此处可以说用无参的方法调用委托
                for (int i = 0; i < 5; i++)
                    Console.WriteLine("我的线程一-[{0}]", i);
            }).Start(); Console.ReadKey();
        }     
    }
    }
在这里插入图片描述

前面所述的示例中创建的线程并未指定任何参数。假设有一个带有参数的方法,请问在这种情况下如何为该方法生成线程?

别担心,在.NET中存在一个ParameterizedThreadStarted 类来解决带一个参数的问题,请看以下示例。

复制代码
    new Thread((num) =>{
    for (int i = 0; i < (int)num; i++)
        Console.WriteLine("我的线程二--[{0}]", i);
    }).Start(5);
    /*由于ParameterizedThreadStart 委托传的值是object类型的,所以要强制转化一下*/

运行结果如下:

在这里插入图片描述

那么问题来了?ParameterizedThreadStart委托仅有一个带数据的参数?对于多个参数来说?我们可以使用一个无参方法来包装它?如下:

先创建一个带参数的方法:

复制代码
    static void myThreadStart(int numA, int numB)
    {
    for (int i = (int)numA; i < (int)numB; i++)
        Console.WriteLine("我的线程三---[{0}]", i);
    }

然后通过无参数的委托来包装它,如下 :

复制代码
    //这里默认匹配是public delegate void ThreadStart();的一个实例
    new Thread(() => myThreadStart(0, 5)).Start();
    //事到如今我感觉可以肯定的说lambda表达式实质上就是一个委托

运行结果如下:

在这里插入图片描述
5.4 Thread状态

在系统中涉及线程启动后的操作流程中,在完成启动后的一段时间内会进入一个特定的状态

System.Threading.Thread.ThreadState属性决定了在线执行状态下的线程特征。自创建至终止期间,所有可能的ThreadState值必定位于该属性所定义的所有可能状态之一。

**A.Unstarted:**当线程被创建时,它处在Unstarted状态。

**B.Running:**Thread类的Start()方法会导致线程的状态转换为Running状态。一旦处于该状态,则没有外部干预的情况下会持续下去;然而,在以下情况下可能会发生改变:当调用相应的函数或方法时(包括但不限于挂起、阻塞、销毁以及自然终止)。

**C.Suspended:**如果线程被挂起,它将处于Suspended状态。

**D.Running:**通过调用Resume()方法实现线程重新执行,在此过程中该线程的状态将更新为Running状态。

**D.Running:**通过调用Resume()方法实现线程重新执行,在此过程中该线程的状态将更新为Running状态。

E.Stopped: 当一个线程被销毁或停止时,在其生命周期结束时会被标记为...E.Stopped...。该状态下的所有资源都会被释放,并从系统中移除出队列。处于这个状态下运行的进程将不再存在,并且无法重新回到未启动的状态(即Unstarted)。

F.Background: 除了当前主态之外还存在一个背景(background)状态字段用于指示线程是处于前 jogging 还是后台运行的状态。在同一特定时间点线程可能会同时处于多个不同的运行状态。

G.WaitSleepJoin和AbortRequested状态:例如,在一个线程被调用Sleep后处于阻塞状态时(即被阻塞),随后另一个线程调用Abort方法于该阻塞的线程,则该阻塞的线程将同时处在WaitSleepJoin和AbortRequested状态。

H.当线程响应被阻塞或中断时销毁会导致ThreadAbortException异常

ThreadState枚举的10种执行状态如下:

在这里插入图片描述

上图了解一个WaitSleepJoin就可以了 monitor 监视器,监听器,监控器

对于线程阻塞和同步问题,将在下一节继续介绍。

5.5. 线程优先级

对于多线程任务来说,在配置时可以根据任务的重要性和对资源的需求来为其设定相应的优先等级。该类库通过System.Threading.ThreadPriority来确定各线程的优先级设置从而影响它们获得的CPU时间分配。

高优先级的线程通常会比一般优先级的线程得到更多的CPU时间

主线程与其他线程(包括高优先级与低优先级线程)争夺cup资源;通常情况下,高优先级的线程获得cup资源的机会会更高;这会导致cpu的使用时间更加持久

不为每个线程逐个排序,而是将高优先级的cup分配时间设置得较长,以及将低优先级的cup分配时间设置得较短

新建的线程默认采用普通优先级策略,并且其优先级别能够被设定为其他等级。

在这里插入图片描述

线程抢占cpu资源可以用如下代码测试:

复制代码
    static void Main(string[] args)
    {
    int numberH1 = 0,numberH2=0, numberL1 = 0, numberL2=0;
    bool state = true;
    new Thread(() => { while (state) { numberH1++; Console.WriteLine("H1"); }; }) { Priority = ThreadPriority.Highest, Name = "线程A" }.Start();
    new Thread(() => { while (state) { numberH2++; Console.WriteLine("H2"); }; }) { Priority = ThreadPriority.Highest, Name = "线程A" }.Start();
    new Thread(() => { while (state) { numberL1++; Console.WriteLine("L1"); }; }) { Priority = ThreadPriority.Lowest, Name = "线程B" }.Start();
    //让主线程挂件1秒
    Thread.Sleep(1000);
    state = false;
    Console.WriteLine("线程H1: {0}, 线程H2: {1}, 线程L1: {2}", numberH1,numberH2,numberL1);
    Console.ReadKey();
    }
5.6 前台线程和后台线程

该段描述了系统中存在两种类型的线程,并说明了如何将默认设置下的主导线程转换为后台运行的状态。具体而言,默认情况下系统采用的是主导型线程这一类型;而如果需要将其调整为后台运行模式,则只需在代码中增加一个属性即可实现转变:通过设置属性thread.IsBackground = true; 就能将该类别的程序运行状态由主动转为被动。

重点来了,前后台线程的区别:

A.前台线程:应用程序必须执行完所有的前台线程才能退出;

B. 后台线程:应用于无需判断所有任务是否完成即可自行关闭。在应用关闭时被后台进程自动终止后台进程。

为了便于比较分析,在实验中我们设置了一个数值输出字段,并将其范围设定为在0至1000之间的数值

复制代码
    static void Main(string[] args)
    {
    Thread myThread = new Thread(() => { for (int i = 0; i < 1000; i++) Console.WriteLine(i); });
    
    var key = Console.ReadLine();
    if (key == "1")
    {
        myThread.IsBackground = true;
        myThread.Start();
    }
    else
    {
        myThread.IsBackground = false;
        myThread.Start();
    }
    }

如果输入1(后台线程),线程会很快关闭,并不会等输出完1000个数字再关闭;

如果输入其它(前台线程),回车后,则线程会等1000个数字输出完后,窗口关闭;

6. 本节要点:

A.本节主要介绍了线程的基本知识;

B.Thread常用的属性、方法;

C.Thread委托的方法有多个参数的用法;

D.Thread的优先级;

E.Thread的执行状态;

F.前台线程和后台线程;

后面会继续深入介绍利用线程提高程序性能。

二、多线程高级应用

本节要点:

上节讲述了多线程的基础知识及其在实际开发中的常见应用场景,并提供了多个入门实例供参考。本节则深入探讨了.NET框架下更为复杂的多线程实现技术与高级应用方案。

主要采用在共享资源的场景下解决thread安全与thread冲突的问题;采用thread信号量和thread通知机制来实现thread同步。

1、 ThreadStatic特性

特性:[ThreadStatic]

功能:指定静态字段在不同线程中拥有不同的值

在此之前,我们先看一个多线程的示例:

我们定义一个静态字段:

复制代码
    static int num = 0;
    new Thread(() =>
    {
    for (int i = 0; i < 1000000; i++)
        ++num;
    Console.WriteLine("来自{0}:{1}", Thread.CurrentThread.Name, num);
    })
    { Name = "线程一" }.Start();
    隐藏代码
    new Thread(() =>
    {
    for (int i = 0; i < 2000000; i++)
        ++num;
    Console.WriteLine("来自{0}:{1}", Thread.CurrentThread.Name, num);
    })
    { Name = "线程二" }.Start();

运行多次结果如下:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

可以看到,在三次运行中所得结果各异。产生的原因是由于多线程中的同步共享问题所导致。即意味着多个线程同时访问并使用同一个资源。

此处代码与上下文无关,知识两个疑惑,注意看注释

复制代码
    using System;
    using System.Threading;
    
    namespace MultithreadingApplication
    {
    class ThreadCreationProgram
    {       
        static int num = 0;
        static void Main(string[] args)
        {
            new Thread(() =>
            {
                int k=0;
                for (k = 0; k < 100000; k++) 
                    ++num;
                Console.WriteLine("来自{0}:{1} 此时k的值为{2}", Thread.CurrentThread.Name, num,k);
            })
            { Name = "线程一" }.Start();
    
            new Thread(() =>
            {
                int j = 0;
                for (; j < 200000; j++)
                    ++num;
                Console.WriteLine("来自{0}:{1} 此时j的值为{2}", Thread.CurrentThread.Name, num,j);
            })
            { Name = "线程二" }.Start();
    
            Thread.Sleep(5*1000);
            Console.WriteLine("主线程又开始");
            Console.ReadKey();
        }
    }
    //疑惑 1 两个线程执行次数竟然大于3000000,也竟然有小于3000000的   
    //2 竟然会输出:来自线程一:56265   答;这是因为不仅只有两个线程在执行,还有个主线程在执行,不要忽略了.
    //因为主线程走到了Console.ReadKey(),所以会在控制台输出线程一还未走完的num值,此时num值也就小于1000000了
    //那么为了避免主线程对子线程的影响可以阻塞主线程一段时间知道子线程完成(用sleep方法)--我的天呐我发现排除了主线程readkey的干扰后
    //仍然会输出:来自线程一:989265的情况者,这发生了什么?
    }

为了解决上述问题,我们可以采取一种最为简便的方式——利用静态字段中的ThreadStatic特性进行处理。

在定义静态字段时,加上[ThreadStatic]特性,如下:

复制代码
    [ThreadStatic]
    static int num = 0;

两个线程不变的情况下,再次运行,结果如下:

在这里插入图片描述

无论运行多少次的结果都一致;当一个字段被ThreadStatic特性标注后其在各个线程中的值都是独立且互不影响的;这是因为每个线程都会独立地分配static字段所占用的内存空间;这样一来(static字段所引发的问题)也就不存在了

2. 资源共享

多个线程的共享使用,其实也就是多线程的资源共享(即多个资源之间的共享)。值得注意的是,线程同步指的是各个线程所访问的资源之间的相互同步,并不是单个线程自身的独立运行。

在实际使用多线程的过程中,并非都是各个线程访问不同的资源。

来看一个线程示例,假设我们不知道该线程完成所需的时间,我们可以设置固定的等待时间(例如500毫秒):

先定义一个静态字段:

复制代码
    static int result;
    Thread myThread = new Thread(() =>
    {
    Thread.Sleep(1000);
    result = 100;
    });
    myThread.Start();
    Thread.Sleep(500);             
    Console.WriteLine(result);

运行结果如下:

在这里插入图片描述

观察到结果为0,并非是我们所期望的结果。通常情况下,在线程执行期间,我们无法预知其完成时间。是否能在任务完成后提供相应的通知?

下面的代码与上下文无关,只是一个小注意点

复制代码
    using System;
    using System.Threading;
    
    namespace MultithreadingApplication
    {
    class ThreadCreationProgram
    {
        static int result;
        static void Main(string[] args)
        {
           
            Thread myThread = new Thread(() =>
            {
                Thread.Sleep(10000);
                result = 100;
                Console.WriteLine(result);
                Console.ReadKey();
            });
            myThread.Start();//这一步再往下走两个线程就开始抢夺cup资源了
            Thread.Sleep(1000);
            Console.WriteLine(result);
            Console.ReadKey();//执行完这一步,并不会就一直停在这里,当myThread线程睡眠时间到了,会自动执行myThread线程
           //然后停在myThread的Readkey处,在控制台输入任意值,走到24,再输入任意值,走到18.
        }
    }
    }

.NET为我们提供了Join方法这一功能模块,在处理线程阻塞行为时具有显著效果。该方案通过采用Stopwatch(sw)来记录时间。

改进线程代码如下:

复制代码
    using System;
    using System.Threading;
    
    namespace MultithreadingApplication
    {
    class ThreadCreationProgram
    {
        static int result;
        static void Main(string[] args)
        {
            //Diagnostic 诊断.   Stopwatch 跑表   
            //StartNew()初始化新的 System.Diagnostics.Stopwatch 实例,将运行时间属性设置为零,然后开始测量运行时间。
            System.Diagnostics.Stopwatch watch = System.Diagnostics.Stopwatch.StartNew();
            Thread myThread = new Thread(() =>
            {
                Thread.Sleep(1000);
                result = 100;
            });
            myThread.Start();
            Thread.Sleep(500);//走到这一步主线程睡眠,进入子线程myThread
            myThread.Join();//0.5秒钟后回到主线程这一步,走到这一步时会停下来直到子线程myThread执行完毕.
            Console.WriteLine(watch.ElapsedMilliseconds);//Elapsed 消逝、过去    Millisecond 毫秒
            Console.WriteLine(result);
            Console.ReadKey();
        }
    }
    }//Join()和sleep()都是线程阻塞

运行结果如下:

在这里插入图片描述

结果和我们想要的是一致的。

3. 线程锁

除了上述方法之外,在处理线程同步问题时,.NET系统提供了相应的锁机制来实现对线程同步的支持,并在此基础上进一步优化了上述示例。

复制代码
    using System;
    using System.Threading;
    
    namespace MultithreadingApplication
    {
    class ThreadCreationProgram
    {
        //先定义一个静态字段来存储锁
        static object locker = new object();
        static int result;
        static void Main(string[] args)
        {
            System.Diagnostics.Stopwatch watch = System.Diagnostics.Stopwatch.StartNew();
            Thread t1 = new Thread(() =>
            {
                lock (locker)//lock (x)里面的x是引用类型
                {
                    Thread.Sleep(10000);
                    result = 100;
                }
            });
            t1.Start();
            Thread.Sleep(5000);
            lock (locker)//lock(x)中的x是同一个引用类型的变量时,这些锁之间是互斥的,只有最先执行的锁执行完,才会执行下一个锁
            {
                Console.WriteLine("线程耗时:" + watch.ElapsedMilliseconds);
                Console.WriteLine("线程输出:" + result);
            }
            Console.ReadKey();
        }
    }
    }

运行结果如下:

在这里插入图片描述

运行结果与上一节示例一致,在线程处理较为复杂的场景下,可以看到耗时显著降低;采用非阻塞方式可实现线程同步。

4. 线程通知

前面提到了是否可以在一个线程完成时通知处于等待状态的其他线程呢?这里.NET通过提供一个事件通知的方法实现了在单个线程完成时触发所有等待完成的任务。

4.1 AutoResetEvent

改进上面的线程如下:

复制代码
    using System;
    using System.Threading;
    //一个线程完成后通知另外一个线程(是一个!与下面的几个不同)
    namespace MultithreadingApplication
    {
    class ThreadCreationProgram
    {
        //先定义一个通知对象                                     //EventWaitHandle 表示一个线程同步事件。         
        static EventWaitHandle tellMe = new AutoResetEvent(false);//里面的boolean该值指示是否将初始状态设置为终止状态的类。
        static int result = 0;                              
        static void Main(string[] args)
        {
            System.Diagnostics.Stopwatch watch = System.Diagnostics.Stopwatch.StartNew();
            Thread myThread = new Thread(() =>
            {
                Thread.Sleep(5000);
                result = 100;
                tellMe.Set();//将事件状态设置为有信号,从而允许一个或多个等待线程继续执行。
            });
            myThread.Start();
            tellMe.WaitOne();//阻止当前线程,直到当前 System.Threading.WaitHandle 收到信号。
            Console.WriteLine("线程耗时:" + watch.ElapsedMilliseconds);
            Console.WriteLine("线程输出:" + result);
        }
    }//待在同一个代码块的两个线程是资源共享的,即两个线程是同步的
    }

运行结果如下:

在这里插入图片描述
4.2 ManualResetEvent

与其相对应的是ManualResetEvent这一手动模式。主要区别体现在在线程结束后ManualResetEvent仍可通行(. 当且仅当手动Reset被关闭时才不再通行)。下面看一个示例:

这句话的意思是在调用mre.Set()mre.WaitOne()完成后,在再次调用mre.WaitOne()时仍然有效。然而,在AutoResetEvent的情况下则不适用。为了测试这个情况,请尝试将以下代码中的ManualResetEvent替换为AutoResetEvent

复制代码
    using System;
    using System.Threading;
    
    namespace MultithreadingApplication
    {
    //一個線程完成後通知其他個線程.(其他的意思是多于一个)
    class ThreadCreationProgram
    {
        //EventWaitHandle 表示一个线程同步事件。
        static EventWaitHandle mre = new ManualResetEvent(false);//布尔值指示是否将初始状态设置为终止状态的类。
        static int result = 0;
        static void Main(string[] args)
        {
            System.Diagnostics.Stopwatch watch = System.Diagnostics.Stopwatch.StartNew();
            Thread myThreadFirst = new Thread(() =>
            {
                Thread.Sleep(10000);
                result = 100;
                mre.Set();//将事件状态设置为有信号,从而允许一个或多个等待线程继续执行。
            })
            { Name = "线程一" };
            Thread myThreadSecond = new Thread(() =>
            {
            	两个WaitOne()执行后进入线程一执行Set(),Set执行后代表两个WaitOne都已经通过
                mre.WaitOne();
                Console.WriteLine(Thread.CurrentThread.Name + "获取结果:" + result + "(" + System.DateTime.Now.ToString() + ")");
            })
            { Name = "线程二" };
            myThreadFirst.Start();
            myThreadSecond.Start();
            mre.WaitOne();//阻止当前线程,直到当前 System.Threading.WaitHandle 收到信号。
            Console.WriteLine("线程耗时:" + watch.ElapsedMilliseconds + "(" + System.DateTime.Now.ToString() + ")");
            Console.WriteLine("线程输出:" + result + "(" + System.DateTime.Now.ToString() + ")");
            Console.ReadKey();
        }
    }//手动Reset关闭,mre.Reset();
    }

运行结果如下:

在这里插入图片描述

结果100可能先被输出,或者在中间被输出,也可能最后被输出,具体取决于主线程与线程二对CPU资源的争夺

下面代码是手动 Reset()关闭展示

复制代码
    using System;
    using System.Threading;
    
    namespace MultithreadingApplication
    {
    //一個線程完成後通知其他個線程.(其他的意思是多于一个)
    class ThreadCreationProgram
    {
        //EventWaitHandle 表示一个线程同步事件。
        static EventWaitHandle mre = new ManualResetEvent(false);//布尔值指示是否将初始状态设置为终止状态的类。
        static int result = 0;
        static void Main(string[] args)
        {
            System.Diagnostics.Stopwatch watch = System.Diagnostics.Stopwatch.StartNew();
            Thread myThreadFirst = new Thread(() =>
            {
                Thread.Sleep(1000);
                result = 100;
                mre.Set();//将事件状态设置为有信号,从而允许一个或多个等待线程继续执行。
               
            })
            { Name = "线程一" };
            Thread myThreadSecond = new Thread(() =>
            {
                
                mre.WaitOne();//两个WaitOne()同时执行后进入线程一执行Set(),Set执行后代表两个WaitOne都已经通过
                
                Console.WriteLine(Thread.CurrentThread.Name + "获取结果:" + result + "(" + System.DateTime.Now.ToString() + ")");
            })
            { Name = "线程二" };
    
            Thread myThreadThird = new Thread(() =>
            {
                mre.Reset();
                mre.WaitOne();                
                Console.WriteLine(Thread.CurrentThread.Name + "获取结果:" + result + "(" + System.DateTime.Now.ToString() + ")");
            })
            { Name = "线程三" };
    
            myThreadFirst.Start();
            myThreadSecond.Start();
            mre.WaitOne();//阻止当前线程,直到当前 System.Threading.WaitHandle 收到信号。
            Console.WriteLine("线程耗时:" + watch.ElapsedMilliseconds + "(" + System.DateTime.Now.ToString() + ")");
            Console.WriteLine("线程输出:" + result + "(" + System.DateTime.Now.ToString() + ")");
    
            myThreadThird.Start();
            Thread.Sleep(1000);
            mre.Set();//将这一句注释掉线程三WaitOne()就等不到信号,从而会被一直阻塞.
            Console.ReadKey();
        }
    }
    }
4.3. Semaphore

它也是一种用于管理多线程同步的通知机制。当同时开启大量线程时,在关闭该机制时采用Reset方法可能会引发问题:若未适当调用Sleep方法进行休眠,则可能导致某些线程未能及时恢复状态而提前退出系统。对此情况较为难以预测的问题,.NET引入了更为先进的同步机制

复制代码
    using System;
    using System.Threading;//semaphor 发信号,打旗语
    
    namespace MultithreadingApplication
    {
    class Program
    {
        //先定义一个通知对象的静态字段
        //Semaphore(初始授予1个请求数,设置最大可授予5个请求数)
        static Semaphore semaphore = new Semaphore(1, 5);//初始授予1个请求数,如果没有semaphore.Release()语句,则只会执行一个子线程,执行完之后请求数又会变成0
        static void Main(string[] args)
        {
            for (int i = 1; i <= 5; i++)
            {
                Thread thread = new Thread(Work);
                thread.Start(i);
            }
            Thread.Sleep(2000);
            //授予3个请求
            semaphore.Release(3);
            Console.ReadLine();
        }
        static void Work(object obj)
        {
            semaphore.WaitOne();
            Console.WriteLine("print: {0}", obj);
        }
    }//程序执行完毕会输出四个记录
    }

5. 本节要点:

A. 线程中的静态变量具有Thread-Static特性,在不同线程中的取值各不相同

B.线程同步的几种方式,线程锁和线程通知

C.线程通知的两种方式:AutoResetEvent /ManualResetEvent 和 Semaphore

多线程的更多特性,下一节继续深入介绍。

三、利用多线程提高程序性能(下)

本节导读:

在上一节中提到,在多线程同步过程中,通过使用线程锁和线程通知机制来解决资源共享问题。这构成了多线程编程的基础逻辑。

.NET 4.0 以后对多线程的实现变得更简单了。

本节主要讨论 .NET4.0 多线程的新特性——使用 Task类创建多线程。

读前必备:

.net 面向对象编程基础

B. 类型[.NET面向对象编程基础] (第18版)

1.线程池ThreadPool

在介绍4.0以后的多线程新特征之前,先简单说一下线程池。

从之前的多线程学习中可以看出, 多线程的操作相对简单, 但真正困难的是如何合理规划好多线程之间的管理与资源释放, 尤其是当线程数量较多时, 更需要有效的机制来进行管理. 同时, 线程池是一种非常有效的工具, 它能够帮助我们更好地解决多线程之间的协调问题

简单来说线程池就是.NET提供的存放线程的一个对象容器。

为什么要使用线性池

在微软官方文档中指出,在开发过程中常见到许多应用程序会创建大量处于睡眠状态(Sleep State)、等待特定事件发生(Event-Driven)的线程(Thread)。此外,在某些情况下,在程序运行期间还会有许多其他线程会暂时进入休眠状态(Hibernation),这些休眠状态的线程仅在需要时被唤醒(Rearranged),并用于定期检查更新或获取最新信息。通过系统提供的工作队列机制(Work Stealing),程序能够更高效地利用资源(Resource Utilization)。

为了简化表述,在介绍新概念时通常会采用更加专业的术语

线程池中的线程分为两种类型:工作线程与IO线程. 线程池是一种多ithink脚本处理机制,在处理任务时会将任务加入队列中等待后续处理.

下面是一个线程池的示例:

复制代码
    using System;
    using System.Threading;
    
    namespace MultithreadingApplication
    {
    class Program
    {
        //先设置一个创建线程总数静态字段:
        static readonly int totalThreads = 20; 
        static void Main(string[] args)
        {
            //线性池是静态类可以直接使用
            //参数1:要由线程池根据需要创建的新的最小工作程序线程数。
            //参数2:要由线程池根据需要创建的新的最小空闲异步 I/O 线程数。
            ThreadPool.SetMinThreads(2, 2);
            //参数1:线程池中辅助线程的最大数目。
            //参数2:线程池中异步 I/O 线程的最大数目。
            ThreadPool.SetMaxThreads(20, 20);
            for (int i = 0; i < totalThreads; i++)
            {
                ThreadPool.QueueUserWorkItem(o =>
                {
                    Thread.Sleep(1000);
                    int a, b;
                    //参数1:可用辅助线程的数目。
                    //参数2:可用异步 I/O 线程的数目。
                    ThreadPool.GetAvailableThreads(out a, out b);
                    Console.WriteLine(string.Format("({0}/{1}) #{2} : {3}", a, b, Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString()));
                });
            }
            Console.WriteLine("主线程完成");
            Console.ReadKey();
        }
    }
    }

2. Task类

通过调用 ThreadPool 的 QueueUserWorkItem() 方法来启动一次异步任务非常简单。然而该方法的主要缺陷在于缺乏内置的方式来获取任务完成状态以及结果。因此,在 .NET 4.0 及以上版本中建议采用 System.Threading.Tasks 中的任务来实现多线程处理。这也是一种在 .NET 4.0 后期被广泛采用的最佳实践。

构造一个 Task 对象,并为泛型 T 参数传递一个操作的返回类型。

Task类可以使用多种方法创建多线程,下面详细介绍。

2.1 使用Factory属性

基于Factory属性的查询返回的结果能够支持多种功能模块的需求

例如,要创建运行操作的 Task ,可以使用工厂的 StartNew 方法:

复制代码
    //最简单的线程示例
    Task.Factory.StartNew(() =>
    {
    Console.WriteLine("我是使用Factory属性创建的线程");
    });

如果想简单的创建一个Task,那么使用Factory.StartNew()来创建,很简便。

如果为所创建的Task增加更多自定义设置并定义特定属性,请继续往下看

2.2 使用Task实例实现多线程
复制代码
    //简单的Task实例创建线程
    Action<object> action = (object obj) =>
    {
    Console.WriteLine("Task={0}, obj={1}, Thread={2}", Task.CurrentId, obj.ToString(), Thread.CurrentThread.ManagedThreadId);
    };
    //上面的是简写形式,也可以写成下面的形式.
    //Action<object> action = new Action<object>((object obj) =>
    //{
    //    Console.WriteLine("Task={0}, obj={1}, Thread={2}", Task.CurrentId, obj.ToString(), Thread.CurrentThread.ManagedThreadId);
    //});
    
    Task t1 = new Task(action, "参数");
    t1.Start();

运行结果如下:

在这里插入图片描述
复制代码
    //简写上面实例,并创建100个线程
    System.Diagnostics.Stopwatch watch = System.Diagnostics.Stopwatch.StartNew();
    int m = 100;
    Task[] tasks = new Task[m];
    for (int i = 0; i < m; i++)
    {
    tasks[i] = new Task((object obj) =>
        {
            Thread.Sleep(200);
            Console.WriteLine("Task={0}, obj={1}, Thread={2},当前时间:{3}",
            Task.CurrentId, obj.ToString(),
            Thread.CurrentThread.ManagedThreadId,
            System.DateTime.Now.ToString());
        }, "参数" + i.ToString()   //public Task(Action<object> action, object state);
    );
    tasks[i].Start();//线程开始
    }
           
    Task.WaitAll(tasks);  //等待提供的所有 System.Threading.Tasks.Task 对象完成执行过程。
    Console.WriteLine("线程耗时:{0},当前时间:{1}" ,watch.ElapsedMilliseconds,System.DateTime.Now.ToString());

这里task创建的100个线程貌似是异步执行的

运行结果如下:

在这里插入图片描述
2.3 Task传入参数

上文阐述了通过将一个Action委托传递给线程以实现其功能的方法。进而当向线程传递参数时,则可借助System.Action来完成相应的操作。

传入一个参数的示例:

复制代码
    /// <summary>
    /// 一个参数的方法
    /// </summary>
    /// <param name="parameter"></param>
    static void MyMethod(string parameter)
    {
    Console.WriteLine("{0}", parameter);
    }

调用如下:

复制代码
    //Task传入一个参数
    Task myTask = new Task((parameter) => MyMethod(parameter.ToString()), "aaa");
    myTask.Start();

传入多个参数如下:

复制代码
    /// <summary>
    /// 多个参数的方法
    /// </summary>
    /// <param name="parameter1"></param>
    /// <param name="parameter2"></param>
    /// <param name="parameter3"></param>
    static void MyMethod(string parameter1,int parameter2,DateTime parameter3)
    {
    Console.WriteLine("{0} {1} {2}", parameter1,parameter2.ToString(),parameter3.ToString());
    }

调用如下:

复制代码
    //Task传入多个参数
    for (int i = 1; i <= 20; i++)
    {              
    new Task(() => { MyMethod("我的线程", i, DateTime.Now); }).Start();
    Thread.Sleep(200);
    }

运行结果如下:

在这里插入图片描述

对于传入多个参数,可以使用无参数委托包装一个多参数的方法来完成。

2.4 Task的结果

要获取Task的结果,在创建Task的时候,就要采用Task来实例化一个Task。

其中的T就是Task执行完成之后返回结果的类型。

通过Task实例的Result属性就可以获取结果。

复制代码
    System.Diagnostics.Stopwatch watch = System.Diagnostics.Stopwatch.StartNew();
    Task<int> myTask = new Task<int>(() =>//这里面泛型委托修饰符out表示协变
    {
    int sum = 0;
    for (int i = 0; i < 10000; i++)
        sum += i;
    return sum;
    });
    myTask.Start();           
    Console.WriteLine("结果: {0} 耗时:{1}", myTask.Result,watch.ElapsedMilliseconds);

这里task创建线程执行完才会继续执行主线程

运行结果如下:

在这里插入图片描述

使用Factory属性来完成上面的示例:

复制代码
    //使用Factory属性创建
    System.Diagnostics.Stopwatch watchSecond = System.Diagnostics.Stopwatch.StartNew();
    Task<int> myTaskSecond = Task.Factory.StartNew<int>(() =>
    {
    int sum = 0;
    for (int i = 0; i < 10000; i++)
        sum += i;
    return sum;
    });            
    Console.WriteLine("结果: {0} 耗时:{1}", myTaskSecond.Result, watchSecond.ElapsedMilliseconds);

这里task创建线程执行完才会继续执行主线程

运行结果如下:

在这里插入图片描述

多线程的基础知识中的一些内容,在处理各种并行任务以及在多核编程中应用时非常关键;学习者可以通过阅读相关的书籍来深入理解该领域。

想要完全深入的学习多线程需要慢慢修炼,不断积累。

3. 本节要点:

A.本点简单介绍了线程池ThreadPool的使用;

B.介绍一使用Task进行多线程创建及Tast的参数传入和返回结果。

线程一些小知识点

concurrent execution and parallel processing represent distinct approaches in task management. example: imagine you want to eat but also play a game. sequential execution entails finishing one task before moving on to the next, whereas concurrent execution allows multitasking by starting multiple tasks simultaneously. concurrent execution enables users to perform tasks in parallel, such as eating and gaming concurrently. parallel processing, however, goes further by allowing simultaneous multitasking without interrupting each individual task, akin to eating while playing a game. this distinction is crucial in understanding how these approaches optimize resource utilization and task completion efficiency.

对应地,在一个异步操作被触发后(即发出后),执行者在未获取结果之前即可进行后续操作(即继续执行后续操作)。一旦该异步操作完成(即完成后),通常会借助状态信号、通知机制以及回调函数等手段来向执行完毕的操作提供反馈信息(即返回信息)。值得注意的是,在异步模式下(即在这种模式下),执行者的返回结果并不受当前操作的影响。

concurrent handling of multi-threaded resource sharing; in Java, all methods must be executed in a synchronized manner, meaning one must wait for the result before proceeding. In simpler terms, synchronization necessitates sequential execution of tasks.

该系统支持多种线程池配置:包括基于主线程的单实例线程池(如\texttt{ThreadPool})、基于任务的并行执行框架(如\texttt{Task})以及自定义化的多级别负载均衡机制等

ThreadPool是一种基于 Thread 的软件组件,在多任务处理中具有重要地位。 由于单个 Thread 在运行时会占用较多资源,并且频繁切换到新的 Thread 会导致 CPU 上下文切换开销增大;因此,在需要频繁启动并立即完成的小规模任务场景下,使用 ThreadPool 是一种高效的选择。 其调度算法是自适应的;根据当前程序的行为模式自动调整配置参数以优化性能;通常情况下,并不需要手动管理这些 Thread 调度工作;此外;ThreadPool 还分为两种类型:Worker 池和 I/O 池

任务或者说是TPL被视为一个更高层次的封装方案。注释(Note)指出其重要性在于续作的作用。续作的作用体现在:高性能的应用通常运行在I/O边界或者UI事件边界上;而TPL通过其续作机制则能够更加便捷地编写高可扩展性的代码。依据某些标志符(例如是否为长运行任务),Task会决定底层使用线程还是多线程队列来进行处理。

结论:优先考虑使用Task而非单独的线程实现。主要基于Thread或ThreadPool实现,并需注意以下几点:首先判断Task是否为长运行任务;其次尽量避免使用Wait方法;再次在完成IO操作后应尽快关闭continuation并释放线程资源;最后若有条件可考虑增加一个Worker来处理后续操作以避免影响后续IO性能。

全部评论 (0)

还没有任何评论哟~