多线程(进阶篇小白易懂版)

文章目录

  • 多线程
    • 为什么要有多线程
    • 多线程案例
    • 线程通讯分传主
    • 线程通讯主传分
    • 关闭线程
    • 线程锁

多线程

概念:多线程就是多个线程同时工作的过程,我们可以将线程看作是程序的执行路径,每个线程都定义了一个独特的控制流,用来完成特定的任务。如果您的应用程序涉及到复杂且耗时的操作,那么使用多线程来执行是非常有益的。使用多线程可以节省 CPU 资源,同时提高应用程序的执行效率,例如现代操作系统对并发编程的实现就用到了多线程。

本篇为多线程进阶篇,使用C#语言,用winform辅助演示。多线程的基础(如线程生命周期,线程的属性和方法等)在本人C#博客有具体讲解
直达链接:link

在这里插入图片描述

为什么要有多线程

使用场景:当你有一些可能会阻塞线程(如IO操作、网络请求、复杂计算等)的操作时,可以将它们封装为线程,并使用Thread类创建子线程来异步执行它们。这样可以避免阻塞UI线程或其他重要线程,提高应用程序的响应性和性能。

具体举例比如本段代码中卖烧饼,若一名顾客要求烧饼不带芝麻,店内只有老板一个,老板骂骂咧咧得挑芝麻,此时有人再来买烧饼,只能老板挑完后才能做烧饼,这可以理解为单线程

比如店内还有一名员工,老板则可以把这SB要求交个员工完成,直接为下名顾客服务,这可以理解为多线程。

请添加图片描述

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace _01_多线程初始
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            //
            Thread thread = Thread.CurrentThread;
            thread.Name = "主线程";
        }
        public void tzm()
        {
            Console.WriteLine($"当前任务执行在{Thread.CurrentThread}");
            Thread.Sleep(5000);
            MessageBox.Show("调好了");

        }
        private void button1_Click(object sender, EventArgs e)
        {
            tzm();
        }

        private void button2_Click(object sender, EventArgs e)
        {

            MessageBox.Show("给");
        }

        private void button3_Click(object sender, EventArgs e)
        {
            ThreadStart threadStart = new ThreadStart(tzm);
            Thread thread = new Thread(threadStart);
            thread.Name = "分";
            thread.Start();
        }
    }
}

多线程案例

案例反映

  1. 有的错误(比如分线程操作UI报错)需要使用启动并调试才会检测到错误
  2. 从1中得出分线程不能操作UI(窗体,控件)

在这里插入图片描述

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace _02_多线程案例
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            Thread.CurrentThread.Name = "主";
        }
        void Jisun()
        {
            Console.WriteLine(Thread.CurrentThread.Name+"开始执行");
            int sum = 0;
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(1);
                sum+=i;
            }
            //需要使用启动并调试查看错误
            //因为lable1控件是由主线程创建的,分线程不能去操作
            
            //分线程不能操作UI(窗体,控件)
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Jisun();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            ThreadStart threadStart = new ThreadStart(Jisun);
            Thread thread = new Thread(threadStart);
            thread.Name = "分";
            thread.Start();
        }
    }
}

代码解释

线程通讯分传主

使用场景:有耗时任务交给分线程并执行,然后在需要的时候将分线程的结果传递回主线程。

关键点提取:分传主的两个方法

较为复杂:

  1. 定义一个变量存储主线程的执行期上下文
  2. 设置变量存储当前主线程的执行期上下文
  3. 启动分线程让他执行耗时任务
  4. 分线程调用方法,给主线程传递数据
  5. 定义函数,当分线程发送数据的时候执行

简单方法: Invoke在分线程中修改UI线程(主线程)中对象的属性(下小节详讲)
在这里插入图片描述

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace _02_多线程案例
{
    public partial class Form1 : Form
    {
        //1.定义一个变量存储主线程的执行期上下文
        SynchronizationContext mainContent;
        public Form1()
        {
            InitializeComponent();
            Thread.CurrentThread.Name = "主线程";
        }
        void Fn()
        {
            Console.WriteLine(Thread.CurrentThread.Name);
            Thread.Sleep(3000);
            //4.分线程调用方法,给主线程传递数据
            mainContent.Post(Abc, "我是分线程计算的结果");
        }
        private void button1_Click(object sender, EventArgs e)
        {
            //线程通讯_主传分
            //2.设置变量存储当前主线程的执行期上下文
            mainContent = SynchronizationContext.Current;
            //ThreadStart threadStart = new ThreadStart(Fn);
            // Thread thread = new Thread(threadStart);
            //thread.Name = "分线程";
            //thread.Start();
            //启动分线程执行耗时任务
            //3.启动分线程让他执行耗时任务
            new Thread(new ThreadStart(Fn)) { Name = "分线程" }.Start();
        }
        //5.定义函数,当分线程发送数据的时候执行
        void Abc(object o)
        {
            //函数的参数就是第4步调用函数的时候传递的第二个参数
            Console.WriteLine(o);
            Console.WriteLine("Abc" + Thread.CurrentThread.Name);
            //主线程
            label1.Text = o.ToString();
        }
    }

}

