C# 多线程与网络编程

多线程

概述

进程

进程是应用程序的一个运行实例,包含程序所需资源的内存区域,是操作系统进行资源分配的单元。进程之间相互独立,互不影响。

线程

线程是进程中的一个执行单元,也是 CPU 分配时间片的单位。一个进程可以包含多个线程,线程间相互独立,同一进程中的线程共享当前所有资源。

缺点:

  1. 频繁创建、销毁线程会损耗性能。
  2. 访问共享内存可能冲突,需要解决线程安全问题。
  3. 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();
    }
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值