C#线程入门

前言,多线程在日常编码中经常会用,本文就主线程和子线程之间的关系做个大概的总结,若有差错,欢迎斧正。

首先打开任务管理器,查看当前运行的进程: 菜单栏右键选择“Task Manager(任务管理)”或 “Ctrl”+“Alt”+“Delete”点击“Task Manager(任务管理)”

从任务管理器里面可以看到当前所有正在运行的进程。

Tab点击“性能”:

 可以看到当前正在运行的所有进程数“Processes”和线程数“Threads”

一般来说一个应用程序就对应一个进程,一个进程可有一个或多个线程即(>=1个线程),有且只有一个主线程。

进程和线程:

进程:是Windows系统中的一个基本概念,它包含着一个运行程序所需要的资源。一个正在运行的应用程序在操作系统中被视为一个进程,进程可以包括一个或多个线程。线程是操作系统分配处理器时间的基本单元,在进程中可以有多个线程同时执行代码。进程之间是相对独立的,一个进程无法访问另一个进程的数据(除非利用分布式计算方式),一个进程运行的失败也不会影响其他进程的运行,Windows系统就是利用进程把工作划分为多个独立的区域的。进程可以理解为一个程序的基本边界。是应用程序的一个运行例程,是应用程序的一次动态执行过程。

线程:是进程中的基本执行单元,是操作系统分配CPU时间的基本单位,一个进程可以包含若干个线程,在进程入口执行的第一个线程被视为这个进程的主线程。在.NET应用程序中,都是以Main()方法作为入口的,当调用此方法时系统就会自动创建一个主线程。线程主要是由CPU寄存器、调用栈和线程本地存储器(Thread Local Storage,TLS)组成的。CPU寄存器主要记录当前所执行线程的状态,调用栈主要用于维护线程所调用到的内存与数据,TLS主要用于存放线程的状态信息。

前台线程后后台线程:

前台线程:默认情况下创建的线程都是前台线程,只有所有的前台线程关闭才能完成程序关闭。只有将线程属性IsBackground设置为true,才是后台线程

后台线程:程序关闭,后台线程不管是否执行完都将关闭

示例如下:

        static void Main(string[] args)
        {
            Console.OutputEncoding = Encoding.UTF8;

            //前台线程
            Thread qtxc = new Thread(() => RunLoop(10));
            qtxc.Name = "qiantai thread";


            //后台线程
            Thread htxc = new Thread(() => RunLoop(20));
            htxc.Name = "houtai thread";
            htxc.IsBackground = true; //将线程设置为后台线程

            //启动线程
            qtxc.Start();
            htxc.Start();
        }



        public static void RunLoop(int count)
        {
            //获取当前线程名称
            string threadName = Thread.CurrentThread.Name;
            for (int i = 0; i < count; i++)
            {
                Console.WriteLine($"{threadName}  index:{(i + 1).ToString()}");

                //休眠1秒
                Thread.Sleep(1000);
            }
            Console.WriteLine($"{threadName} complete");
        }

代码中定义了一个循环打印的方法,其中前台线程打印10次,后台线程打印20次,运行结果如下:

从运行结果可以看出,前台线程打印完成后,后台线程未打印完,但是程序自动结束了。

然后调整参数,前台线程打印20次,后台线程打印10次:

    class Program
    {
        static void Main(string[] args)
        {
            //前台线程
            Thread qtxc = new Thread(() => RunLoop(20));
            qtxc.Name = "qiantai thread";


            //后台线程
            Thread htxc = new Thread(() => RunLoop(10));
            htxc.Name = "houtai thread";
            htxc.IsBackground = true; //将线程设置为后台线程

            //启动线程
            qtxc.Start();
            htxc.Start();
        }



        public static void RunLoop(int count)
        {
            //获取当前线程名称
            string threadName = Thread.CurrentThread.Name;
            for (int i = 0; i < count; i++)
            {
                Console.WriteLine($"{threadName}  index:{(i + 1).ToString()}");

                //休眠1秒
                Thread.Sleep(1000);
            }
            Console.WriteLine($"{threadName} complete");
        }
    }

 

运行结果如下:

从运行结果可以看出,后台线程打印完成后,前台线程未打印完继续打印,前台线程打印完成后,程序结束。

结论就是:前台线程结束时,不管后台线程是否结束,程序都将结束;后台线程结束时,若有前台线程再运行,程序将继续执行,等到前台线程执行完后,程序结束。

后台线程一般用于处理不重要的事情,应用程序结束时,后台线程是否执行完成对整个应用程序没有影响。如果要执行的事情很重要,需要将线程设置为前台线程。

 主线程和子线程之间的关系:

  • 默认情况,在新开启一个子线程的时候,他是前台线程,只有将线程的IsBackground属性设为true;他才是后台线程
  • 当子线程是前台线程,则主线程结束并不影响其他线程的执行,只有所有前台线程都结束,程序结束
  • 当子线程是后台线程,则主线程的结束,会导致子线程的强迫结束
  • 不管是前台线程还是后台线程,如果线程内出现了异常,都会导致进程的终止。
  • 托管线程池中的线程都是后台线程

C#中线程的使用:

引入命名空间:System.Threading

Thread线程常用方法列表:

方法 说明
Start 开始线程
Sleep 使线程暂停指定的一段时间。如Thread.Sleep(3000),即线程暂停三秒后继续执行
Abort 在线程到达安全点时,使其停止。
Suspend  在线程到达安全点时,使其暂停。
Resume 重新启动挂起的线程
**安全点: **
安全点是指代码中公共语言运行时可以安全地执行自动“垃圾回收”的位置。
垃圾回收是指释放不再使用的变量并回收内存的过程。 调用线程的 Abort 或 Suspend 方法时,公共语言运行时将对代码进行分析,确定让线程停止运行的适当位置。
Thread常用属性:
属性 说明
IsAlive  如果线程处于活动状态,则返回TRUE
IsBackground  是否是后台线程
Name  获取或设置线程的名称。 通常用于在调试时发现各个线程。 
Priority   获取或设置操作系统用于确定线程调度优先顺序的值
 ApartmentState 获取或设置用于特定线程的线程模型。 线程模型在线程调用非托管代码时很重要。
ThreadState  线程状态
IsThreadPoolThread 获取一个值,该值指示线程是否属于托管线程池。
ManagedThreadId 获取当前托管线程的唯一标识符。
每个线程都有一个优先级属性,用于确定其执行所占用的处理器时间片大小。 操作系统为高优先级线程分配较长的时间段,并为低优先级线程分配较短的时间段。 新创建的线程具有值 Normal,但可以将 Priority 属性更改为 ThreadPriority 枚举中的任何值
以上就是Thread类常用属性即方法,其他用法可以查看Thread底层。
 
线程同步:
所谓同步:实质就是在某一时刻,只有一个线程可以访问变量;如果不能确保对变量的访问是同步的,就会产生错误。
C#为同步访问变量提供了一个非常简单的方式,即使用“Lock”关键词,它可以把一段代码定义为互斥段,互斥段在某一个时刻内,只允许一个线程进入执行,而其他线程必须等待。Lock使用语法如下:
Lock(expression)
{
   statement_block
}
来个实例,就拿药店卖药为例子:假设药店的“999感冒灵”总量是固定的,然后分发到各个药店出售,一个药店卖出一包,总量就会减少一包,
再不考虑线程同步的情况下,
示例如下:
    class Program
    {
        static void Main(string[] args)
        {
            //开启两个线程来代表两个分店
            Shop shop = new Shop();

            //线程1,分店1
            Thread thread1 = new Thread(shop.Sell);
            thread1.Name = "shop1";

            //线程2,分店2
            Thread thread2 = new Thread(shop.Sell);
            thread2.Name = "shop2";

            //开启线程,开始卖药
            thread1.Start();
            thread2.Start();

            Console.ReadKey();
        }


    }

    public class Shop
    {
        /// <summary>
        /// 剩余“999感冒灵”的数量
        /// </summary>
        public int num = 10;

        public void Sell()
        {
            do
            {
                string threadName = Thread.CurrentThread.Name;
                if (num > 0)
                {
                    Thread.Sleep(1000);
                    num -= 1;
                    Console.WriteLine($"{threadName} sell a“999”,stock{num} pack");
                }
                else
                {
                    Console.WriteLine("stock nothing!");
                }
            } while (num > 0);//判断是否有库存,如果有就可以持续出售
        }
    }

运行结果:

从运行结果可以看出,在线程未同步的情况下,两家药店都在销售库存的药,但是店铺1销售的时候不知道库存没有了,于是出现了库存 “-1”的情况,显然是有问题的;

然后调整方案,加入线程同步,示例如下:

    class Program
    {
        static void Main(string[] args)
        {
            //开启两个线程来代表两个分店
            Shop shop = new Shop();

            //线程1,分店1
            Thread thread1 = new Thread(shop.Sell);
            thread1.Name = "shop1";

            //线程2,分店2
            Thread thread2 = new Thread(shop.Sell);
            thread2.Name = "shop2";

            //开启线程,开始卖药
            thread1.Start();
            thread2.Start();

            Console.ReadKey();
        }


    }

    public class Shop
    {
        /// <summary>
        /// 剩余“999感冒灵”的数量
        /// </summary>
        public int num = 10;

        public void Sell()
        {
            do
            {
                string threadName = Thread.CurrentThread.Name;
                lock (this) //使用lock关键字解决线程同步的问题
                {
                    if (num > 0)
                    {
                        Thread.Sleep(1000);
                        num -= 1;
                        Console.WriteLine($"{threadName} sell a“999”,stock{num} pack");
                    }
                    else
                    {
                        Console.WriteLine("stock nothing!");
                    }
                }
            } while (num > 0);//判断是否有库存,如果有就可以持续出售
        }
    }

运行结果如下:

 从运行结果可以看出,在线程同步的情况下,每个药店都从库存里面拿药,药店1出售的时候,药店2就等着,当药店1销售完最后一包药,就没有另外的销售记录了,无论是药店1还是药店2来拿药,库存都没有了,这就是线程同步的重要性!

结论:expression代表你希望跟踪的对象:
           如果你想保护一个类的实例,一般地,你可以使用this;
           如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可以了
          而statement_block就算互斥段的代码,这段代码在一个时刻内只可能被一个线程执行。

 以上就是线程的入门级教程~

posted @ 2021-10-29 10:27  #疆先绅#  阅读(362)  评论(0编辑  收藏  举报