咖啡丶七

自律给我自由


GraphicRaycaster组件

Graphic Raycaster组件一般是和Canvas挂载在同一个物体下面
管理他下面的所有子UI物体的点击响应方式
在一些交互部件没响应的时候可以看下是不是这部分出问题了

如:

  • 激活该组件时,用户从相机发射的摄像是会被挡住的
  • 关闭该组件时,该Canvas下的UI组件如button,dropdown等都无法使用

注意Graphic Raycaster只对UI下的点击交互起作用,而Physics类里面的api不影响UI上面的交互

Ignore Reversed Graphics

这个属性是用来决定当交互部件水平或者垂直翻转到背面对着屏幕(不一定是180度,只要翻转到背面对着屏幕)的时候,是否忽略背面点击,勾上(翻转到背面不能点击) 取消勾选(不管怎么翻转都能点击)

Blocked Objects

这个属性决定了当有物体遮挡在UI前面,并且点击了遮挡部分的时候,是否应该忽略这次点击,
Three D (3D):遮挡在本UI前的是带有3DCollider的物体,点击遮挡部分,忽略本UI的响应,(点自己没反应)
**Two D(2D):**遮挡在本UI前的是带有2DCollider的物体,点击遮挡部分,忽略本UI的响应,(点自己没反应)

**None:**不忽略本UI的点击,不管有3D/2D的物体挡住,都响应本UI的点击
**All:**都忽略响应,当UI前的遮挡物体是带有任意Collider组件的,点击遮挡部分的时候,都忽略本UI,(点自己没反应)

Blocking Mask

这个属性一般和Blocked Objects参数一起调节起作用,默认是EveryThing
遮挡的物体如果刚好在勾选的层级下面的话,会构成阻挡点击交互的作用


Canvas Group组件

CanvasGroup可以影响该组UI元素的部分性质,而不需要费力的对该组UI下的每个元素进行逐一得得调整。Canvas Group是同时作用于该组件UI下的全部元素。

参数

Alpha : 该组UI元素的透明度。注:每个UI最终的透明度是由此值和自身的alpha数值相乘得到。

Interactable : 是否需要交互(勾选的则是可交互),同时作用于该组全部UI元素。

Blcok Raycasts : 是否可以接收图形射线的检测(勾选则接受检测)。注:不适用于Physics.Raycast.。

Ignore Parent Group : 是否需要忽略父级对象中的CanvasGroup的设置。(勾选则忽略)

应用场景

  • 在窗口的GameObject上添加一个CanvasGroup,通过控制它的Alpha值来淡入淡出整个窗口

  • 通过给父级GameObject上添加一个CanvasGroup并设置它的Interactable值为false来设置一套没有交互(灰色)的控制

  • 通过将元素或元素的一个父级添加Canvas Group并设置BlockRaycasts值为false来制作一个或多个不阻止鼠标事件的UI元素

CanvasGroup的Alpha与SetActive()方法比较:

  • CanvasGroup的Alpha与SetActive()两者之间的性能区别不大

  • CanvasGroup的Alpha由0设为1的时候,并不会让自己活着的子节点中脚本执行Awake()方法,而SetActive(true)则会执行Awake()方法

  • CanvasGroup的Alpha设为0和SetActive(false)的时候,同样不会调用drawcall

小实例(一闪一暗)代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Test : MonoBehaviour {

private float alpha0 = 0.0f;

private float alphaSpeed = 2.0f;
bool isShow;

private CanvasGroup cg;

void Start () {
cg = this.transform.GetComponent<CanvasGroup>();
}

void Update ()
{
if (!isShow)
{
cg.alpha = Mathf.Lerp(cg.alpha, alpha0, alphaSpeed * Time.deltaTime);
if (Mathf.Abs(alpha0 - cg.alpha) <= 0.01)
{
isShow = true;
cg.alpha = alpha0;
alpha0 = 1;
}
}
else
{
//*0.5是因为从隐藏当显示感觉很快
cg.alpha = Mathf.Lerp(cg.alpha, alpha0, alphaSpeed * Time.deltaTime * 0.5f);
if (Mathf.Abs(alpha0 - cg.alpha) <= 0.01)
{
isShow = false;
cg.alpha = alpha0;
alpha0 = 0;
}
}
}

}

判断鼠标是否在UI上

