Contents

多线程和异步方法

什么是进程

  • 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动
  • 是系统进行资源分配和调度的基本单位,是操作系统结构的基础
  • 打开一个应用程序 就是在操作系统上创建了一个进程
  • 进程之间可以相互独立运行 不糊干扰
  • 进程之间也可以互相访问、操作

什么是线程

  • 操作系统能够进行运算调度的最小单位
  • 他被包含在进程之中,是进程中的实际运作单位
  • 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程
  • 我们目前写的程序 都在主线程中

通过代码开启线程

  • 线程类Thread
  • 需要引用命名空间 System.Threading;
using System.Threading;

class Test{

    public Main(){
        //开启一个新的线程 需要传入一个委托类型
        Thread t = new Thread(NewThreadLogic);
        //启动线程 默认是前台线程
        t.Start();
	}

    static void NewThreadLogic(){
        //新开线程 执行的代码逻辑 
        while(true){
        //前台线程:当主线程的代码执行完 依然不会停止 可能会导致进程无法正常关闭
        Console.WriteLine("新的线程");
        }
    }
}

后台线程

  • 主线程的代码执行完毕后 如果前台线程的代码没有执行完 程序依然不会关闭
  • 前台线程可能会影响程序的正常关闭
  • 可通过IsBackground属性 将线程设置为后台线程 在主线程的代码执行完后 后台线程也会一起关闭
t.IsBackground = true;//设置为后台线程	

关闭线程

  • 如果开启的线程中不是死循环 是能够结束的逻辑 那么 不用刻意的去关闭它
  • 如果是死循环 想要中止这个线程 有两种方式

方法一:将线程置空

//这时t会变成一个垃圾对象 被垃圾回收
t = null;

方法二:通过线程提供的方法(注意在.Net core版本中无法中止 会被报错)

//中止线程
t.Abort();//C#控制台程序会报错 可以使用tray catch
t.null;

线程休眠

  • 在哪个线程里执行 就休眠哪个线程
  • 让线程休眠多少毫秒 1s = 1000毫秒
//休眠两秒
Thread.Sleep(2000);

线程之间共享数据

  • 多个线程使用的内存是共享的,都属于该应用程序(进程)
  • 所以要注意 当多个线程 同时操作同一片内存区域时可能会出现问题
  • 可通过加锁lock的形式避免问题
    • 当我们在多个线程中想要访问同样的东西进行逻辑处理时为了避免不必要的逻辑顺序执行的差错
    • lock(引用类型对象)
Main(){
    Thread t = new Thread(NewThreadLogic);
    t.Start();
    while(true){
        //执行代码前会去检查obj是否被锁住 如果没有被锁住则执行代码块里的逻辑
        //如果被锁住 就等解锁了再执行
        local(obj){
            //执行代码时 会锁住obj
            Console.SetCursorPosition(1,1);
            Console.ForgroundColor = ConsoleColor.Yellow;
            Console.WriteLine("▲");
        }
     }
}
static Object obj;
static void NewThreadLogic(){
        while(true){
            local(obj){
                Console.SetCursorPosition(10,5);
                Console.ForgroundColor = ConsoleColor.Red;
                Console.WriteLine("▮▲");
            }
        }
    }

多线程对我们的意义

  • 可以用多线程处理一些复杂耗时的逻辑(寻路 网络通信等)
  • 这些如果卸载主线程 会卡住

ThreadPool线程池

  • 在多线程的应用程序开发中,频繁的创建删除线程会带来性能消耗,产生内存垃圾
  • 为了避免这种开销C#推出了 线程池ThreadPool类

ThreadPool中有若干数量的线程,如果有任务需要处理时,会从线程池中获取一个空闲的线程来执行任务

任务执行完毕后线程不会销毁,而是被线程池回收以供后续任务使用

当线程池中所有的线程都在忙碌时,又有新任务要处理时,线程池才会新建一个线程来处理该任务,

如果线程数量达到设置的最大值,任务会排队,等待其他任务释放线程后再执行

线程池能减少线程的创建,节省开销,可以减少GC垃圾回收的触发

优点:节省开销,减少线程的创建,进而有效减少GC触发

缺点:不能控制线程池中线程的执行顺序,也不能获取线程池内线程取消/异常/完成的通知

获取可用的工作线程数和I/O线程数

int num1;
int num2;
ThreadPool.GetAvailableThreads(out num1, out num2);
print(num1);
print(num2);

设置线程池中可以同时处于活动状态的 工作线程的最大数目和I/O线程的最大数目

//大于次数的请求将保持排队状态,知直到线程池线程变为可用
//更改成功返回true,失败返回false
if(ThreadPool.SetMaxThreads(20, 20))
{
    print("更改成功");
}

获取线程池中工作线程的最大数目和I/O线程的最大数目

ThreadPool.GetMaxThreads(out num1, out num2);
print(num1);
print(num2);

设置 工作线程的最小数目和I/O线程的最小数目

if(ThreadPool.SetMinThreads(5, 5))
{
    print("设置成功");
}

获取线程池中工作线程的最小数目和I/O线程的最小数目

ThreadPool.GetMinThreads(out num1, out num2);
print(num1);
print(num2);

