目录
多线程
概述
进程
进程是应用程序的一个运行实例,包含程序所需资源的内存区域,是操作系统进行资源分配的单元。进程之间相互独立,互不影响。
线程
线程是进程中的一个执行单元,也是 CPU 分配时间片的单位。一个进程可以包含多个线程,线程间相互独立,同一进程中的线程共享当前所有资源。
缺点:
- 频繁创建、销毁线程会损耗性能。
- 访问共享内存可能冲突,需要解决线程安全问题。
- Unity 辅助线程无法访问 Unity API(可通过委托解决,在主线程中调用委托)
多线程实现
Thread
用于创建和控制线程,设置其优先级并获取其状态。
构造方法:
//ParameterizedThreadStart:含一个object类型参数的无返回值的委托
public Thread(ParameterizedThreadStart start);
//ThreadStart:无参无返回值类型委托
public Thread(ThreadStart start);
//分配最大栈空间,超过该分配空间时会抛出 Stack overflow
public Thread(ParameterizedThreadStart start, int maxStackSize);
public Thread(ThreadStart start, int maxStackSize);
示例:(引用自微软官方开发者帮助文档)
using System;
using System.Threading;
public class ThreadExample {
public static void ThreadProc() {
for (int i = 0; i < 10; i++) {
Console.WriteLine("ThreadProc: {0}", i);
Thread.Sleep(0);
}
}
public static void Main() {
Console.WriteLine("Main thread: Start a second thread.");
Thread t = new Thread(new ThreadStart(ThreadProc));
t.Start();
for (int i = 0; i < 4; i++) {
Console.WriteLine("Main thread: Do some work.");
Thread.Sleep(0);
}
Console.WriteLine("Main thread: Call Join(), to wait until ThreadProc ends.");
t.Join();
Console.WriteLine("Main thread: ThreadProc.Join has returned. Press Enter to end program.");
Console.ReadLine();
}
}
ThreadPool
频繁创建、销毁线程会损耗性能,频繁使用的线程可用线程池启用。(默认为后台线程)
//只能使用带 object 参数的委托
//public delefate void WaitCallback(object state);
ThreadPool.QueueUserWorkItem(fun: WaitCallback);
ThreadPool.QueueUserWorkItem(fun: WaitCallback, state: object);
//如果要传入不含参数的委托,可写作
ThreadPool.QueueUserWorkItem(obj =>{
Fun();
});
线程状态
通过 ThreadState 属性可获取线程状态,线程的状态有如下几种:
- Unstarted:线程还未开启
- Running:线程正在正常运行
- Background:线程在后台执行
- Aborted:线程已经“死亡”,但还未转换至 Stopped 状态。
- AbortRequested:已经调用了 Abort() 方法,但还未停止。
- Stopped:线程完全结束(方法执行完毕或 Abort 请求)
- StopRequested:已经发出请求,但还未 Stop
- Suspended:线程已被挂起(通过 Resume() 继续执行)
- SuspendRequest:已经调用 Suspend()方法,但还未挂起
- WaitSleepJoin:线程处于等待、睡眠状态或被新加入线程阻塞
ManualResetEvent
俗称信号灯,用于控制线程的等待、继续执行
//信号灯对象,构造方法参数为默认状态
//true: 绿灯 false: 红灯
ManualResetEvent signal = new ManualResetEvent(false);
new Thread(()=> {
while (true)
{
//绿灯行,红灯停
signal.WaitOne();
Console.WriteLine("running");
Thread.Sleep(100);
}
}).Start();
while (true)
{
Console.WriteLine("Set");
//设置为绿灯
signal.Set();
Thread.Sleep(1000);
Console.WriteLine("ReSet");
//设置为红灯
signal.Reset();
Thread.Sleep(1000);
}
前/后台线程
前台线程:程序会等待所有的前台线程结束后才会退出。
后台线程:后台线程随着程序退出而结束。
通过 Thread 创建的默认为前台线程;通过 ThreadPool 创建的默认为后台线程。
注:在 Unity 中,程序退出时会结束前台线程
//通过属性直接设置
t.IsBackground = true;
线程同步
C#中可以通过 lock 来实现线程同步.
public static void Main()
{
new Thread(Fun).Start();
new Thread(Fun).Start();
Thread.Sleep(100);
}
//只能是引用类型
static object locker = new object();
static void Fun()
{
//等待 locker 同步块索引为 -1 再执行。
//每次只能有一个线程执行块中语句
lock (locker)
{
//...
}
}
ThreadCrossTool
Unity 中辅助线程禁止访问 Unity API,需要将访问到 Unity 相关 API 的代码放入主线程中执行,为了方便,编写一个工具类,提供把委托放入主线程执行的方法。
using System;
using System.Collections.Generic;
using UnityEngine;
public class ThreadCrossTool : MonoSingleton<ThreadCrossTool>
{
class ActionInfo
{
public Action action;
public long excuteTime;
public ActionInfo(Action action, long excuteTime)
{
this.action = action;
this.excuteTime = excuteTime;
}
}
LinkedList<ActionInfo> actionInfoList;
LinkedList<ActionInfo> removeActionInfoList;
private object locker = new object();
private void Awake()
{
actionInfoList = new LinkedList<ActionInfo>();
removeActionInfoList = new LinkedList<ActionInfo>();
Debug.Log(Instance);
}
private void Update()
{
lock (locker)
{
foreach (var item in actionInfoList)
{
if (DateTime.Now.Ticks >= item.excuteTime)
{
item.action();
removeActionInfoList.AddLast(item);
}
}
foreach(var item in removeActionInfoList)
{
actionInfoList.Remove(item);
}
}
removeActionInfoList.Clear();
}
public void ExcuteOnMainThread(Action action, float delay = 0)
{
lock (locker)
{
actionInfoList.AddLast(new ActionInfo(action, (long)(delay * 1000) + DateTime.Now.Ticks));
}
}
}
Socket 编程
UDP
报文模式,数据最大为1472字节,超过部分会被丢弃,一般不要超过512字节。每次发送都需要一次接收对应,顺序与可靠不能保证。
发送
//创建 Socket 对象
IPEndPoint endPoint = new IPEndPoint(IPAddress.Parse("yy.yy.yy.yy"), 9987);
UdpClient client = new UdpClient(endPoint);
//也可一开始绑定接收端
//client.Connect(..);
//字节数据
byte[] dgram = Encoding.UTF8.GetBytes("hello world");
//发送到指定终结点(不写参数发到连接的终结点)
client.Send(dgram, dgram.Length, new IPEndPoint(IPAddress.Parse("xx.xx.xx.xx"), 9985));
//一定记得释放资源
client.Close();
接收
实际中一般会在子线程中阻塞接收数据,下面只是展示用法。
IPEndPoint ip = new IPEndPoint(IPAddress.Parse("xx.xx.xx.xx"), 9985);
UdpClient server = new UdpClient(ip);
while (true)
{
//表示任意终结点
IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, 0);
//阻塞当前线程,接收数据
byte[] data = server.Receive(ref endPoint);
//解码
Console.WriteLine(Encoding.UTF8.GetString(data));
}
//释放资源(这一句在这个代码里是无法访问到的,一般在主线程里释放)
server.Close();
TCP
面向连接的流模式,每次传输数据最大为1470字节。可一次Write,多次Read,反之也可。
三次握手: 客户端执行connect触发
四次挥手: 任意一方执行close触发,先断开的会等待2MSL,避免由网络原因丢失最后一个报文,处于等待状态。
以客户端先先断开为例:
第一次:客户端发出报文请求关闭
第二次:服务端发出报文表示确认收到请求
第三次:服务端发出确认关闭报文
第四次:客户端发出报文表示收到,服务端收到后关闭连接
客户端等待2MSL
客户端等待状态端口占用问题:
解决方案一:客户端每次更换端口
解决方案二:服务端先断开连接(服务器压力大,不推荐)
客户端
//创建socket对象
//客户端绑定端口
TcpClient client = new TcpClient(new IPEndPoint(IPAddress.Parse(""), 9998));
//解决方案一:客户端使用本地随机端口
//TcpClient client = new TcpClient();
//连接服务器
client.Connect(new IPEndPoint(IPAddress.Parse(""), 9997));
//获取网络流
using (NetworkStream stream = client.GetStream())
{
//写入数据
stream.Write(Encoding.UTF8.GetBytes(""));
}
//释放资源
client.Close();
服务端
一般会每连接到一个客户端,就开启一个客户端线程,分别处理每个客户端的消息。
//创建监听器
TcpListener listener = new TcpListener(new IPEndPoint(IPAddress.Parse(""), 9876));
//开始监听
listener.Start();
//接收连接的客户端
TcpClient client = listener.AcceptTcpClient();
using (NetworkStream stream = client.GetStream())
{
byte[] buffer = new byte[512];
while (true)
{
//如果没有读到,阻塞线程
int count = stream.Read(buffer, 0, buffer.Length);
string msg = Encoding.UTF8.GetString(buffer, 0, count);
Console.WriteLine(msg);
//解决方案二:客户端发出断开请求,服务端先于客户端断开
//if(msg == "xx")
//{
// break;
//}
}
client.Close();
listener.Stop();
}