1
2
3
# true:在UI上,false:不在UI上
# 受GraphicRaycaster组件影响,鼠标在未激活GraphicRaycaster的canvas上时返回false
bool isOn = UnityEngine.EventSystems.EventSystem.current.IsPointerOverGameObject()

转载:
unity Graphic Raycaster 作用详解
Unity中CanvasGroup组件

使用

命令 解释
top -d 1 1秒跟新一次
top -p {PID} 指定特定的pid进程号进行观察

命令

在top界面还可以输入命令(需要区分大小写)

命令 解释
d 更改刷新时间间隔
P CPU使用率降序排序
M 内存使用率降序排序
T 进程使用时间降序排序
k 杀进程
m 切换到内存模式
1 查看每个CPU的详细信息

参数含义

第一行

top - 11:48:58 up 3:08, 1 user, load average: 0.40, 0.32, 0.41

内容 含义
11:48:58 当前时间
up 3:08 系统运行时间 格式为时:分
1 user 当前登入用户数
load average: 0.40, 0.32, 0.41 系统负载,即任务队列的平均长度。

load average
三个数值分别为 1分钟、5分钟、15分钟前到现在的平均值
如果这个数除以逻辑CPU的数量,结果高于5的时候就表明系统在超负荷运转了

第二行

Tasks: 453 total, 2 running, 451 sleeping, 0 stopped, 0 zombie

内容 含义
Tasks: 453 total 总进程数
2 running 正在运行的进程数(即正在使用CPU执行任务)
451 sleeping 休眠的进程数(即处于非执行状态,等待被唤醒)
0 stopped 停止的进程数(即处于停止状态)
0 zombie 僵尸进程数
  • sleep休眠:表示当前没有运行但处于等待状态的进程数量。这些进程暂时没有分配到 CPU 时间,它们处于休眠状态,等待某个事件的发生,如等待 I/O 完成、等待信号、或者等待其他资源。休眠状态的进程不会占用 CPU 时间,系统调度器会将 CPU 时间分配给其他需要执行的进程。
  • stopped暂停:表示当前被暂停(停止)的进程数量。这些进程可能是由用户发送 SIGSTOP 信号而暂停的。停止的进程不会占用 CPU 时间。
  • 僵尸进程是已经终止(terminated)但其父进程尚未等待(wait)其终止状态的进程。当一个进程结束时,它的退出状态(exit status)会保留在系统中,直到其父进程通过 wait() 系统调用来获取。如果父进程没有等待子进程的终止状态,那么子进程就会变成僵尸进程。僵尸进程占用了进程表中的资源,因此应该及时被清理,通常是通过父进程调用 wait()waitpid() 来回收。

第三行

%Cpu(s): 7.0 us, 0.2 sy, 0.3 ni, 92.5 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st

内容 含义
7.0us 用户空间(User space)占用CPU百分比,即用户进程占用的CPU时间百分比
0.2sy 内核空间(System space)占用CPU百分比,即内核进程占用的CPU时间百分比
0.3ni 表示调整过优先级的用户态进程的 CPU 使用百分比。ni 表示 “nice” 值,即用户通过 nice 命令调整的进程优先级
92.5 id 空闲CPU百分比
0.0 wa 等待输入输出的CPU时间百分比
0.0 hi 硬中断(Hardware IRQ)占用CPU的百分比
0.0 si 软中断(Software Interrupts)占用CPU的百分比
0.0 st 被虚拟化环境中其他虚拟机”偷走”(Steal)的 CPU 时间百分比

第四、五行

MiB Mem : 16000.5 total, 5664.6 free, 8620.5 used, 1715.4 buff/cache

MiB Swap: 0.0 total, 0.0 free, 0.0 used. 7069.0 avail Mem

内容 含义
MiB Mem
16000.5 total 物理内存总量
5664.6 free 空闲内存总量
8620.5 used 使用的物理内存总量
1715.4 buff/cache 用作内核缓存的内存量
MiB Swap
0.0 total 交换区总量
0.0 free 空闲交换区总量
0.0 used 使用的交换区总量
7069.0 avail Mem 代表可用于进程下一次分配的物理内存数量
617312 cached Mem 缓冲的交换区总量

上述最后提到的缓冲的交换区总量,这里解释一下,所谓缓冲的交换区总量,即内存中的内容被换出到交换区,而后又被换入到内存,但使用过的交换区尚未被覆盖,该数值即为这些内容已存在于内存中的交换区的大小。相应的内存再次被换出时可不必再对交换区写入。

