打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
C# 多线程(菜鸟教程及爱整理)
userphoto

2022.12.09 广东

关注

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

线程生命周期

线程生命周期开始于 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 )
        {
            Thread th = Thread . CurrentThread ;
            th . Name = 'MainThread' ;
            Console . WriteLine ( 'This is {0}', th . 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 threadChild 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: Creating the Child threadChild thread startsChild Thread Paused for 5 secondsChild thread resumes

销毁线程

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 the Child threadChild thread starts012In Main: Aborting the Child threadThread Abort ExceptionCouldn't catch the Thread Exception

C# 多线程--爱整理

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

本节导读:

随着硬件和网络的高速发展,为多线程(Multithreading)处理并行任务,提供了有利条件。

其实我们每时每刻都在享受多线程带来的便利,多核处理器多线程工作、Windows操作系统、Web服务器都在使用多线程工作。

使用多线程直接提高了程序的执行效率,因此学习多线程对提高程序运行能力非常必要,本节主要介绍多线程原理及.NET中多线程在.NET面向对象程序设计中的应用。

1. 关于多线程

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

进程:独立运行的程序称为进程。(比如Windows系统后台程序,也可以称为后台进程)

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

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

并发是针对于单核处理器而言,但是目前市场上的CPU是多核的(一个芯片多个CPU核心),多线程设计可以让多个任务分发到多个CPU上并行执行,可以让程序更快的执行。但是并发通常是提高运行在单核处理器上的程序的性能。这么说感觉有点违背直觉,因为在单核处理器上运行并发程序开销要比顺序执行该程序的开销要大(从上到下顺序执行程序),因为并发程序中增加了上下文切换的代价(一个线程切换到另外一个线程),从表面上看如果顺序执行所有程序反而节省了上下文切换的代价。 让这个问题变得不同的是阻塞,程序中的某个任务因为该程序控制范围之外的一些条件(通常是I/O),整个程序就会停止下来,直到外部条件发生变化。此时多线程的优势就会体现出来了,其他任务可以通过获得CPU时间而继续执行,而不会让整个程序停下来。从性能的角度来看,如果没有线程阻塞,那么在单核处理器上使用并发那将毫无意义。

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命名空间提供了大量的类和接口支持多线程。System.Threading.Thread类是创建并控制线程,设置其优先级并获取其状态最为常用的类。

下面是该类几个至关重要的方法: Thread.Start():启动线程的执行; Thread.Suspend():挂起线程,或者如果线程已挂起,则不起作用; Thread.Resume():继续已挂起的线程; Thread.Interrupt():中止处于Wait或者Sleep或者Join线程状态的线程; Thread.Join():阻塞调用线程,直到某个线程终止时为止 Thread.Sleep():将当前线程阻塞指定的毫秒数; Thread.Abort():终止此线程。如果线程已经在终止,则不能通过Thread.Start()来启动线程。

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为我们提供了一个ParameterizedThreadStart 委托 来解决带一个参数的问题,如下:

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属性定义了执行时线程的状态。线程从创建到线程终止,它一定处于其中某一个状态。

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

B.Running:Thread类的Start() 方法将使线程状态变为Running状态,线程将一直处于这样的状态,除非我们调用了相应的方法使其挂起、阻塞、销毁或者自然终止。

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

D.Running:我们调用Resume()方法使其重新执行,这时候线程将重新变为Running状态。

E.Stopped:一旦线程被销毁或者终止,线程处于Stopped状态。处于这个状态的线程将不复存在,正如线程开始启动,线程将不可能回到Unstarted状态。

F.Background:线程还有一个Background状态,它表明线程运行在前台还是后台。在一个确定的时间,线程可能处于多个状态。

G.WaitSleepJoin、AbortRequested:举例子来说,一个线程被调用了Sleep而处于阻塞,而接着另外一个线程调用Abort方法于这个阻塞的线程,这时候线程将同时处于WaitSleepJoin和AbortRequested状态。

H.一旦线程响应转为Sleep阻塞或者中止,当销毁时会抛出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多线程中的高级应用。

主要有在线程资源共享中的线程安全和线程冲突的解决方案;多线程同步,使用线程锁和线程通知实现线程同步。

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字段都会重新分配内存空间,就当然于一次new操作,这样一来,由于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来记时

改进线程代码如下:

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

和AutoResetEvent 相对的还有一个 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

Semaphore也是线程通知的一种,上面的通知模式,在线程开启的数量很多的情况下,使用Reset()关闭时,如果不使用Sleep休眠一下,很有可能导致某些线程没有恢复的情况下,某一线程提前关闭,对于这种很难预测的情况,.NET提供了更高级的通知方式Semaphore,可以保证在超多线程时不会出现上述问题。

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.线程中静态字段的ThreadStatic特性,使用该字段在不同线程中拥有不同的值

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

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

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

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

本节导读:

上节说了线程同步中使用线程锁和线程通知的方式来处理资源共享问题,这些是多线程的基本原理。

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

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

读前必备:

A. LINQ使用 [.net 面向对象编程基础] (20) LINQ使用

B. 泛型 [.net 面向对象编程基础] (18) 泛型

1.线程池ThreadPool

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

通过前面对多线程的学习,我们发现多线程的创建和使用并不难,难的在于多线程的管理,特别是线程数量级很多的情况下,如何进行管理和资源释放。需要使用线程池来解决。

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

为什么要使用线性池

微软官网说法如下:许多应用程序创建大量处于睡眠状态,等待事件发生的线程。还有许多线程可能会进入休眠状态,这些线程只是为了定期唤醒以轮询更改或更新的状态信息。 线程池,使您可以通过由系统管理的工作线程池来更有效地使用线程。

说得简单一点,每新建一个线程都需要占用内存空间和其他资源,而新建了那么多线程,有很多在休眠,或者在等待资源释放;又有许多线程只是周期性的做一些小工作,如刷新数据等等,太浪费了,划不来,实际编程中大量线程突发,然后在短时间内结束的情况很少见。于是,就提出了线程池的概念。线程池中的线程执行完指定的方法后并不会自动消除,而是以挂起状态返回线程池,如果应用程序再次向线程池发出请求,那么处以挂起状态的线程就会被激活并执行任务,而不会创建新线程,这就节约了很多开销。只有当线程数达到最大线程数量,系统才会自动销毁线程。因此,使用线程池可以避免大量的创建和销毁的开支,具有更好的性能和稳定性,其次,开发人员把线程交给系统管理,可以集中精力处理其他任务。

线程池线程分为两类:工作线程和 IO 线程 . 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。

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

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 中的 Task 类。 这也是.NET 4.0 以后多线程的推荐做法。

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

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

2.1 使用Factory属性

Task 实例可以用各种不同的方式创建。 最常见的方法是使用任务的 Factory 属性检索可用来创建用于多个用途的 TaskFactory 实例。

例如,要创建运行操作的 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<object>来完成。

传入一个参数的示例:

/// <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<T>来实例化一个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的参数传入和返回结果。

线程一些小知识点

并发与并行
例子:当你要吃饭又要玩游戏
顺序执行:先吃完饭再玩游戏
并发:吃口饭玩一会游戏
并行:边吃饭边玩游戏

异步
与同步相对应,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。

同步
同步多线程资源共享;Java中所有方法都是同步调用,应为必须要等到结果后才会继续执行。
简单来说,同步就是必须一件一件事做,等前一件做完了才能做下一件事。

Thread ThreadPool和Task
Thread就是Thread,需要自己调度,适合长跑型的操作。

ThreadPool是Thread基础上的一个线程池,目的是减少频繁创建线程的开销。线程很贵,要开新的stack,要增加CPU上下文切换,所以ThreadPool适合频繁、短期执行的小操作。调度算法是自适应的,会根据程序执行的模式调整配置,通常不需要自己调度线程。另外分为Worker和IO两个池。

Task或者说TPL是一个更上层的封装,NB之处在于continuation。continuation的意义在于:高性能的程序通常都是跑在IO边界或者UI事件的边界上的,TPL的continuation可以更方便的写这种高scalability的代码。Task会根据一些flag,比如是不是long-running来决定底层用Thread还是ThreadPool 

结论:能用Task就用Task,底下都是用的Thread或者ThreadPool。但是要注意细节,比如告诉Task是不是long-running;比如尽量别Wait;再比如IO之后的continuation要尽快结束然后把线程还回去,有事开个Worker做,要不然会影响后面的IO,等等。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
一文说通C#中的异步编程
C#多线程编程(二)线程池与TPL
C# 多线程入门系列(三)
多线程之旅(Thread)
多线程 thread和Task的用法以及注意事项
C#多线程编程
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服