C#学习(十三)——多线程与异步

一、什么是线程

程序执行的最小单元
一次页面的渲染、一次点击事件的触发、一次数据库的访问、一次登录操作都可以看作是一个一个的进程

在一个进程中同时启用多个线程并行操作,就叫做多线程
由CPU来自动处理
线程有运行、阻塞、就绪三态

代码示例:

class Program
{
    static void Main(string[] args)
    {
        Thread thread = new Thread(() =>
        {
            Print1();
        });

        thread.Start();

        for(int i = 0; i < 1000; i++)
        {
            Console.Write(0);
        }

        Console.Read();
    }
    
    static void Print1()
    {
        for (int i = 0;i < 1000; i++)
        {
            Console.Write(1);
        }
    }

}

运行结果为
示例代码运行结果
可以看到,在结果中,0和1的输出是交织在一起的,原因为两个线程交替着被运行,不断反复直到结束。
另外一个常用操作为Sleep();让时间暂停,使得线程进入静默状态。

二、前台线程、后台线程与线程池托管

代码举例:

class Program
{
    static void Main(string[] args)
    {
        Thread thread = new Thread(PrintHello);
        thread.Start();
        Console.WriteLine("退出主程序");
    }

    private static void PrintHello(object? obj)
    {
        while (true)
        {
            Thread.Sleep(1000);
            Console.WriteLine("Hello from PrintHello!");
        }
    }
}

运行会发现,即使主线程运行结束了,子线程依旧在持续运行;持续运行的子线程就称为前台线程
一般来说,只有等待前台线程运行完毕后,程序才可以进行关闭。
与之对应的是 后台线程,可以通过thread.IsBackground = true;//切换为后台线程将前台 线程切换到后台线程,这样再次运行会发现,当主线程结束后,后台线程就会被强制结束。

一般来说,前台线程用于需要时间比较长的等待业务,比如监听客户端请求,而后台线程适用于时较短的业务比如执行客户端发来的请求,后台进程不会影响程序的终止。
托管在线程池中的线程全部为后台线程
所有使用new Thread创建的线程默认均为前台线程

三、线程池

示例代码

for(int i = 0;i < 100; i++)
{
    ThreadPool.QueueUserWorkItem((o) =>
    {
        Console.WriteLine($"循环次数{i} 线程id {Thread.CurrentThread.ManagedThreadId}");
    });
}

线程池执行结果
可以看到执行结果出现了id重复的状况,原因就是线程池会重复使用已经完成的线程,极大节约硬件资源。
另外,可以看到,for循环有100次,但是从输出结果来看,只执行了十几次,原因为线程池创建的线程均为后台线程,只要主程序退出,线程池的后台线程就会被停止,而主程序main执行的时间很短,因此线程池内线程没有来得及执行就被停止了。

对于重要的并发量小的线程,需要手动创建管理,对于并发量大而又不太重要的线程,最好托管到线程池中。

四、结束线程与CancellationToken

不管程序有多少个进程,进程内部的资源都是被共享的。所以C#对进程的取消代码作了更高层次的抽象,把进程的取消过程封装成为了Token的形式,也就是CancellationToken(取消令牌)。不仅可以使用在多线程中,还可以用于异步操作。

class Program
{
    static void Main(string[] args)
    {
        CancellationTokenSource cts = new CancellationTokenSource();

        Thread thread = new Thread(() => { PrintHello(cts.Token); });
        thread.Start();

        //下载文件
        Thread.Sleep(5000);

        //关闭子进程
        //cts.Cancel();
        cts.CancelAfter(3000);//在下载完成后3s失效


        Console.WriteLine("退出主程序");
    }

    private static void PrintHello(CancellationToken tokenSource)
    {
        while (!tokenSource.IsCancellationRequested)
        {
            Thread.Sleep(1000);
            Console.WriteLine("Hello from PrintHello!");
        }
    }
}

五、Join与IsAlive

对于子线程执行时间不确定的情况,需要使用Join的方法,加入至主程序执行中,或者使用IsAlive方法进行判断