进程信息

列明 含义
PID 进程id
PPID 父进程id
RUSER Real user name
UID 进程所有者的用户id
USER 进程所有者的用户名
GROUP 进程所有者的组名
TTY 启动进程的终端名。不是从终端启动的进程则显示为 ?
PR 优先级
NI nice值。负值表示高优先级,正值表示低优先级
P 最后使用的CPU,仅在多CPU环境下有意义
%CPU 上次更新到现在的CPU时间占用百分比
TIME 进程使用的CPU时间总计,单位秒
TIME+ 进程使用的CPU时间总计,单位1/100秒
%MEM 进程使用的物理内存百分比
VIRT 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES
SWAP 进程使用的虚拟内存中,被换出的大小,单位kb
RES 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA
CODE 可执行代码占用的物理内存大小,单位kb
DATA 可执行代码以外的部分(数据段+栈)占用的物理内存大小,单位kb
SHR 共享内存大小,单位kb
nFLT 页面错误次数
nDRT 最后一次写入到现在,被修改过的页面数
S 进程状态。D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程
COMMAND 命令名/命令行
WCHAN 若该进程在睡眠,则显示睡眠中的系统函数名
Flags 任务标志

前言

配置环境变量的位置在/etc/profile文件中(系统级别的配置文件)
~/.bashrc是用户级别的配置文件

在修改/etc/profile之后需要执行source /etc/profile命令使修改内容生效
~/.bashrc是该用户每次登入时会自动运行。以root用户进入时会执行/root/.bashrc配置文件,以steam用户进入会执行/home/steam/.bashrc配置文件

两个文件可以实现完美的搭配:

  1. 修改/etc/profile配置文件实现自己的需求,如export PS1='[\t \u@\h \w]\$ '
  2. ~/.bashrc文件末尾添加source /etc/profile

这样在每次用户进入后都会执行一遍source /etc/profile命令,使/etc/profile配置文件生效

创建快捷方式

/usr/bin/路径下创建一个pbulic快捷方式,使其指向/home/hexo/public

1
ln -sf /home/hexo/public /usr/bin/public
  • ln:创建链接命令
  • -s:表示创建软链接
  • -f:表示在目标文件存在时强制删除并重新创建

public可以是文件夹,也可以是文本文件,也可以是执行文件(linux一个路径下不允许同时存在同名文件,同名的文件夹和文本文件也不行,所以不用担心索引错)
不管是什么,在访问/usr/bin/public时等同于访问/home/hexo/public

  • 文件夹:cd /usr/bin/public等同于cd /home/hexo/public
  • 文本文件:vim /usr/bin/public等同于vim /home/hexo/public
  • 执行文件:/usr/bin/public等同于/home/hexo/public

执行文件最好放在/usr/bin/路径下,该路径下的执行文件是全局可用的
如,使用ln -s /usr/local/nginx/sbin/nginx /usr/bin/nginx创建了nginx的快捷方式,则在任意路径下执行nginx都等同于执行/usr/local/nginx/sbin/nginx

使用systemctl让服务器在后台运行

需要在/etc/systemd/system/路径下创建后缀为.service服务器配置文件,如vim /etc/systemd/system/pal.service

文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[Unit]
# 服务器简介
Descripteion=pal server
# 前置service,都在/etc/systemd/system/下,network.target是连接上网络
After=network.target XXX.service

[Service]
# 指定运行的sh
ExecStart=/path/to/start/PalServer.sh
# 停止和重新加载服务时执行的命令
ExecStop=/path/to/stop/command
ExecReload=/path/to/reload/command
# 指定服务器运行的用户和组
User=yourusername
Group=yourgroupname
# 定义服务器在退出后是否自动重启
Restart=always

# 默认格式
[Install]
WantedBy=multi-user.target
systemctl常用命令 含义
sudo systemctl daemon-reload 重载服务,每次修改后需要执行
sudo systemctl start pal 启动服务,可不用后缀直接使用pal
sudo systemctl stop pal 停止服务
sudo systemctl restart pal 重启服务
sudo systemctl status pal 查看状态
sudo systemctl enable pal 设置开机启动
sudo systemctl disenable pal 关闭开机启动
sudo journalctl -u pal 查看服务日志
sudo journalctl -f 持续输出所有后台服务器日志

配置shell命令提示符

PS1代表提示符的变量,可以在shell中输入echo $PS1查看当前PS1的值

