C#多线程编程:Thread类和线程池详解

我们在写程序的时候经常会用到线程技术。简单理解,线程就是可以让程序同时做两(多)件事。比如我们看直播,能在看视频直播和弹幕滚动能同时发生。如果单线程在直播的时候是不能发弹幕的,发送弹幕则不能直播。c#创建和使用线程是利用System.Threading命名空间下的Thread类,下面来介绍一下线程的概念以及如何使用。
在这里插入图片描述

Thread类

我们利用Thread类的方法可以实现线程的操作,比如创建、启动、终止、休眠等。我们创建的程序都会有一个Main()方法,Main()其实就在一个线程中执行,这个线程是我们生成程序时自动创建的。线程分为前台线程和后台线程。我们使用Thread创建的线程默认都是前台线程,在线程池里的线程属于后台线程,如果一个程序的所有前台线程都结束了,那么程序就会结束。后台线程也会结束(无论是否执行完毕),但是后台线程全部结束却不会影响前台线程和整个程序是否结束。我们可以通过IsBackground属性来设置线程是否为后台线程。
创建和启动线程
线程是通过委托来创建的,实例化一个Thread,就能创建一个线程。线程的创建语法为Thread t = new Thread(myMethod),可以使用t.IsBackground = true;来设置线程为后台线程。调用实例的Start()方法即可启动线程。
举一个具体的例子:

using System;
using System.Threading;
namespace ThreadDemo
{
    
    
    class Program
    {
    
    
        static void Main(string[] args)
        {
    
    		
            //创建三个线程,分别打印三个字符
            Thread printA = new Thread(PrintA); 
            Thread printB = new Thread(PrintB);
            Thread printString = new Thread(PrintString); 
            
            //启动这三个线程
            printA.Start();
            printB.Start();
            printString.Start("cc");   //启动带参数的线程
            Console.WriteLine("Main Start!");  
        }
        //打印字符A,50次
        private static void PrintA()
        {
    
       
            for(int i = 0; i < 50; i++)
            {
    
    
                Console.Write("A");
            }           
        }
        private static void PrintB()
        {
    
       
            for(int i = 0;i < 50; i++)
            {
    
    
                Console.Write("B");
            }          
        }
        //带参数的方法只能为Object类型,
        //如果是多个参数可以封装到一个类中,在线程中访问该类实例来访问数据
        private static void PrintString(Object obj)   
        {
    
       
            for(int i = 0;i < 50; i++)
            {
    
    
                Console.Write(obj);
            }           
        }
     }
}
        

执行该程序,可以看到如下(测试时结果可能不一样)。
在这里插入图片描述
我们可以很明显的看出来,打印的结果和我们执行代码的顺序是不一样的!这就是因为这三个函数在“同时”执行。
我们右键Thread转到定义,可以如下代码:

 //
        // 摘要:
        //     Initializes a new instance of the System.Threading.Thread class, specifying a
        //     delegate that allows an object to be passed to the thread when the thread is
        //     started.
        //
        // 参数:
        //   start:
        //     A delegate that represents the methods to be invoked when this thread begins
        //     executing.
        //
        // 异常:
        //   T:System.ArgumentNullException:
        //     start is null.
        public Thread(ParameterizedThreadStart start);
        //
        // 摘要:
        //     Initializes a new instance of the System.Threading.Thread class.
        //
        // 参数:
        //   start:
        //     A System.Threading.ThreadStart delegate that represents the methods to be invoked
        //     when this thread begins executing.
        //
        // 异常:
        //   T:System.ArgumentNullException:
        //     The start parameter is null.
        public Thread(ThreadStart start);

可以看到参数start确实是一种委托(delegate)类型,至于使用哪种委托,要看定义的方法是否带参数,如果不带参数就自动使用第二种ThreadStart类型的委托调用该方法;如果带参数则使用ParameterizedThreadStart类型的委托调用该方法。另外通过这两种构造函数可以知道,我们还可以这样创建线程:

 Thread a = new Thread(new ThreadStart(PrintA)); //不带参数
 Thread c = new Thread(new ParameterizedThreadStart(PrintString));  //带参数

这种方式和例子中的代码是等价的,实际中使用哪种写法都可以。
传递多个参数
前面说过,给线程传递参数只能传递一个Object,但是我们想要传递多个数据怎么办?可以将数据封装到一个类中。在刚才的例子中做这样的修改:

...
Data data = new Data();
printString.Start(data);
...
private static void PrintString(Object obj)
        {
    
    
            Data data = obj as Data;
            for(int i = 0;i < 25; i++)
            {
    
    
                Console.Write(data.c);
            }
            for (int i = 0; i < 25; i++)
            {
    
    
                Console.Write(data.d);
            }
        }
        class Data
        {
    
    
            public string c = "cc";
            public string d = "dd";
        }

