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

多线程

概念:多线程就是多个线程同时工作的过程,我们可以将线程看作是程序的执行路径,每个线程都定义了一个独特的控制流,用来完成特定的任务。如果您的应用程序涉及到复杂且耗时的操作,那么使用多线程来执行是非常有益的。使用多线程可以节省 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的值,因此需要使用线程锁来保证操作的安全性。

在这里插入图片描述

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

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

  • 26
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值