前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C# 学习笔记(17)—— 多线程编程

C# 学习笔记(17)—— 多线程编程

作者头像
Karl Du
发布2023-10-20 18:52:47
2640
发布2023-10-20 18:52:47
举报
文章被收录于专栏:Web开发之路Web开发之路

多线程

进程和线程的概念

当我们打开一个应用程序后,操作系统就会为该应用程序分配一个进程ID,例如打开Word时,你将在任务管理器虚的进程选项卡中看到WINWORD.EXE进程

进程可以理解为一块包含了某些资源的内存区域,操作系统通过进程这一方式把它的工作划分为不同的单元。一个应用程序可以对应多个进程,例如在打开Chrome浏览器时,任务管理器的应用程序选项卡中只有一个Chrome应用程序,而在进程选项卡中却又多个chrome.exe进程

线程是进程中独立执行单元,对于操作系统而言,它通过调度线程来使用应用程序工作。一个进程中至少包含一个线程,我们把该线程称为主线程。线程和进程之间的关系可以理解为:线程是进程的执行单元,操作系统通过调度线程来使应用程序工作;而进程则是线程的容器,它由操作系统创建,又在具体的执行过程中创建了线程。

线程的调度

生活中,要想在吃饭的时候看电视,你需要来回切换这两个动作,它们时由你来进行调度的。在计算机里,线程就相当于你的动作,操作系统就相当于你,操作系统需要调度线程使它们轮流工作。

在操作系统课程中,老师会介绍说“Windows是抢占式多线程操作系统”。之所以说它是抢占式的,是因为线程可以在任意时间里被抢占,来调度另一个线程。操作系统为每个线程分配了0~31中的某一级优先级,而且会把优先级高的线程优先分配给CPU去执行。

Windows 支持七个相对线程优先级:IDle、lowest、BelowNormal、Normal、AboveNormal、hightest 和 Time-Critical。其中,Normal 是默认的线程优先级。程序可以通过设置 Thread 的 Priority 属性来改变线程的优先级,该属性的类型为 ThreadPriority 枚举类型,其成员包括lowest、BelowNormal、Normal、AboveNormal、hightest

成员名称

描述

Lowest

可以将Thread 安排在其他任何优先级的线程之后

BelowNormal

可以将Thread安排在具有Normal优先级的线程之后,在具有Lowest优先级的线程之前

Normal

可以将Thread安排在具有AboveNormal优先级的线程之后,在具有BelowNormal优先级的线程之前。默认情况下,线程具有Normal优先级

AboveNormal

可以将Thread安排在具有Highst优先级的线程之后,在具有Normal优先级的线程之前

Highest

可以将Thread安排在具有任何其他优先级的线程之前

线程也分前后台

线程由前台线程和后台线程之分。在一个进程中,当所有前台线程停止运行后,CLR 会强制结束所有仍在运行的后台线程,这些后台线程被直接种植,却不会抛出任何异常。主线程将一直是前台线程。我们可以使用Thread类来创建前台线程

下面代码演示了前台线程和后台线程之间的区别:

代码语言:javascript
复制
using System;
using System.Threading;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread backThread = new Thread(Worker);
            backThread.IsBackground = true;
            backThread.Start();
            Console.WriteLine("从主线程退出");
        }

        public static void Worker()
        {
            Thread.Sleep(1000);
            Console.WriteLine("从后台线程退出");
        }
    }
}

以上代码首先通过Thread类创建一个线程对象,然后通过设置IsBackground属性来指明该线程为后台线程。如果不设置IsBackground属性,则Thread类所创建的线程将默认为前台线程。

接着,程序会调用Start函数来启动该线程,此时后台线程会执行Worker函数的代码。在Worker函数中,为了体现出前台线程与后台线程的区别,这里调用了Sleep使该后台线程睡眠1秒,然后再执行。

从前面的分析可以看出,该控制台程序有两个线程,一个是运行Main函数的主线程,另一个是运行Worker函数的后台线程。由于前台线程执行完毕后CLR会无条件地终止后台线程地运行,所以在前面地代码中,若启动了后台进程,则主线程将会继续执行。

主线程运行完Console.WriteLine("从主线程退出")语句后就会退出。此时CLR发现主线程运行结束后,则会种植后台线程,然后使整个应用程序结束运行。所以Worker函数中地Console.WriteLine("从后台线程退出")语句将不会执行

如果我们想要代码执行,有3种办法:

1、将所创建的线程设置为非后台线程

2、将主线程在后台线程执行完再执行(Thread.Sleep(1000))

3、在主函数中调用Join函数的方法,确保主线程会在后台线程执行结束后才开始运行

代码语言:javascript
复制
using System;
using System.Threading;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Thread backThread = new Thread(Worker);
            backThread.IsBackground = true;
            backThread.Start();
            backThread.Join();
            Console.WriteLine("从主线程退出");
        }

        public static void Worker()
        {
            Thread.Sleep(1000);
            Console.WriteLine("从后台线程退出");
        }
    }
}