代码解释

在这个示例中,有一个方法被封装为线程并执行,然后在需要的时候将计算结果传递回主线程。

  1. Fn():这个方法模拟了一个耗时操作。它让当前线程休眠3秒,然后使用mainContent.Post()方法将计算结果传递回主线程。

button1_Click事件处理器中,首先获取并保存了主线程的同步上下文,然后创建并启动了一个新的线程来执行Fn()方法。

线程通讯主传分

使用场景:你有一些可能会阻塞线程(如IO操作、网络请求、复杂计算等)的操作时,可以将它们封装为线程,并使用Thread类来异步执行它们。然后,在需要的时候,你可以使用Invoke()方法在UI线程中更新UI元素。这样可以避免阻塞UI线程或其他重要线程,提高应用程序的响应性和性能

本代码实例中,计算任务交给分线程,得出sum结果,需要显示在U世界界面的label1上

关键点提取

  • new Thread(Calc) { Name = "分线程" }.Start(100);:创建一个新的线程,设置其名称为"分线程",并立即启动它,传递参数100给Calc()方法。
  • Invoke(new Action(() => {...})):在UI线程中执行指定的操作。
  • 如果分线程执行的任务方法要接收参数,只能接收object类型,调用Start方法的时候 传递参数即可

在这里插入图片描述

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace _03线程通讯主传分
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        private void Calc(object o)
        {
            int sum = 0;
            for (int i = 0; i < Convert.ToInt32(o); i++)
            {
                Thread.Sleep(1);
                sum += i;
            }
            //Invoke在分线程中修改UI线程(主线程)中对象的属性
            //参数是一个委托类型
            Invoke(new Action(() =>
            {
                label1.Text = $"1到{o}的和为{sum}";
            }));
        }

        private void button1_Click(object sender, EventArgs e)
        {

            //主线程
            // Calc();
            // ThreadStart 没有参数的函数类型
            //1.创建接收参数的委托
            //ParameterizedThreadStart ts = new ParameterizedThreadStart(Calc);
            2.创建线程
            //Thread thread = new Thread(ts);
            //thread.Name = "分线程";
            3.执行线程
            //thread.Start(100);//调用Start方法的时候传递参数即可

            //简写形式
            //注意:如果分线程执行的任务方法要接收参数,只能接收object类型,调用Start方法的时候 传递参数即可
            new Thread(Calc) { Name = "分线程" }.Start(100);
        }

        private void button2_Click(object sender, EventArgs e)
        {
            new Thread(Calc) { Name = "分线程" }.Start(300);
        }

        private void button3_Click(object sender, EventArgs e)
        {
            new Thread(Calc) { Name = "分线程" }.Start(200);
        }
    }
}

代码解释

在这个示例中,有一个方法被封装为线程并执行,然后在需要的时候被终止。

  1. Calc(object o):这个方法计算从1到指定数(由参数o决定)的和,并在计算完成后更新UI元素label1的文本。

button1_Click, button2_Click, 和 button3_Click事件处理器中,分别创建并启动了新的线程来执行Calc()方法,并传递了不同的参数。

关闭线程

使用场景:当一个线程使用完毕,需要使用手动关闭时

关键点提取

  • new Thread(Fn):创建一个新的线程,但不立即启动。
  • thread.Start():启动线程。
  • thread.Abort():终止线程。
    在这里插入图片描述
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace _04关闭线程
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
        void Fn()
        {
            Thread.Sleep(1000);
            Console.WriteLine("1秒过去了");
            Thread.Sleep(1000);
            Console.WriteLine("2秒过去了");
            Thread.Sleep(1000);
            Console.WriteLine("3秒过去了");
            Thread.Sleep(1000);
            Console.WriteLine("4秒过去了");
        }
        Thread thread;
        private void button1_Click(object sender, EventArgs e)
        {
            //Start() 开启线程
            thread = new Thread(Fn);
            thread.Start();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            //销毁当前执行的线程

            thread.Abort();
        }
    }
}