个人喜欢的一个配置export PS1='[\t \u@\h \w]\$ '

缩写 含义
\d 代表日期,格式为weekday month date
\H 完整的主机名称
\h 仅主机的第一个名字
\t 显示24小时格式的时间,HHMMSS
\T 显示12小时格式的时间
\A 显示24小时格式的时间,HHMM
\u 当前账户的账号名称
\v BASH的版本信息
\w 完整的工作目录名称,家目录会以~显示
\W 利用basename去的工作目录名称,所以只会列出最后一个目录
# 下达的第几个命令
$ 提示字符,如果是root,提示符为#,普通用户则为$


在unity3D中经常用线性插值函数Lerp()来在两者之间插值,两者之间可以是两个材质之间、两个向量之间、两个浮点数之间、两个颜色之间。

为了更好的理解线性插值的概念,我们先讨论一下浮点数的线性插值

浮点数的线性插值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void Start()
{
Debug.Log(Mathf.Lerp(0, 100, 0).ToString());
Debug.Log(Mathf.Lerp(0, 100, 0.1f).ToString());
Debug.Log(Mathf.Lerp(0, 100, 0.2f).ToString());
Debug.Log(Mathf.Lerp(0, 100, 0.3f).ToString());
Debug.Log(Mathf.Lerp(0, 100, 0.4f).ToString());
Debug.Log(Mathf.Lerp(0, 100, 0.5f).ToString());
Debug.Log(Mathf.Lerp(0, 100, 0.6f).ToString());
Debug.Log(Mathf.Lerp(0, 100, 0.7f).ToString());
Debug.Log(Mathf.Lerp(0, 100, 0.8f).ToString());
Debug.Log(Mathf.Lerp(0, 100, 0.9f).ToString());
Debug.Log(Mathf.Lerp(0, 100, 1).ToString());
}

// 结果为
// 0
// 10
// 20
// ....
// 90
// 100

可以看出输出的结果最终取决于第三个参数
如果将第一个参数改为100,第二个参数改为110,第三个参数不变,那么结果为100,101,102,103,104,105,106,107,108,109,110。

向量的线性插值

1
2
3
4
void Update()
{
transform.position = Vector3.Lerp(A, B, Time.time);
}

该物体会慢慢的移动到从A移动到B

现有两个点a, b

假设Y值都相等

  • a到b的向量:var direction = a - b;
  • 以a为中心,世界坐标为方向,b在a的什么弧度上:var angleRag = math.atan2(direction.z, direction.x);
  • 什么角度上:var angleDeg = math.degrees(angleRag)

向量之间的计算

点乘(内积)运算

a 与 b 的点乘(也称内积)公式如下

$$
\vec{a}\vec{b}=|\vec{a}||\vec{b}|cosθ (0°<θ<180°)
$$
若a=(ax, ay, az),b=(bx, by, bz),则
$$
\vec{a}\vec{b}=a_xb_x+a_y
b_y+a_z*b_z
$$
在unity中,提供了Vector3.Dot()方法计算两个向量的点乘

1
2
3
4
5
6
7
8
9
10

Vector3 vectorA = new Vector3(1, 2, 3);
Vector3 vectorB = new Vector3(4, 5, 6);

float dotProduct = Vector3.Dot(vectorA, vectorB);

// 结果输出: 32,32>0,即向量a,b的夹角为锐角
// dotProduct > 0,为锐角
// dotProduct = 0, 为直角
// dotProduct < 0, 为钝角

点乘的结果是|a||b|cosθ

  • 如果只用判断两个向量夹角的关系,则只用判断dotProduct的值就好了
  • 如果需要得到cosθ,需要除以|a||b|

叉乘(外积)运算

a 与 b 的叉乘(也称外积、向量积、叉积)公式如下
$$
|\vec{c}|=|\vec{a} \times \vec{b}|=|\vec{a}|*|\vec{b}|*sinθ
$$

若a=(a_x, a_y, a_z),b=(b_x, b_y, b_z),i,j,k分别为x,y,z轴的单位向量,则
$$
\vec{a}×\vec{b}=(a_yb_z-a_zb_y)\vec{i}+(a_zb_x-a_xb_z)\vec{j}+(a_xb_y-a_yb_x)\vec{k}
$$
在unity中,提供了Vector3.Cross()方法计算两个向量的叉乘