以上代码在调用backThread.Join来确保主线程会在后台线程结束后再运行。这种方式虽然涉及线程同步的概念:再某些情况下,需要两个线程同步运行,即一个线程必须等待另一个线程结束后才能运行。

在前面的代码中,我们使用了Thread(ThreadStart)构造函数来创建线程对象。除此之外,Thread类还提供了另外3个构造函数,它们分别为Thread(ParameterizedThreadStart start)Thread(ParameterizedThreadStart start, int maxStackSize)Thread(ThreadStart start, int maxStackSize)。其中ParameterizedThreadStartThreadStart都是委托类型

线程的容器——线程池

前面我们都是通过Thread类来手动创建进程的,然而线程的创建和销毁都会耗费大量时间,这样的手动操作将造成性能损失。因此,为了避免因通过Thread手动创建线程而造成的损失,.Net引入了线程池机制

线程池

线程池是指用来存放应用程序中要使用的线程集合,你可以将它理解为一个存放线程的地方,这种集中存放的方式有利于对线程进行管理。

CLR初始化时,线程池中没有线程的。在内部,线程池维护了一个操作请求队列,当应用程序想要执行一个异步操作时,你需要调用QueueUserWorkItem方法来将对应的任务添加到线程池的请求队列中。线程池实现的代码会从队列中提取任务,并将其委派给线程池中的线程去执行。

如果线程池中没有空闲的线程,线程池就会创建一个新线程去执行提取的任务。而当线程池线程完成了某个任务时,线程也不会被销毁,而是返回线程池中,等待响应另一个请求。由于线程不会被销毁,所以就避免了由此产生的性能损失

这里需要明确一点:由线程池所创建的线程是后台线程,且它的优先级默认为Normal

通过线程池来实现多线程

要使用线程池中的线程,需要调用静态方法ThreadPool.QueueWorkItem,以指定线程要调用的方法,该静态方法有两个重载版本:

代码语言:javascript
复制
public static bool QueueUserWorkItem(WaitCallback callBack);
public static bool QueueUserWorkItem(WaitCallback callBack, Object state);

这两个方法用于向线程池队列添加一个工作项(work item)以及一个可选的状态数据。然后,这两个方法就会立即返回。工作项是指一个由callback参数标志的委托对象,被委托对象包装的回调方法将由线程池来执行。传入的回调方法必须匹配System.Threading.WaitCallback委托类型,该委托定义为:

代码语言:javascript
复制
public delegate void waitCallbak(object state);

下面通过实例来延时如何使用线程池来实现多线程编程,具体的演示代码如下:

代码语言:javascript
复制
using System;
using System.Threading;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("主线程ID = {0}", Thread.CurrentThread.ManagedThreadId);
            ThreadPool.QueueUserWorkItem(CallBackWorkItem);
            ThreadPool.QueueUserWorkItem(CallBackWorkItem, "work");
            Console.WriteLine("从主线程退出");
        }

        public static void CallBackWorkItem(object state)
        {
            Console.WriteLine("线程池线程开始执行");
            if (state != null)
            {
                Console.WriteLine("线程池线程ID = {0} 传入的参数为 {1}", Thread.CurrentThread.ManagedThreadId, state.ToString());
            }
            else
            {
                Console.WriteLine("线程池线程ID = {0}", Thread.CurrentThread.ManagedThreadId);
            }
        }
    }
}

协作式取消线程池线程

.Net Framework 提供了取消操作的模式,这个模式是协作式的。为了取消一个操作,我们必须首先创建一个System.Threading.CancellationTokenStateSource对象

下面的代码演示了协作式取消的使用方法,主要实现了用户在控制台下敲下回车键后就停止计数的功能:

代码语言:javascript
复制
using System;
using System.Threading;

namespace Demo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("主线程运行");
            CancellationTokenSource cts = new CancellationTokenSource();
            ThreadPool.QueueUserWorkItem(callBack, cts.Token);
            Console.WriteLine("按下回车键取消");
            Console.Read();
            cts.Cancel();
            Console.ReadKey();
        }

        private static void callBack(object state)
        {
            CancellationToken cancellationToken = (CancellationToken)state;
            Console.WriteLine("开始计数");
            Count(cancellationToken, 100);
        }

        private static void Count(CancellationToken token, int countno)
        {
            for (int i = 0; i < countno; i++)
            {
                if (token.IsCancellationRequested)
                {
                    Console.WriteLine("计数取消");
                    return;
                }
                Console.WriteLine("计数为:" + i);
                Thread.Sleep(300);
            }
            Console.WriteLine("计数完成");
        }
    }
}

线程同步

线程同步技术是指在多线程程序中,为了保证后者线程,只有等待前者线程完成之后才能继续执行。这就好比在生活中排队买票,在前面的人没买到票之前,后面的人必须等待

多线程程序中存在的隐患