终止、取消和休眠线程
我们再看刚才的定义,可以看到有Abort()Join()这个两个方法。这个Abort()方法的作用就是终止线程,我们可以在其他的线程中调用Thread实例的Abort方法来将该线程终止。但是使用这个方法是强制终止线程,属于非正常情况,实际上是取消线程(可能任务还没有完成,线程就被取消了)的执行而不是销毁线程。如果需要等待线程结束,可以调用Join()方法。A线程实例调用B线程的Join()方法后,B会进入等待状态,一直到A线程执行完毕后,B再进入执行状态。我们在第一版的代码上修改一下PrintA方法:

private static void PrintA()
        {
    
    
            Thread tj = new Thread(PrintString);
            tj.Start("d");
            for(int i = 0; i < 50; i++)
            {
    
    
                Console.Write("A");
                if (i == 25)
                    tj.Join();
            }           
        }

运行后会发现打印A字符过程中会打印tj线程的d字符,而且是等待d字符打印完50次后,A再继续打印。
当我们不希望当前线程继续执行,而是希望它停一会时,可以调用Thread的静态方法Sleep(),例如Thread.Sleep(1000),表示当前线程暂停1000ms,也就是1s。需要注意的时,Sleep方法只能暂停当前线程,不能在一个线程中暂停另一个线程。
在这里插入图片描述
线程优先级
线程实际上是由操作系统调用,如果给线程指定优先级那么可以改变线程的调用顺序。操作系统在调度线程时,首先调用优先级最高的线程。如果线程在等待(sleep指令、磁盘io等),那么它会停止运行并释放cpu资源。这时候优先级较低的线程会来抢占cpu。优先级相同的线程会被线程调度器循环调度,逐个交给cpu使用,如果线程被其他线程抢占,那么它就会排到最后。
我们创建的线程优先级默认是Normal,可以设置Priority属性来改变线程的优先级,例如t.priority = ThreadPriority.AboveNormal。优先级级别有Highest,AboveNormal,BelowNormal和Lowest。注意,如果想把某线程设为最高级,会让其他线程难以被执行,程序可能会产生“假死”状态。

线程池(ThreadPool)

我们创建一两个线程的时候使用Thread来创建还是挺方便的,但是如果要创建许多线程时 ,会给系统增加很大的负担。这时我们要将暂时不需要的线程关掉,而当该线程再次需要时打开。如果我们手动来创建这样一个线程列表并维护,这会很复杂。ThreadPool能很好的帮助我们解决这个问题。线程池,顾名思义,是许多个线程的集合。当我们需要创建线程时,它会自动创建;当线程执行完毕是,它会自动将线程关闭。
前面说到过,线程池中的线程是后台线程,这些后台线程是相互关联,由ThreadPool统一调度的。线程池有一个最大线程数,我们创建的线程数如果多于这个最大值,那么就会进入等待队列等待(都是由线程池自动调度的),等某一个现有的线程执行完毕后再加入线程池执行队列执行。另外,一个池中的线程执行完任务后不会被销毁而是会返回等待队列,也就是说,一个线程执行结束了,它还可以被线程池重新使用而不必再次(都是自动的)。
下面来看一个例子:

 class Program
    {
    
    
    	 static void Main(string[] args)
        {
    
    
            for(int i = 0;i < 5; i++)
            {
    
    	
            	//可以直接使用QueueUserWorkItem方法来向线程池中添加任务
                ThreadPool.QueueUserWorkItem(new WaitCallback(tPool));
            }
            Thread.Sleep(1000);
        }
         static void tPool(Object state)
        {
    
    
            for(int i = 0; i < 3; i++)
            {
    
    
                Console.WriteLine("线程{0},正在执行第{1}次循环", Thread.CurrentThread.ManagedThreadId, i);
                Thread.Sleep(100);
            }
        }
     }

在这里插入图片描述
可以看到我们虽然创建了任务,但是实际上只有4个线程被使用,可以改变两次循环的参数再观察结果。
另外线程池还有几点需要注意:

  • 线程池所有线程都是后台线程且不可以设置为前台线程
  • 可以手动设置线程池中最大线程数,默认线程数与虚拟地址空间大小有关
  • 线程池中的线程不是全部都在活跃状态,需要的时候才会有更多线程被激活
  • 添加到线程池中的任务不是一定会被立即执行,如果当前可用线程不足则需要等待
  • 不能给线程池中的线程设置优先级
  • 线程池创建的线程不适合长期运行,适用于不确定线程的使用个数,但每个线程的运行时间不会很长的场景。比如QQ聊天,每打开一个聊天窗口则开启一个池中线程,关上聊天框就会自动结束线程,而QQ本身则应该在一个Thread创建的线程中运行。

猜你喜欢

转载自blog.csdn.net/qq_42893430/article/details/102709705