1
2
3
4
5
6
Vector3 vectorA = new Vector3(1, 2, 3);
Vector3 vectorB = new Vector3(4, 5, 6);

Vector3 crossProduct = Vector3.Cross(vectorA, vectorB);

// 结果输出: (3, -6, 3)

叉乘的结果是是一个新的向量c,c垂直与a和b所在平面,且方向由右手定则确定(当右手的四指从a以不超过180度的转角转向b时,竖起的大拇指指向是c的方向)。但Unity是左手坐标系,所以这里应该是左手定则

1
2
// right是(1, 0, 0)    forward是(0, 0, 1)
Vector3.Cross(Vector3.right, Vector3.forward) = (0, -1, 0)

数乘运算

数乘是将向量的每个坐标值乘以该数值
$$
k \vec{v} = (k \times v_x, k \times v_y, k \times v_z)
$$


向量的概念

向量的模

返回向量的长度

1
float length = vector.magnitude;

向量的平方长度

返回向量的平方长度,通常用于比较向量大小而无需进行开方运算,从而提高效率

1
float sqrMagnitude = vector.sqrMagnitude;

单位向量

返回向量的单位向量,即长度为1但方向相同的向量

1
Vector3 normalizedVector = vector.normalized;

线性插值

在两个向量之间进行线性插值

1
Vector3 interpolatedVector = Vector3.Lerp(startVector, endVector, t);

两点之间的距离

返回两点之间的距离

1
float distance = Vector3.Distance(vectorA, vectorB);

向量之间的夹角

返回两个向量之间的夹角

1
float angle = Vector3.Angle(vectorA, vectorB);

投影向量

返回向量在另一个向量上的投影向量,一般是计算在坐标轴上的投影向量

1
Vector3 projectionVector = Vector3.Project(vectorToProject, ontoVector);

示例:

1
2
Vector3 B = new Vector3(2, 2, 2);
Vector3 result = Vector3.Project(B, Vector3.forward); // 结果为 OP(0,0,2)

上图中是边长为2的正方形,OP是B在Z轴上的投影
B是B减去OP的向量,B(2,2,0) (向量本身减去此投影向量就为在平面上的向量)

反射向量

返回在另一个向量上的反射向量

1
Vector3 reflectionVector = Vector3.Reflect(incidentVector, normal);

浅拷贝和深拷贝的区别

  • 浅拷贝只是拷贝了原对象的引用,而不是对象本身。因此,浅拷贝后的对象与原对象共享内存空间,即修改其中一个对象的值会影响到另一个对象的值
  • 深拷贝则是完全拷贝了原对象及其引用的对象,因此,深拷贝后的对象与原对象不共享内存空间,即修改其中一个对象的值不会影响到另一个对象的值

在C#中深拷贝的用法

反射

输入的对象可以和输出的对象不一样。如果输出的对象拥有与输入的对象相同的属性,则会把该属性的值赋给输出对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static TOut TransReflection<TIn, TOut>(TIn tIn)
{
// 创建一个新的 TOut 实例
TOut tOut = Activator.CreateInstance<TOut>();
// 获取到 tIn 的类型
var tInType = tIn.GetType();
// 遍历 TOut 的属性
foreach (var itemOut in tOut.GetType().GetProperties())
{
// 获取到 tIn 的属性
var itemIn = tInType.GetProperty(itemOut.Name);

if (itemIn != null)
{
// 将 tIn 的属性赋给 tOut 的对应属性
itemOut.SetValue(tOut, itemIn.GetValue(tIn));
}
}
return tOut;
}

第二种反射反射深拷贝函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static T DeepCopy<T>(T obj)
{
if (obj == null)
{
return obj;
}
var type = obj.GetType();
if (obj is string || type.IsValueType)
{
return obj;
}

var result = Activator.CreateInstance(type);
var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
foreach (var field in fields)
{
field.SetValue(result, field.GetValue(obj));
}
return (T)result;
}

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using System;