class Program
{
    static void Main(string[] args)
    {
        Thread thread = new Thread(() => { PrintHello(); });
        thread.Start();

        //方法一
        //thread.Join();

        //方法二
        while(thread.IsAlive)
        {
            Console.WriteLine("子线程仍在工作");
            Thread.Sleep(100);
        }

        Console.WriteLine("退出主程序");
    }

    private static void PrintHello()
    {
        int i = 0;
        while (i++ < 10)
        {
            Thread.Sleep(new Random().Next(100, 1000));
            Console.WriteLine("Hello from PrintHello!");
        }
    }
}

六、资源竞争与线程锁lock

使用线程可以并发的在CPU的核心中执行任务,最大化CPU的利用率,但是并发执行任务也可能产生各种各样的资源竞争问题。

举例:

    private static void AddText()
    {
        File.AppendAllText(@"D:\test.txt", $"开始{Thread.CurrentThread.ManagedThreadId}");
        Thread.Sleep(100);
        File.AppendAllText((@"D:\test.txt", $"结束{Thread.CurrentThread.ManagedThreadId}");
    }

当两个线程同时需要使用同一个文件资源时,产生资源竞争,导致系统崩溃。
因此必须保证同一时刻只能有一个线程访问资源,避免出现资源恶性竞争。
使用线程锁就可以解决

class Program
{
    static object lockedObj = new object();
    static void Main(string[] args)
    {
        for(int i = 0; i < 10; i++)
        {
            var t = new Thread(AddText); 
            t.Start();
        }

        Console.WriteLine("退出主程序");
    }

    private static void AddText()
    {
        lock(lockedObj)
        {
            File.AppendAllText(@"D:\test.txt", $"开始{Thread.CurrentThread.ManagedThreadId}");
            Thread.Sleep(100);
            File.AppendAllText(@"D:\test.txt", $"结束{Thread.CurrentThread.ManagedThreadId}");
        }           
    }
}

七、异步

在之前项目中,我们实现的所有操作都是同步进行的,然而当有同时10000个请求发生时,会使得用户有很长的等待,服务器会等待数据库的响应,完成后反馈至用户。
而异步操作要实现,不要等待数据库,继续执行下一个请求,当数据返回数据以后,再回头继续处理上一个请求。
然而对于更高级别的数量请求,仅仅依靠异步也是不够的,因此需要:
异步服务+每个机器多开进程+多个机器组合实现;
K8s, Kubernetess容器化分布式部署;
.NET Core对容器化非常非常友好、支持度极高

八、异步编程Task

我们使用异步处理并行,使用多线程处理并发。

异步逻辑是要基于方法没有依赖关系的,例如

 class Program
 {
     static void Main(string[] args)
     {

         Calculate();
         Console.Read();

     }

     static void Calculate()
     {
         //Task->异步,Thread->线程
         Task.Run(() =>
         {
             Calculate1();
         });
         Task.Run(() =>
         {
             Calculate2();
         });
         Task.Run(() =>
         {
             Calculate3();
         });
     }

     static int Calculate1()
     {
         var result = 3;
         Console.WriteLine($"Calculate1: {result}");
         Task.Delay(2000);
         return result;
     }

     static int Calculate2()
     {
         var result = 4;
         Console.WriteLine($"Calculate2: {result}");
         Task.Delay(3000);
         return result;
     }

     static int Calculate3()
     {
         var result = 5;
         Console.WriteLine($"Calculate3: {result}");
         Task.Delay(1000);
         return result;
     }
 }

但如果是具有依赖关系的,例如,Caculate2()需要Caculate1()的结果,Caculate3需要Caculate1()和Caculate2()的结果,那么就需要做如下调整

class Program
{
    static void Main(string[] args)
    {

        Calculate();
        Console.Read();

    }

    static void Calculate()
    {
        //Task->异步,Thread->线程
        var task1 = Task.Run(() =>
        {
            return Calculate1();
        });

        var awaiter1 = task1.GetAwaiter();//获得异步等待对象
        awaiter1.OnCompleted(() =>
        {
            var result1 = awaiter1.GetResult();//获得异步逻辑的最终计算结果
            var task2 = Task.Run(() =>
            {
                return Calculate2(result1);
            });
            var awaiter2 = task2.GetAwaiter();
            awaiter2.OnCompleted(() =>
            {
                var result2 = awaiter2.GetResult();
                var result = Calculate3(result1, result2);
                Console.WriteLine(result);
            });
        });          
    }

    static int Calculate1()
    {
        var result = 3;
        Console.WriteLine($"Calculate1: {result}");
        Task.Delay(2000);
        return result;
    }

    static int Calculate2(int a)
    {
        var result = a * 2;
        Console.WriteLine($"Calculate2: {result}");
        Task.Delay(3000);
        return result;
    }

    static int Calculate3(int a, int b)
    {
        var result = a + b;
        Console.WriteLine($"Calculate3: {result}");
        Task.Delay(1000);
        return result;
    }
}

九、C#的异步 async/await

可以看到,上面的异步操作代码非常复杂繁琐,接下来使用async/await化解上面操作

同步方法
指程序调用某个方法,需要等待执行完成以后才进行下一步操作
异步方法
指程序调用某个方法的时候,不做任何等待,在处理完成之前就返回该方法,继续执行接下来的操作,即函数在执行完成前就可以先返回调用方,然后继续执行接下来的逻辑完成任务的函数

举例:

public async Task<int> DoSomethingAsync()
{
    //创建一个计算1万毫秒的任务
    Task<int> longRunningTask = LongRunningTaskAsync();
    //使用await执行这个任务
    int result = await longRunningTask;
    return result;
}
//假装计算1w毫秒,输出为1
private async Task<int> LongRunningTaskAsync()
{
    await Task.Delay(10000);//延迟10s
    return 1;
}

1.需要使用async关键词
2.返回类型为:voidTaskTask<T>IAsyncEnumerable<T>
3.命名规范:Async结尾
4.需要有await表达式
5.要有返回值
6.async函数只能被async函数调用

共有三个部分:
第一部分异步调用:Task<int> longRunningTask = LongRunningTaskAsync();
第二部分执行异步:int result = await longRunningTask;
第三部分异步方法:private async Task<int> LongRunningTaskAsync(){}

注:

  • [ 在函数声明中,async关键字要放到返回类型之前 ]
  • [ async函数本身不创建异步操作,只有在调用await的时候才会进行异步操作 ]

下面对之前的异步代码进行优化:

class Program
{
    static void Main(string[] args)
    {
        Calculate();
        Console.Read();
    }

    static async void Calculate()
    {
        var result1 = await Calculate1Async();

        var result2 = await Calculate2Async(result1);

        var result = await Calculate3Async(result1, result2);

        Console.WriteLine(result);          
    }

    static async Task<int> Calculate1Async()
    {
        var result = 3;
        Console.WriteLine($"Calculate1: {result}");
        await Task.Delay(2000);
        return result;
    }

    static async Task<int> Calculate2Async(int a)
    {
        var result = a * 2;
        Console.WriteLine($"Calculate2: {result}");
        await Task.Delay(3000);
        return result;
    }

    static async Task<int> Calculate3Async(int a, int b)
    {
        var result = a + b;
        Console.WriteLine($"Calculate3: {result}");
        await Task.Delay(1000);
        return result;
    }
}

十、Task VS. Thread

异步不是多线程!!!
异步用来处理并行,多线程用于处理并发

class Program
{
    static void Main(string[] args)
    {
        TaskTest();
        ThreadTest();
        
        Console.Read();
    }

    static void TaskTest()
    {
        var sw = new Stopwatch();
        sw.Start();

        for(int i = 0; i < 100; i++)
        {
            Task.Factory.StartNew(() => { });
        }

        sw.Stop();
        Console.WriteLine($"Task {sw.ElapsedMilliseconds}");
    }

    static void ThreadTest()
    {
        var sw = new Stopwatch();
        sw.Start();

        for (int i = 0; i < 100; i++)
        {
            new Thread(() => { }).Start();
        }

        sw.Stop();
        Console.WriteLine($"Thread {sw.ElapsedMilliseconds}");
    }
}

执行结果为
Task VS. Thread执行结果
可以看到Task的执行速度要远高于Thread!
异步并不会创建线程,只是通过主线程来执行,同时开出一条分路来执行其他任务,非同步分别执行。但是在最后会创建一个非常轻量级的Worker Thread,用于通知主程序异步结束,也称为回调Call Back.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/391566.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AI:131- 法律文件图像中的隐含信息挖掘与敲诈勒索检测

🚀点击这里跳转到本专栏,可查阅专栏顶置最新的指南宝典~ 🎉🎊🎉 你的技术旅程将在这里启航! 从基础到实践,深入学习。无论你是初学者还是经验丰富的老手,对于本专栏案例和项目实践都有参考学习意义。 ✨✨✨ 每一个案例都附带有在本地跑过的关键代码,详细讲解供…

[AIGC ~ coze] Kafka 消费者——从源码角度深入理解

Kafka 消费者——从源码角度深入理解 一、引言 Kafka 是一个分布式的流处理平台&#xff0c;广泛应用于大规模数据处理和实时数据管道。在 Kafka 生态系统中&#xff0c;消费者扮演着至关重要的角色&#xff0c;它们从 Kafka 主题中读取数据并进行处理。本文将深入探讨 Kafka …

七天入门大模型 :大模型LLM 训练理论和实战最强总结!

本文对于想入门大模型、面试大模型岗位、大模型实具有很强的指导意义。喜欢记得收藏、关注、点赞 文章目录 技术交流群用通俗易懂方式讲解系列总览介绍预训练范式如何确定自己的模型需要做什么训练&#xff1f;模型推理的一般过程PyTorch 框架设备PyTorch基本训练代码范例Trans…

自动化测试:电商管理系统元素定位练习​

本次专题我们来说一下 Python中Unittest 框架的使用及如何通过HTMLTestRunner实现自动化测试报告的自动生成。案例中的代码我们仍旧使用课堂学习中部署的“电商管理系统”来实现。本次练习包括以下几个操作&#xff1a; l 测试用例整体结构设计 l 测试用例的实现 l 测试套的…

linux kernel 内存踩踏之KASAN_SW_TAGS(二)

一、背景 linux kernel 内存踩踏之KASAN&#xff08;一&#xff09;_kasan版本跟hasan版本区别-CSDN博客 上一篇简单介绍了标准版本的KASAN使用方法和实现&#xff0c;这里将介绍KASAN_SW_TAGS和KASAN_HW_TAGS 的使用和背后基本原理&#xff0c;下图是三种方式的对比&#x…

线索化二叉树(先序,中序,后序)+线索化二叉树的遍历【java详解】

目录 线索化二叉树的基本介绍&#xff1a; 举个栗子&#xff1a; 二叉树的中序线索化&#xff1a; 创建HeroNode类&#xff0c;表示节点信息&#xff1a; 编写中序线索化方法代码&#xff1a; 中序线索化遍历代码&#xff1a; 测试代码&#xff1a; 测试结果&#xff1a…

一起学量化之RSI指标

RSI指标 Relative Strength Index,相对强弱指数(RSI),是一个衡量资产过度买入或过度卖出状态的技术指标。 1. RSI的基本概念 当RSI超过70时,通常被认为是超买状态。当RSI低于30时,通常被认为是超卖状态。RSI超过80,被认为是严重超买状态。RSI低于20,被认为是严重超卖状…

微波炉维修笔记

微波主要是靠2.45GHz 左右的微波(12.2cm 波长)加热水分子实现食物加热 所有不要使用金属器皿进行加热&#xff0c;要么因为电磁屏蔽&#xff0c;起不到加热效果&#xff0c;要么火光四射 微波炉基本组成 借鉴姜师傅的视频 碰到不加热其它都正常的问题 1.检查高压电容 使用万…

第10章 JDBC

10.1 什么是JDBC JDBC的全称是Java数据库连接&#xff08;Java Database Connectivity&#xff09;&#xff0c;它是一套用于执行SQL语句的Java API。应用程序可通过这套API连接到关系型数据库&#xff0c;并使用SQL语句完成对数据库中数据的新增、删除、修改和查询等操作。 …

Trie 字典树的两种实现方式

Trie&#xff0c;又称字典树、单词查找树或键树&#xff0c;是一种树形结构&#xff0c;是一种哈希树的变种。典型应用是用于统计&#xff0c;排序和保存大量的字符串&#xff08;但不仅限于字符串&#xff09;&#xff0c;所以经常被搜索引擎系统用于文本词频统计。它的优点是…

github Two-factor authentication (2FA)is required for your GitHub account

问题 github 2FA认证 详细问题 笔者使用GitKraken&#xff0c;使用github登录&#xff0c;github要去 Two-factor authentication (2FA)is required for your GitHub account&#xff0c;即进行2FA认证 解决方案 解决方案一、 微信 → \rightarrow →搜索腾讯身份验证器…

内存块与内存池

&#xff08;1&#xff09;在运行过程中&#xff0c;MemoryPool内存池可能会有多个用来满足内存申请请求的内存块&#xff0c;这些内存块是从进程堆中开辟的一个较大的连续内存区域&#xff0c;它由一个MemoryBlock结构体和多个可供分配的内存单元组成&#xff0c;所有内存块组…

java 培训班预定管理系统Myeclipse开发mysql数据库web结构jsp编程servlet计算机网页项目

一、源码特点 java 培训班预定管理系统是一套完善的java web信息管理系统 采用serlvetdaobean&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发&#xf…

java 宠物医院系统Myeclipse开发mysql数据库web结构jsp编程计算机网页项目

一、源码特点 java 宠物医院系统是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mysql5.0&…

Excel如何把窗口冻结,在下拉滚动条的时候仍然可以看到前几行数据?

** 共分2个情况&#xff1a; ①&#xff1a; 冻结首行&#xff1a; 作用&#xff1a;只冻结第一行的数据窗口。在下拉滚动条时&#xff0c;首行不会动&#xff0c;其他数据行会动步骤如下&#xff1a;1、鼠标放在首行的最左边&#xff0c;然后左键点一下先选中整行2、然后&am…

OpenAI发布Sora技术报告深度解读!真的太强了!

&#x1f60e; 作者介绍&#xff1a;我是程序员洲洲&#xff0c;一个热爱写作的非著名程序员。CSDN全栈优质领域创作者、华为云博客社区云享专家、阿里云博客社区专家博主、前后端开发、人工智能研究生。公粽号&#xff1a;洲与AI。 &#x1f388; 本文专栏&#xff1a;本文收录…

IP地址+子网掩码+CIDR学习笔记

目录 一、IP地址 1、表示方法&#xff1a; 2、特殊IP地址 二、子网掩码 1、判断网络位和主机位 2、子网划分 三、无分类编址CIDR 1、CIDR路由汇聚 汇聚规则&#xff1a; 汇聚ID&#xff1a; 2、最佳路由匹配原则 一、IP地址 1、表示方法&#xff1a; 机器中存放的…

UE Get节点和源码

文章目录 概要UE Get节点有哪些常见的应用场景相关源码 概要 UE Get节点在Unreal Engine的蓝图系统中用于获取变量的值。这个节点通常用于从变量中读取数据&#xff0c;以便在游戏的逻辑流程中使用。 要使用Get节点&#xff0c;你首先需要有一个已经定义的变量。然后&#xf…

电梯控制系列之电梯结构介绍

这篇博客介绍单部10层电梯的完整控制程序框架编写过程&#xff0c;编程语言&#xff1a;SCL&#xff0c;控制器型号&#xff1a;S7-1200PLC。本篇博客介绍和电梯控制相关的一些电梯结构介绍。本文只可作为学习参考资料&#xff0c;行业控制需要遵循电梯安全相关规范。 1、电梯…

【Linux系统化学习】缓冲区

目录 缓冲区 一个样例 现象解释 缓冲区存在的位置 缓冲区 在刚开始学习C语言的时候我们就听过缓冲区这个名词&#xff0c;很是晦涩难懂&#xff1b;在Linux下进程退出时也包含缓冲区&#xff0c;因此缓冲区到底是什么&#xff1f;有什么作用&#xff1f; 让我们先从一个小…