代码解释

在这个示例中,有一个方法被封装为线程并执行,然后在需要的时候被终止。

  1. Fn():这个方法模拟了一个耗时操作。它让当前线程休眠4秒,并在每秒结束时打印一条消息。

button1_Click事件处理器中,创建并启动了一个新的线程来执行Fn()方法。

button2_Click事件处理器中,终止了上述创建的线程。

线程锁

使用场景:当有多个线程需要访问和修改同一个共享资源(如变量、数据结构、文件等)时,就需要使用线程锁来保证操作的安全性。否则,可能会出现数据竞争和不一致的问题。

关键点提取

  1. 创建一个锁(锁一般是一个引用类型 private static readonly object key = new object();
  2. lock (key) {...}:给需要保护数据的地方加锁
  3. 使用锁的注意事项
    1. lock锁括号中使用的锁必须是引用类型,string除外
    2.推荐锁使用静态的、私有的、只读的对象
    3.我们的锁一定要保证不会被对象的外部所操作才有意义,否则就有可能被手动上锁造成死锁
    在这里插入图片描述
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace _05线程锁
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            new Thread(Fn1).Start();
            new Thread(Fn2).Start();
            new Thread(Fn3).Start();
          

        }
      
        void Fn1()
        {
            Thread.Sleep(100);
            Invoke(new Action<string>(changui), "1号线程修改的内容");
        } void Fn2()
        {
            Thread.Sleep(100);
            Invoke(new Action<string>(changui), "2号线程修改的内容");
        } void Fn3()
        {
            Thread.Sleep(100);
            Invoke(new Action<string>(changui), "3号线程修改的内容");
        }
        void changui(object o)
        {
            this.label1.Text = o.ToString();
        }
        //每次点击按钮lable都显示不同的文本,因为不同的线程执行的先后顺序不一定相同
        //执行完成的时刻也不一定相同,因此我们无法掌控某个线程执行的实际,
        // 上面是哪个线程谁都可以先执行完毕,也有可能同时执行完毕


        //1.创建一个锁(锁一般是一个引用类型)
        private static readonly object key = new object();
        
        //多个线程操作一个变量,导致我们的判断无法进行限制
        int apple;
        //使用锁的注意事项
        //1、lock锁括号中使用的锁必须是引用类型,string除外
        //2、推荐锁使用静态的、私有的、只读的对象
        //3、我们的锁一定要保证不会被对象的外部所操作才有意义,否则就有可能被手动上锁造成死锁
        private void button2_Click(object sender, EventArgs e)
        {
            apple = 1;
            new Thread(lisiEat).Start();
            new Thread(zhangsanEat).Start();
           
        }
        void zhangsanEat()
        {
            Console.WriteLine("张三吃苹果,去看看还有没有");
            //2.使用锁将代码锁起来,(互斥的,相互排斥的锁,当锁关闭的时候,另一个位置无法进入)
            lock (key)
            {
                if (apple <= 0)
                {
                    Console.WriteLine("张三:没有苹果了,不吃了");
                    return;
                }
                Thread.Sleep(100);
                apple--;
            }
            Console.WriteLine($"张三吃完了,现在还剩{apple}个苹果");
            Thread.Sleep(2000);
            Console.WriteLine("张三吃完了");
        }
        void lisiEat()
        {
            Console.WriteLine("李四吃苹果,去看看还有没有");
            lock (key)
            {
                if (apple <= 0)
                {
                    Console.WriteLine("李四:没有苹果了,不吃了");
                    return;
                }
                Thread.Sleep(100);
                apple--;
            }
            Console.WriteLine($"李四吃完了,现在还剩{apple}个苹果");
            Thread.Sleep(2000);
            Console.WriteLine("李四吃完了");
        }

      
    }
}

代码解释

button1_Click事件处理器中,创建了三个新的线程,分别执行Fn1(), Fn2(), 和 Fn3()方法。这三个方法都会尝试修改UI元素label1的文本,但由于它们是在不同的线程中运行的,所以我们无法预测它们的执行顺序和完成时间。

button2_Click事件处理器中,创建了两个新的线程,分别执行zhangsanEat()lisiEat()方法。这两个方法都会尝试修改共享变量apple的值,因此需要使用线程锁来保证操作的安全性。