namespace AsyncTest
{
class Program
{
static void Main(string[] args)
{
Player A = new Player("A", 10);
//Player B = A; // 浅拷贝
Player B = TransReflection<Player, Player>(A); // 深拷贝
B.Age = 20;
Console.WriteLine($"{A.Name} {A.Age}");
Console.WriteLine($"{B.Name} {B.Age}");
Console.ReadKey();
}
private static TOut TransReflection<TIn, TOut>(TIn tIn)
{
// 创建一个新的 TOut 实例
TOut tOut = Activator.CreateInstance<TOut>();
// 获取到 tIn 的类型
var tInType = tIn.GetType();
// 遍历 TOut 的属性
foreach (var itemOut in tOut.GetType().GetProperties())
{
// 获取到 tIn 的属性
var itemIn = tInType.GetProperty(itemOut.Name);

if (itemIn != null)
{
// 将 tIn 的属性赋给 tOut 的对应属性
itemOut.SetValue(tOut, itemIn.GetValue(tIn));
}
}
return tOut;
}
}
public class Player
{
public string Name { get; set; }
public int Age { get; set; }
public Player() { }
public Player(string name, int age)
{
this.Name = name;
this.Age = age;
}
}
}

静态转换(Static Cast)

静态转换是将一种数据类型的值强制转换为另一种数据类型的值。

静态转换通常用于比较类型相似的对象之间的转换,例如将 int 类型转换为 float 类型。

静态转换不进行任何运行时类型检查,因此可能会导致运行时错误。

1
2
int i = 10;
float f = static_cast<float>(i); // 静态将int类型转换为float类型

动态转换(Dynamic Cast)

动态转换通常用于将一个基类指针或引用转换为派生类指针或引用。动态转换在运行时进行类型检查,如果不能进行转换则返回空指针或引发异常。

1
2
3
4
class Base {};
class Derived : public Base {};
Base* ptr_base = new Derived;
Derived* ptr_derived = dynamic_cast<Derived*>(ptr_base); // 将基类指针转换为派生类指针

常量转换(Const Cast)

常量转换用于将 const 类型的对象转换为非 const 类型的对象。

常量转换只能用于转换掉 const 属性,不能改变对象的类型。

1
2
const int i = 10;
int& r = const_cast<int&>(i); // 常量转换,将const int转换为int

拓展:const定义的常量表示只读,不能更改。


重新解释转换(Reinterpret Cast)

重新解释转换将一个数据类型的值重新解释为另一个数据类型的值,通常用于在不同的数据类型之间进行转换。

重新解释转换不进行任何类型检查,因此可能会导致未定义的行为。

1
2
int i = 10;
float f = reinterpret_cast<float&>(i); // 重新解释将int类型转换为float类型

Task和Thread的区别

  • 基于不同的 .NET 框架:Thread 是基于 Windows 操作系统提供的 API 实现,而 Task 则是基于 .NET框架提供的 TPL(Task Parallel Library)实现。

  • 默认执行线程池:Thread 默认使用前台线程,而 Task 默认使用后台线程。这意味着,Thread 会阻塞主线程,而 Task不会。

  • 异步执行:Task 支持异步执行,而 Thread 不支持。这意味着,在使用 Task 时,可以通过 await 和 async关键字轻松实现异步编程,而 Thread 则需要手动管理线程的启动和等待。

  • 异常处理:Task 提供了更好的异常处理机制,可以将异常传递给调用方,而 Thread 则需要在每个线程中处理异常。

  • 任务调度器:Task 提供了任务调度器(TaskScheduler),可以控制任务的并发性和调度方式,而 Thread 则没有这个功能。

  • 返回值:Task 可以有返回值,而 Thread 没有。这是因为 Task 是基于 TPL 实现的,可以利用 .NET框架提供的并发编程模型来实现任务之间的依赖和调度。

Task

TaskTask<T>的创建

Task的创建

使用Task的构造函数

1
2
3
Task task = new Task(() => {
// 异步操作的代码
});

使用Task.Run的构造函数

1
2
3
Task task = Task.Run(() => {
// 异步操作的代码
});
Task<T>的创建

Task<T>与会返回一个类型为T的结果

使用Task的构造函数

1
2
3
4
Task<int> task = new Task<int>(() =>{
// 异步操作的代码,返回int类型的结果
return 42;
});

使用Task.Run的构造函数

1
2
3
4
Task<int> task = Task.Run(() => {
// 异步操作的代码,返回int类型的结果
return 7;
});

启动和等待TaskTask<T>

启动

使用start方法启动

1
task.Start();
使用Wait()阻塞等待

直接控制Task,实现异步等待任务的完成
会阻塞主线程,类似于thread1.Join()

1
2
3
4
5
task.Wait();	// 阻塞当前线程,等待任务完成
int result = task.Result; // 阻塞当前线程,等待任务完成,并获取结果