将方法排入队列以便执行,当线程池中线程变得可用时执行

ThreadPool.QueueUserWorkItem((obj) =>
{
    print(obj);
    print("开启了一个线程");
}, "123452435345");

Task

Task顾名思义就是任务的意思

Task是在线程池基础上进行的改进,它拥有线程池的优点,同时解决了使用线程池不易控制的弊端

它是基于线程池的优点对线程的封装,可以让我们更方便高效的进行多线程开发

创建无返回值Task的三种方式

//通过new一个Task对象传入委托函数并启动
Task t1 = new Tast(()=>{print("方式1");});
//通过Task中的Run静态方法传入委托函数
Task t2 = Task.Run(() =>{print("方式2");});
//通过Task.Factory中的StartNew静态方法传入委托函数
Task t3 = Task.Factory.StartNew(() =>{print("方式2");});

创建有返回值的Task

Task t1 = new Tast<int>(()=>{return 1;});

获取返回值

//注意:
//Resut获取结果时会阻塞线程即如果task没有执行完成会等待task执行完成获取到Result
t1.Result

同步执行Task

Task t = new Task(()=> {
    Thread.Sleep(1000);
    print("哈哈哈");
});
//t.Start();异步执行
t.RunSynchronously();

Task中线程阻塞的方式

Wait方法:等待任务执行完毕,再执行后面的内容

Task t1 = new Tast(()=>{print("方式1");});
t1.Wait();

WaitAny静态方法:传入任务中任意一个任务结束就继续执行

Task.WaitAny(t1, t2);

WaitAll静态方法:任务列表中所有任务执行结束就继续执行

Task.WaitAll(t1, t2);

Task完成后继续其它Task(任务延续)

//.WhenAll静态方法 + ContinueWith方法:传入任务完毕后再执行某任务
Task.WhenAll(t1, t2).ContinueWith((t) =>
{
    //t1和t2结束后执行
    print("一个新的任务开始了");
});
//写法2
Task.Factory.ContinueWhenAll(new Task[] { t1, t2 }, (t) =>
{
    //t1和t2结束后执行
    print("一个新的任务开始了");
});

WhenAny静态方法 + ContinueWith方法:传入任务只要有一个执行完毕后再执行某任务

Task.WhenAny(t1, t2).ContinueWith((t) =>
{
    //t1或t2结束后执行
    print("一个新的任务开始了");
});
Task.Factory.ContinueWhenAny(new Task[] { t1, t2 }, (t) =>
{
    //t1或t2结束后执行
    print("一个新的任务开始了");
});

取消Task执行

通过加入bool标识 控制线程内死循环的结束

//CancellationTokenSource对象可以达到延迟取消、取消回调等功能
var c = new CancellationTokenSource();
//延迟取消
c.CancelAfter(5000);
//取消回调
c.Token.Register(() =>
{
    print("任务取消了");
});
Task.Run(() =>
{
    int i = 0;
    while (!c.IsCancellationRequested)//默认是false
    {
        print("计时:" + i);
        ++i;
        Thread.Sleep(1000);
    }
});

什么是同步和异步

同步和异步主要用于修饰方法

同步方法:

当一个方法被调用时,调用者需要等待该方法执行完毕后返回才能继续执行

异步方法:

当一个方法被调用时立即返回,并获取一个线程执行该方法内部的逻辑,调用者不用等待该方法执行完毕

什么时候需要异步编程

需要处理的逻辑会严重影响主线程执行的流畅性时

比如:

  1. 复杂逻辑计算时
  2. 网络下载、网络通讯
  3. 资源加载时

异步方法async和await

async和await一般需要配合Task进行使用

async用于修饰函数、lambda表达式、匿名函数

await用于在函数中和async配对使用,主要作用是等待某个逻辑结束

此时逻辑会返回函数外部继续执行,直到等待的内容执行结束后,再继续执行异步函数内部逻辑

在一个async异步函数中可以有多个await等待关键字

注意:Unity中大部分异步方法是不支持异步关键字async和await的,我们只有使用协同程序进行使用

虽然官方 不支持 但是 存在第三方的工具(插件)可以让Unity内部的一些异步加载的方法 支持 异步关键字

https://github.com/svermeulen/Unity3dAsyncAwaitUtil

但是当我们用到.Net 库中提供的一些API时,可以考虑使用异步方法

  1. Web访问:HttpClient
  2. 文件使用:StreamReader、StreamWriter、JsonSerializer、XmlReader、XmlWriter等等
  3. 图像处理:BitmapEncoder、BitmapDecoder
public async void CalcPathAsync()
{
    print("开始处理寻路逻辑");
    int value = 10;
    await Task.Run(() =>//等待这个Task结束
    {
        Thread.Sleep(1000);
        value = 50;
    });
    print("寻路计算完毕 处理逻辑" + value);
}
public async void Timer()
{
    UnityWebRequest q = UnityWebRequest.Get("");
    source = new CancellationTokenSource();
    int i = 0;
    while (!source.IsCancellationRequested)
    {
        print(i);
        await Task.Delay(1000);//等待1秒
        ++i;
    }
}