在这里插入图片描述

总的来说,多线程是一种强大而复杂的技术,它可以让你的程序更加响应用户输入,更有效地利用多核处理器,并同时执行多个任务。然而,多线程编程也带来了一些挑战,如数据竞争死锁活锁等问题。因此,你需要深入理解多线程的原理技术,才能有效地使用它。

  • 拓展(博客推荐)
  • 传送门link
  • 传送门link
  • 传送门link

如果觉得文章还不错,可以点赞、收藏和转发,以支持作者继续创作更多教程。 另外本专栏将会持续更新,作者专栏中有已经更新完毕的C#基础教程!!!

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

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

相关文章

DataX 数据库同步部分源码解析

在工作中遇到异构数据库同步的问题,从Oracle数据库同步数据到Postgres&#xff0c;其中的很多数据库表超过百万&#xff0c;并且包含空间字段。经过筛选&#xff0c;选择了开源的DataXDataX Web作为基础框架。DataX 是阿里云的开源产品&#xff0c;大厂的产品值得信赖&#xff…

【JavaWeb】Day39.MySQL概述——数据库设计-DQL(二)

数据库设计-DQL 聚合函数 聚合函数查询就是纵向查询&#xff0c;它是对一列的值进行计算&#xff0c;然后返回一个结果值。&#xff08;将一列数据作为一个整体&#xff0c;进行纵向计算&#xff09; 语法&#xff1a; select 聚合函数(字段列表) from 表名 ; 注意 : 聚合…

LeetCode 热题 100 | 多维动态规划(二)

目录 1 5. 最长回文子串 2 1143. 最长公共子序列 菜鸟做题&#xff0c;语言是 C 1 5. 最长回文子串 核心思想&#xff1a;把总问题拆解为若干子问题。 总问题&#xff1a;从第 i 个字母到第 j 个字母是回文串子问题&#xff1a;从第 i 1 个字母到第 j - 1 个字母是回文…

Obsidian的初步了解、安装及使用

一、为什么是Obsidian&#xff1f; 笔记软件我用的还是比较多了&#xff0c;一开始用有道云笔记&#xff0c;其实我个人觉得有道云笔记还是做的不错的&#xff0c;除了广告多点、功能弱一点、更新慢一点、偶尔收藏会有问题以外还是不错的&#xff0c;免费软件里性价比算是还可…

前端开发中地图定位与距离计算的应用实践

前端开发中地图定位与距离计算的应用实践 在前端开发中&#xff0c;地图功能的应用日益广泛&#xff0c;无论是用户位置的定位、目标距离的计算&#xff0c;还是地址的解析与展示&#xff0c;地图都发挥着不可替代的作用。本文将重点介绍前端开发中实现地图定位、距离计算以及…

windows 系统下全新下载安装 mysql8.0 数据库(详细)

windows 系统下全新下载安装 mysql8.0 数据库&#xff08;详细&#xff09; 段子手168 1、登录官方网站下载&#xff1a; https://dev.mysql.com/downloads/windows/installer/ 2、下载最新版本&#xff0c;一般可能需要注册登录&#xff0c;下载其他历史版本&#xff0c;请…

【LAMMPS学习】八、基础知识(1.3)从一个输入脚本运行多个模拟

8. 基础知识 此部分描述了如何使用 LAMMPS 为用户和开发人员执行各种任务。术语表页面还列出了 MD 术语&#xff0c;以及相应 LAMMPS 手册页的链接。 LAMMPS 源代码分发的 examples 目录中包含的示例输入脚本以及示例脚本页面上突出显示的示例输入脚本还展示了如何设置和运行各…

【智能算法】省时方便,智能算法统计指标——一键运行~

目录 1.常用统计指标2.参数统计检验3.结果展示4.自定义修改测试框架 1.常用统计指标 测试智能算法性能时&#xff0c;常常会用到以下5种常用指标&#xff0c;简单不赘述&#xff1a; 最优值、最差值、均值、中位数、标准差 2.参数统计检验 单纯依靠常用统计指标说服力不足&…

【noVNC】使用noVNC实现浏览器网页访问vnc(基于web的远程桌面)

1.VNC本身提供的http连接方式&#xff0c;可传输文件&#xff0c;画面有卡顿&#xff0c;需要安装jre 2.noVNC访问方式&#xff0c;不可传输文件&#xff0c;画面较为流畅&#xff0c;不用安装插件运行环境 一、noVNC 是什么 Web 端的Vnc软件&#xff0c;通过noVNC&#xff0…

