本文主要是练习在C#中如何开启多个多线程方便我们在需要多事件并发的时候防止主界面卡死失效,提高并发事件的执行率。
一、线程概述
1.常见并发事件
(1)编写快速响应的用户界面
在WPF、移动应用和WindowsForms应用程序中,都需要并发执行耗时任务以保
证用户界面的响应性。
(2)可以处理同时出现的请求
在服务器上,客户端的请求可能会并发到达,必须通过并行处理才能够保证程序的
可伸缩性。如果使用ASP.NET、WCF或者WebServices,则.NETFramework会自
动执行并行处理。然而,程序员仍然需要关注某些共享的状态(例如使用静态变量
作为缓存)。
(3)并行编程
如果可以将负载划分到多个核心上,那么多核、多处理器计算机就可以提升密集计
算代码的执行速度(第23章将专门介绍这方面的内容)。
(4)预测执行
在多核主机上,有时可通过预测的方式提前执行某些任务来改善程序性能。例如
LINQPad使用这种方式来提高查询的创建速度。而如果事先无法知道哪种方法是
最优的,则可以并行执行多个解决同一任务的不同算法,最先完成的算法就是最
优的。
2.定义
线程是一个可以独立执行的执行路径。
每一个线程都运行在一个操作系统进程中。这个进程提供了程序执行的独立环境。在单线程(singlethreaded)程序中,进程中只有一个线程运行,因此线程可以独立使用进程环境。而在多线程程序中,一个进程中会运行多个线程。它们共享同一个执行环境(特别是内存)。这在一定程度上说明了多线程的作用。例如,可以使用一个线程在后台获得数据,同时使用另一个线程显示所获得的数据。而这些数据就是所谓的共享状态(shared state)。
3.线程基本操作
线程的生命周期包括:创建线程 、暂停线程、线程等待、终止线程:
(1)创建线程:通过new 一个Thread对象并指定线程执行函数创建线程。调用Start方法开启线程
(2)暂停线程:通过在线程函数中调用Thread.Sleep()
暂停当前线程,使线程进入休眠状态
(3)线程等待:假设有线程A,在主程序中调用了A.Join()方法,该方法允许我们等待直到线程A完成。当线程A完成 时,主程序会继续运行。
(4)终止线程:通过调用Thread.Abort()方法强制终止线程。但是通常该操作会比较危险,因此并不建议直接只用。
4.线程同步概述
同步(synchronization)是指协调并发操作,得到可以预测的结果的行为。同步在多个线程访问相同的数据时显得尤为重要,但这种操作很容易出现问题。主要包含以下三种:
(1)排它锁:排它锁每一次只允许一个线程执行特定的活动或一段代码。它的主要目的是
令线程访问共享的写状态而不互相影响。排它锁包括lock、Mutex和SpinLock。本例中会使用lock
(2)非排它锁:非排它锁实现了有限的并发性。非排它锁包括Semaphore(slim)和
ReaderWriterLock(Slim)
(3)信号发送结构:这种结构允许线程在接到一个或者多个其他线程的通知之前保持
阻塞状态。信号发送结构包括ManualResetEvent(Slim)、AutoResetEvent
CountdownEvent和Barrier。前三者就是所谓的事件等待句柄(event waithandles)
二、项目创建
1.项目准备
打开Visual Studio创建一个窗体项目,在里面实现多线程操作。
客户端程序(控制台、WPF、UWP或者WindowsForms)在启动时都会从操作系统自动
创建一个线程(主线程)。除非(直接或者间接的)手动创建多个线程,否则该应用程序就是一个单线程的应用程序。
修改项目基本属性:找到项目-属性-应用程序修改为控制台应用程序。
2.项目创建
(1)项目1-生成不同线程ID
先新创建一按钮,用于通过for循环创建多个线程并在控制台打印:
双击进入点击事件:
#region 项目1
//新创线程变量thread_Create
Thread thread_Create;
//生成多线程ID函数
void ID_Run()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("ID:"+i);
}
}
private void button1_Click(object sender, EventArgs e)
{
thread_Create = new Thread(ID_Run);
thread_Create.Start();
}
#endregion
运行出现当我们线程一经start启用之后便会生成十个线程ID,但是这个并不是生成不同的线程ID,而是在一个线程里面生成了不同的ID,为此,我们需要对代码进行修改,要求在按钮按下的时候自动调用十次以生成不同的线程ID,这里就还要使用到,线程传参:
#region 项目1
//新创线程变量thread_Create
Thread thread_Create;
//同一线程生成线程ID函数
void ID_Run()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("ID:"+i);
}
}
// 多线程生成线程ID函数
void Mutil_ID_Run(int id)
{
Console.WriteLine("ID:"+id);
}
private void button1_Click(object sender, EventArgs e)
{
//thread_Create = new Thread(ID_Run);
//thread_Create.Start();
for (int i = 0;i < 10; i++)
{
int index = i;
thread_Create = new Thread(() =>
{
Mutil_ID_Run(index);
});
thread_Create.Start();
}
}
#endregion
如此便可生成不同的线程ID:
(2)项目二-子线程修改主线程
新创一个开启线程按钮、一个关闭线程按钮、一个label标签用于显示所开启的线程数量。
首先双击进入开启事件的点击事件,在里面将实现对于线程的开启,并通过此线程操作主界面线程的label标签实时改变。在这里我们所创建的WindowsForms窗口系统会默认将它设为主线程,因为会监控主界面所有的操作变化。
#region 项目二-子线程修改主线程
Thread thread;
bool flag=true;//关闭线程使用
private void button_open_Click(object sender, EventArgs e)
{
int index = 0;//记录线程开启了几次
thread = new Thread(() =>
{
//打印查看当前线程是否属于子线程
Console.WriteLine("当前名称" + Thread.CurrentThread.Name + " 当前ID:" + Thread.CurrentThread.ManagedThreadId);
//直接写要执行的逻辑
while (flag)
{
Console.WriteLine("当前名称" + Thread.CurrentThread.Name + " 当前ID:" + Thread.CurrentThread.ManagedThreadId);
index++;
//this.InvokeRequired 判断 当前线程 和我this(Form 主线程)是否线程安全
//如果 this.InvokeRequired 为true证明不安全 如果为false证明安全
if (this.InvokeRequired)
{
//得委托主线程帮忙修改下
this.Invoke(new Action(() =>
{
//打印查看是否是主线程运行
Console.WriteLine("当前名称" + Thread.CurrentThread.Name + " 当前ID:" + Thread.CurrentThread.ManagedThreadId);
label1.Text = index.ToString();
}));
Thread.Sleep(500);
}
else
{
label1.Text = index.ToString();
Thread.Sleep(500);
}
}
});
thread.Name = "我是子线程";
thread.Start();
}
private void button_close_Click(object sender, EventArgs e)
{
flag = false;
thread.Abort();//强行关闭线程
}
#endregion
在控制台打印时会发现,最初我们是出于子线程里面,进入循环之后,要对主线程中的label标签进行操作,利用this.InvokeRequired 判断 当前线程 和我this(Form 主线程)是否线程安全,安全之后进入主线程会打印主线程的相关信息以完成操作:
(3)项目三-多线程抢票小案例
在项目开始之前,先梳理逻辑:假设当前票数只有3张,有10个人要抢票,在抢票窗口开始的一瞬间,10个人同时开始抢票,并非按照顺序抢票,一个窗口只能接受一个人抢票,这时候就会极大可能造成抢票窗口堵塞出现抢票错误问题。
对于电脑运行来说,当执行线程启动的时候也就是一瞬间的事,当线程一启动就会出现抢票错误,我们先实现基本逻辑,新创一个抢票按钮,一个按钮用于操作显示票数,一个label标签显示票数。
此时按照原来的逻辑,利用for循环连续十次调用十次线程,模拟十个人进行抢票,当按下开始按钮之后,一瞬间十个进程全部调用GrabTicket()方法,此时ticket=3,全部显示调用成功!
#region 项目三-多线程抢票小案例
int ticket = 3;
void GrabTicket()
{
if (ticket > 0)
{
Thread.Sleep(1000);
ticket--;
Console.WriteLine(Thread.CurrentThread.Name + "抢票成功Success");
}
else
{
Console.WriteLine(Thread.CurrentThread.Name + "抢票成功Fail");
}
}
private void button2_Click(object sender, EventArgs e)
{
for (int i = 0; i < 10; i++)
{
int index = i;
Thread thread = new Thread(() =>
{
GrabTicket();
});
thread.Name = "ID" + index.ToString();
thread.Start();
}
}
private void button3_Click(object sender, EventArgs e)
{
label2.Text = "当前票数:>" + ticket.ToString();
}
#endregion
运行结果如下,在一瞬间全部成功,显示票数的时候为-7,明显是错误的,因此,我们引入锁lock概念。
锁(lock)的概念:
每一次只能有一个线程锁定同步对象,而其他线程则被阻塞,直至锁释放。如果参与竞争的线程多于一个,则它们需要在准备队列中排队,并以先到先得的方式获得锁”排它锁会强制以所谓序列(serialized)的方式访问被锁保护的资源因为线程之间的访问是不能重叠的。
利用这个特性,在本例中就可以解决十个人同时进行抢票的进程,强制序列化,先实例化一个lock对象,在函数方法里面进行锁的操作。
#region 项目三-多线程抢票小案例
int ticket = 3;
object suo = new object();//实例化锁对象
void GrabTicket()
{
lock (suo)//对访问进程进行序列化操作
{
if (ticket > 0)
{
Thread.Sleep(1000);
ticket--;
Console.WriteLine(Thread.CurrentThread.Name + "抢票成功Success");
}
else
{
Console.WriteLine(Thread.CurrentThread.Name + "抢票成功Fail");
}
}
}
private void button2_Click(object sender, EventArgs e)
{
for (int i = 0; i < 10; i++)
{
int index = i;
Thread thread = new Thread(() =>
{
GrabTicket();
});
thread.Name = "ID" + index.ToString();
thread.Start();
}
}
private void button3_Click(object sender, EventArgs e)
{
label2.Text = "当前票数:>" + ticket.ToString();
}
#endregion
至此案例完成,利用锁的特性可以很好解决面临多进程同时访问的操作,完成代码如下,还请各位大佬批评指正~~~
三、完整代码
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Reflection.Emit;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml.Serialization;
namespace 自测
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
label2.Text = "当前票数:>" + ticket.ToString();
Thread.CurrentThread.Name = "我是主线程";
}
#region 项目1-生成不同线程ID
//新创线程变量thread_Create
Thread thread_Create;
//同一线程生成线程ID函数
void ID_Run()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("ID:"+i);
}
}
// 多线程生成线程ID函数
void Mutil_ID_Run(int id)
{
Console.WriteLine("ID:"+id);
}
private void button1_Click(object sender, EventArgs e)
{
//thread_Create = new Thread(ID_Run);
//thread_Create.Start();
for (int i = 0;i < 10; i++)
{
int index = i;
thread_Create = new Thread(() =>
{
Mutil_ID_Run(index);
});
thread_Create.Start();
}
}
#endregion
#region 项目二-子线程修改主线程
Thread thread;
bool flag=true;//关闭线程使用
private void button_open_Click(object sender, EventArgs e)
{
int index = 0;//记录线程开启了几次
thread = new Thread(() =>
{
//打印查看当前线程是否属于子线程
Console.WriteLine("当前名称" + Thread.CurrentThread.Name + " 当前ID:" + Thread.CurrentThread.ManagedThreadId);
//直接写要执行的逻辑
while (flag)
{
Console.WriteLine("当前名称" + Thread.CurrentThread.Name + " 当前ID:" + Thread.CurrentThread.ManagedThreadId);
index++;
//this.InvokeRequired 判断 当前线程 和我this(Form 主线程)是否线程安全
//如果 this.InvokeRequired 为true证明不安全 如果为false证明安全
if (this.InvokeRequired)
{
//得委托主线程帮忙修改下
this.Invoke(new Action(() =>
{
//打印查看是否是主线程运行
Console.WriteLine("当前名称" + Thread.CurrentThread.Name + " 当前ID:" + Thread.CurrentThread.ManagedThreadId);
label1.Text = index.ToString();
}));
Thread.Sleep(500);
}
else
{
label1.Text = index.ToString();
Thread.Sleep(500);
}
}
});
thread.Name = "我是子线程";
thread.Start();
}
private void button_close_Click(object sender, EventArgs e)
{
flag = false;
thread.Abort();//强行关闭线程
}
#endregion
#region 项目三-多线程抢票小案例
int ticket = 3;
object suo = new object();//实例化锁对象
void GrabTicket()
{
lock (suo)//对访问进程进行序列化操作
{
if (ticket > 0)
{
Thread.Sleep(1000);
ticket--;
Console.WriteLine(Thread.CurrentThread.Name + "抢票成功Success");
}
else
{
Console.WriteLine(Thread.CurrentThread.Name + "抢票成功Fail");
}
}
}
private void button2_Click(object sender, EventArgs e)
{
for (int i = 0; i < 10; i++)
{
int index = i;
Thread thread = new Thread(() =>
{
GrabTicket();
});
thread.Name = "ID" + index.ToString();
thread.Start();
}
}
private void button3_Click(object sender, EventArgs e)
{
label2.Text = "当前票数:>" + ticket.ToString();
}
#endregion
}
}