Task.WaitAll(new Task[]{ task1, task2 }); // 等待所有的task都执行完成再解除阻塞
Task.WaitAny(new Task[]{ task1, task2 }); // 只要有一个task执行完毕就解除阻塞
使用WhenAllWhenAny控制线程

不会阻塞主线程

1
2
Task.WhenAll(task1, task2).ContinueWith((t) => { Console.Writeline("执行异步代码"); });	// 当task1和task2执行完毕后,再执行后续代码
Task.WhenAny(task1, task2).ContinueWith((t) => { Console.Writeline("执行异步代码"); }); // 只要有一个执行完毕,就执行后续代码
使用await等待TaskTask<T>

在异步代码中使用await等待其他的任务完成(为.net5.0推出的方法)
不会阻塞主线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System;
using System.Threading.Tasks;

namespace AsyncTest
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("主线程开始");

t().Start();

Console.WriteLine("主线程结束");

Console.ReadKey();
}

private static Task t()
{
return new Task(async () =>
{
Console.WriteLine("开始执行t");
await Task.Delay(2000); // 等待两秒,模拟一个异步操作
Console.WriteLine("结束执行t");
});
}
}
}

运行结果

1
2
3
4
主线程开始
主线程结束
开始执行t
结束执行t
三个等待的区别
  • Wait()针对线程操作,会阻塞主线程
  • WhenAll针对线程操作,不会阻塞主线程
  • await在线程中针对其他线程操作,不会阻塞主线程

Thread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System.Threading;

void start()
{
Thread t1 = new Thread(A);
Thread t2 = new Thread(B);
t1.start();
t2.start();
}

void A()
{
Debug.Log("我是A函数1");
Debug.Log("我是A函数2");
}

void B()
{
Debug.Log("我是B函数1");
Debug.Log("我是B函数2");
}

此时的输出结果是不可控的,可能先执行A,也可能先执行B,这个是操作系统根据CPU自动计算出来的。
而且A和B是会嵌套交叉执行的

如何让程序先执行A,执行完A之后在执行B;或者先执行完B再执行A:使用lock关键字

lock关键字

可以通过lock关键字来控制A和B的执行顺序。使用同一个lock参数的代码,程序会等待前面的代码执行完之后再执行后面的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System.Threading;

void start()
{
static Object o = new object();
Thread t1 = new Thread(A);
Thread t2 = new Thread(B);
t1.start();
t2.start();
Thread t3 = new Thread(C);
t3.start();
}

static void A()
{
lock(o)
{
Debug.Log("我是A函数1");
Debug.Log("我是A函数2");
}
}

static void B()
{
lock(o)
{
// Thread.Sleep(1000); // 暂停1秒
Debug.Log("我是B函数1");
Debug.Log("我是B函数2");
}
}

static void C()
{
DEbug.Log("我是随机函数");
}

此时可能会先执行A,执行完A后再执行B;也有可能先执行B,执行完B之后再执行A。C函数没有被锁住,所以他能出现在任意位置。

补充:这里的o是Object类(基类)。所以,lock的参数可以是任意的类

拓展

在unity中将子线程的代码转移到主线程中执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using UnityEngine;
using System.Collections.Generic;
using System;

public class MainThreadDispatcher : MonoBehaviour
{
private static MainThreadDispatcher instance;

private Queue<Action> actionQueue = new Queue<Action>(); // 初始化一个队列:先进先出的一个数据结构

private void Awake()
{
if (instance == null)
{
instance = this;
}
else
{
Destroy(gameObject);
}
}

private void Update()
{
lock (actionQueue)
{
while (actionQueue.Count > 0)
{
Action action = actionQueue.Dequeue(); // 取出队列中的一个函数,并执行
action.Invoke();
}
}
}

public static void RunOnMainThread(Action action)
{
lock (instance.actionQueue) // 如果有其他的代码(包括自己)使用了lock(instance.actionQueue),则会等待前面的执行完再执行自己
{
instance.actionQueue.Enqueue(action); // 将传进来的action函数插入到队列中
}
}
}

在其他函数中可以通过调用RunOnMainThread()函数将方法转移到主线程上执行
常用与数据请求上,接收到的数据一般都是在子线程上。但是在unity的子线程中无法访问transform属性等,故需要转移到主线程上执行

1
2
3
4
5
RunOnMainThread(() =>
{
// 转移到主线程上执行代码
textValue.text = "你好";
});