CSS 实现伸缩导航仪表板侧边栏菜单

CSS 实现伸缩导航仪表板侧边栏菜单 效果展示 展开状态 收起状态 CSS 知识点 回顾曲面圆角的实现知识点 字体库准备 菜单的图标使用的是ionicons的图标库&#xff0c;所以需要页面需要引入对应的文件。 <scripttype"module"src"https://unpkg.com/i…

进程间通信 (匿名管道)

一、进程间通信的概念 进程间通信是一个进程把自己的数据交给另一个进程&#xff0c;它可以帮助我们进行数据传输、资源共享、通知事件和进程控制。 进程间通信的本质是让不同的进程看到同一份资源。因此&#xff0c;我们要有&#xff1a; 1、交换数据的空间。2、这个空间不能由…

CNN-Transformer时间序列预测

部分代码&#xff1a; # CNN-Transformer class CNNTransformerEncoder(nn.Module):def __init__(self, input_features, transformer_encoder_heads,embedding_features, cnn_kernel_size, dim_feedforward_enc, n_encoder_layer):super(CNNTransformerEncoder, self).__init…

分析染色体级别的基因组装配揭示了六倍体栽培菊花的起源和进化-文献精读-7

Analyses of a chromosome-scale genome assembly reveal the origin and evolution of cultivated chrysanthemum 分析染色体级别的基因组装配揭示了栽培菊花的起源和进化 六倍体植物基因组的文献&#xff0c;各位同仁还有什么有特色的基因组评论区留言~ 摘要 菊花&#xf…

spring boot —— Spring-Cloud-Zuul(网关服务getway),kafka笔记

一、 引入zuul依赖&#xff1a; org.springframework.cloud spring-cloud-starter-zuul 二、创建应用主类。使用EnableZuulProxy注解开启zuul的API网关服务功能&#xff1a; EnableZuulProxy SpringCloudApplication public class Application { public static void mai…

【漏洞复现】WordPress Welcart 任意文件读取漏洞(CVE-2022-4140)

0x01 产品简介 Welcart 是一款免费的 WordPress 电子商务插件。Welcart 具有许多用于制作在线商店的功能和自定义设置。您可以轻松创建自己的原始在线商店。 0x02 漏洞概述 Welcart存在任意文件读取漏洞&#xff0c;未授权的攻击者可以通过该漏洞读取任意文件&#xff0c;获…

【RAG实践】Rerank,让大模型 RAG 更近一步

RAGRerank原理 上一篇【RAG实践】基于LlamaIndex和Qwen1.5搭建基于本地知识库的问答机器人 我们介绍了什么是RAG&#xff0c;以及如何基于LLaMaIndex和Qwen1.5搭建基于本地知识库的问答机器人&#xff0c;原理图和步骤如下&#xff1a; 这里面主要包括包括三个基本步骤&#…

【无标题】系统思考—心智模式

“直到你使无意识变为有意识&#xff0c;它将指导你的生活并且你会称之为命运。”—卡尔荣格 心智模式深藏于我们内心之中&#xff0c;它潜移默化地影响着我们对世界的理解和判断。往往这些影响是如此隐蔽&#xff0c;以至于我们自己都未必察觉到是什么在驱动我们的选择、决策…

ES7-10:async和await、异步迭代..

1-ES7新特性 indexof如果没有就返回-1&#xff0c;有就返回索引 如果仅仅查找数据是否在数组中,建议使用includes,如果是查找数据的索引位置,建议使用indexOf更好一些 2-ES8-async和await 所有的需要异步处理的Promise对象都写在async中await等待结果 async、await 使异步操…

【MATLAB源码-第184期】基于matlab的FNN预测人民币美元汇率 输出预测图误差图RMSE R2 MAE MBE等指标

操作环境&#xff1a; MATLAB 2022a 1、算法描述 前馈神经网络&#xff08;Feedforward Neural Network, FNN&#xff09;是最简单也是应用最广泛的人工神经网络之一。在许多领域&#xff0c;尤其是数据预测方面&#xff0c;FNN已经展现出了卓越的性能和强大的适应性。 一、…

贪心算法|406.根据身高重建队列

力扣题目链接 class Solution { public:static bool cmp(const vector<int>& a, const vector<int>& b) {if (a[0] b[0]) return a[1] < b[1];return a[0] > b[0];}vector<vector<int>> reconstructQueue(vector<vector<int>…