文章目录
- 多线程
- 为什么要有多线程
- 多线程案例
- 线程通讯分传主
- 线程通讯主传分
- 关闭线程
- 线程锁
多线程
概念
:多线程就是多个
线程同时
工作的过程,我们可以将线程看作是程序的执行路径,每个线程都定义了一个独特的控制流
,用来完成特定的任务。如果您的应用程序涉及到复杂且耗时
的操作,那么使用多线程来执行是非常有益的。使用多线程可以节省 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();
}
}
}
多线程案例
案例反映
:
- 有的错误(比如分线程操作UI报错)需要使用启动并调试才会检测到错误
- 从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();
}
}
}
代码解释
线程通讯分传主
使用场景
:有耗时任务交给分线程并执行,然后在需要的时候将分线程的结果传递回主线程。
关键点提取
:分传主的两个方法
较为复杂:
- 定义一个变量存储主线程的执行期上下文
- 设置变量存储当前主线程的执行期上下文
- 启动分线程让他执行耗时任务
- 分线程调用方法,给主线程传递数据
- 定义函数,当分线程发送数据的时候执行
简单方法: 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();
}
}
}
代码解释
在这个示例中,有一个方法被封装为线程并执行,然后在需要的时候将计算结果传递回主线程。
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);
}
}
}
代码解释
在这个示例中,有一个方法被封装为线程并执行,然后在需要的时候被终止。
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();
}
}
}
代码解释
在这个示例中,有一个方法被封装为线程并执行,然后在需要的时候被终止。
Fn()
:这个方法模拟了一个耗时操作。它让当前线程休眠4秒,并在每秒结束时打印一条消息。在
button1_Click
事件处理器中,创建并启动了一个新的线程来执行Fn()
方法。在
button2_Click
事件处理器中,终止了上述创建的线程。
线程锁
使用场景
:当有多个线程需要访问和修改同一个共享资源(如变量、数据结构、文件等)时,就需要使用线程锁来保证操作的安全性。否则,可能会出现数据竞争和不一致的问题。
关键点提取
:
- 创建一个锁(锁一般是一个引用类型 private static readonly object key = new object();
lock (key) {...}
:给需要保护数据的地方加锁- 使用锁的
注意事项
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#基础教程!!!