多线程应用程序可以提高程序的性能,并提供更好的用户体验。然而当我们创建了多个线程后,它们就有可能同时去访问某一个共享资源,这将损坏资源中保存的数据。在这种情况下,我们需要使用线程同步技术,确保某一时刻只有一个线程在操作共享资源

举例来说,火车售票系统程序员云熙多人同时购票,因此该系统肯定采用了多线程技术。但由于系统中有多个线程在对统一资源进行操作,我们必须确保只有在其他线程执行结束后,新的线程才开始执行。这样可以避免多位顾客买到同一张火车票。此时需要使用的就是线程同步技术

为了更好地说明线程同步的必要性,下面给出模拟火车票购票系统的代码:

代码语言:javascript
复制
using System;
using System.Threading;

namespace Demo
{
    class Program
    {
        static int tickets = 100;
        static void Main(string[] args)
        {
            Thread thread1 = new Thread(SaleTicket1);
            Thread thread2 = new Thread(SaleTicket2);
            thread1.Start();
            thread2.Start();
            thread1.Join();
            thread2.Join();
        }

        public static void SaleTicket1()
        {
            while (tickets > 0)
            {
                Console.WriteLine("线程1正在出票:" + tickets--);
            }
        }

        public static void SaleTicket2()
        {
            while (tickets > 0)
            {
                Console.WriteLine("线程2正在出票:" + tickets--);
            }
        }
    }
}

主线程创建了两个出票线程,然后调用了Start函数让两个线程开始运行,即进行买票。我们执行结果可以看出两个线程交替运行,但是出售的火车票好吗并不是连续的,说明以上应用程序的售票过程是不正确的,这也就是多线程程序所存在的问题,因为两个线程访问了同一个全局变量——tickets

为了避免这种情况的发生,我们需要对多个线程进行同步处理,保证在同一时间内只有一个线程访问共享资源,以及保证前面的线程售票完成后,后面的线程才会访问资源

使用监视器对象实现线程同步

监视器对象(Monitor)能确保线程拥有对共享资源的互斥访问权,C# 通过 lock 关键字来提供简化的语法。下面我们使用线程同步技术来修改前面的代码:

代码语言:javascript
复制
using System;
using System.Threading;

namespace Demo
{
    class Program
    {
        static int tickets = 100;
        static object globalObj = new object();
        static void Main(string[] args)
        {
            Thread thread1 = new Thread(SaleTicket1);
            Thread thread2 = new Thread(SaleTicket2);
            thread1.Start();
            thread2.Start();
            thread1.Join();
            thread2.Join();
        }

        public static void SaleTicket1()
        {
            while (tickets > 0)
            {
                try
                {
                    Monitor.Enter(globalObj);
                    Thread.Sleep(1);
                    Console.WriteLine("线程1正在出票:" + tickets--);
                }
                finally
                {
                    Monitor.Exit(globalObj);
                }
            }
        }

        public static void SaleTicket2()
        {
            while (tickets > 0)
            {
                try
                {
                    Monitor.Enter(globalObj);
                    Thread.Sleep(1);
                    Console.WriteLine("线程2正在出票:" + tickets--);
                }
                finally
                {
                    Monitor.Exit(globalObj);
                }
            }
        }
    }
}

注意,使用 Monitor 锁定的对象需要为引用类型,而不能为值类型。因为在将值类型变量传递给 Enter 时,它将被先装箱为一个单独的对象,之后再传递给 Enter 方法;而在将变量传递给 Exit 方法时,也会创建一个单独的引用对象。此时,传递给 Enter 方法的对象和传递给 Exit 方法的对象不同,Monitor 会引发 SynchronizationLockException 异常

线程同步技术存在的问题

在设计应用程序时,应该尽量避免使用线程同步,因为它会引起一些问题

  • 它的使用比较繁琐。我们要用额外的代码把多个线程同时访问的数据包围起来,并获取和释放线程的同步锁。如果在一个代码块忘记获取锁,就有可能造成数据损坏
  • 使用线程同步会影响程序性能。因为获取和释放锁是需要时间的;并且在决定哪个线程先获取锁的时候,CPU 也必须进行协调。这些额外的工作都会对性能造成影响
  • 线程同步每次只允许一个线程访问资源,这会导致线程阻塞。继而,系统会创建更多的线程,CPU 也就要负担更繁重的调度工作。这个过程会对性能造成影响

所以在实际开发过程中,要尽量避免使用线程同步技术,避免使用共享数据

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/08/16 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 多线程
    • 进程和线程的概念
      • 线程的调度
        • 线程也分前后台
        • 线程的容器——线程池
          • 线程池
            • 通过线程池来实现多线程
              • 协作式取消线程池线程
              • 线程同步
                • 多线程程序中存在的隐患
                  • 使用监视器对象实现线程同步
                    • 线程同步技术存在的问题
                    相关产品与服务
                    容器服务
                    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档