2023年3月

最近状态不好,所以这几天基本没干什么,就分享一下和AI绘画有关的东西吧。
此前我都没有抱着一种教学的心态来写博客,因为我所掌握的东西实在太过简单,只要一说大家就会了,我害怕我在人群里失去自己的特征,但不得不承认,这是一种很丑陋的心态,重要的是我否定了自己所学的东西,如果不愿承认现在的软弱,我就始终看不清我走到了一个什么样的位置上。
学过3D建模的应该都知道,3D建模软件里面都有一套类似于这种的东西:

上面我所展示的,就是今天介绍的重点:AI绘画的工作流,不知道的兄弟也没关系,这东西用起来很简单。
但在介绍怎么使用它之前,我先介绍一下它相比于原来的工作优点。
各位请看,这是我原来的UI:

原来的UI用起来很简单,输入正面提示词和负面提示词后点击生成之后,就会生成图片,这也是一开始就有的生成方式。
但点击生成之后,电脑的内部并不像我们使用起来那么简单:
在加载了这个UI之后,电脑会首先读取你原先设定好的模型,就是我们左上角看见的anything,并且这个模型类型被限定,随着
某个日本大神发明的简易训练模型lora
诞生,越来越多的私人模型诞生,但左上角这个我试了一下,好像没办法读入私人模型,也可能是因为我的电脑没有N卡的问题,而能否使用私人模型,对于流水线式的生产图片很关键
在你点击生成之后,电脑会通过你设置的采样器进行采样,采样生成出来的图片会再次经过vae的处理,如果你的图片生成出来偏灰,可以加载一下vae试试。
这样的生产方式面临两个问题:
1.有些东西你很难用文字表达清楚,比如坐着,脚放在前面。Ai并不知道坐着是什么意思,也不知道脚放在前面是什么意思,但是它会模仿模型里它已经学习过的坐着,脚放在前面的图片来生成一个差不多的给你,问题来了,你怎么知道AI模型里它模仿的图片是什么样的?和你不一样的时候要怎么办,万一这个动作它根本没学过呢?
为了解决的这个问题,
斯坦福的某个天才发明了controlnet
,它可以通过不同的预处理方式,来控制图片的构成,这里贴上链接,想要细看的看官可以去看一下:
https://github.com/Mikubill/sd-webui-controlnet
不想点链接的也没事,我会放出图片:
这是一张用3D建模做的图片:

利用canny预处理,这张图片可以变成这样:

在这个插件面世之前,AI生成图片从来都是随机的,比如这两张:


如果不是我人为的控制了它,这个AI可以放纵得更离谱。
通过上面两个例子的对比各位可以发现,controlnet展现了惊人的图片控制能力,而不需要人为的控制AI,限制AI发挥的强度,更厉害的是controlnet不止有canny预处理这样将图像退化为线稿再处理的预处理方式,它也可以只控制人物的姿势,而不控制图像的其他任何部分,只保留产品和建筑物的结构,对产品和建筑物进行重新装修也不再话下,配合文字,可以变成任何你想要的图片。
说了这么多,其实我想说,控制AI一直是AI绘画爱好者的心头病。
也是面临的第一个问题:AI生成出来的东西不可控,单单controlnet的发明,在AI绘画圈内就引起了巨大的轰动,但这终究只能控制图像的冰山一角,想要将AI的生产力解放出来,这还不够。
举一个很恰当的例子:我要开一个方便面厂,我生产红烧牛肉面的时候你不能给我香辣牛肉面,不然我还要人为地在生产完成后给你分出来,浪费时间浪费金钱,而且我不能只有一条流水线,我还得有另一条流水线生产香辣牛肉面,不能两种面混合着生产,不然你就要人为地调整流水线的生产,费时费力。
但是很不巧的是,这个绘画AI一开始的UI把这两个缺点全占了,我们先不从动画说起,就以游戏业内最简单的图片CG说起:
一个正常的游戏CG的人物表情是有好几种变化的,其背后的景物是不能变,为了解决这个问题,我需要让AI做到景物不变人变,但其实这很难做到。
可能有一些知道AI绘画的人会说利用图生图降低噪声强度,减少修改的范围,只修改你要修改的部分就行了,(或者使用蒙版,意思是一样的)
在这里我可以和你说,在关于这个方法的视频发出来的几个月前我就试出来了这种方法,我可能也是最先放弃它的人。

这是我当时的记录,时间去年11月,这个方法第一个视频我没记错的话是1月才看见有第一个人发。
我不是说我是第一个发现它的人,而是想说,在评价这个方法的缺点的时候,我应该比你更有发言权。
这个方法属于治标不治本,本质上你还是在让AI随机抽卡,而且你修改的范围再大一点你就会发现,我靠,两个部分的色块完全不一样啊。(甚至有可能人都不一样,如果你是个小白的话)
或许也有人会说,如果AI不能生成只变化图片的部分的话,那我多生成几张CG,多良心啊。
大哥,cg的动作,场景细节,光照角度,视角都是要设计的,绘画本身是一门艺术,应该要告诉玩家某种感觉,只是排放一堆图片给观众的话,观众容易审美疲劳,谁还会注意你的音乐,图片的细节啊。像这种东西建议称为AI垃圾,和那种口水歌差不多。
这样的话就更容易出现一个问题了,如果我连场景细节,光照角度都要管,那对AI的控制不是更难了吗?
何止,你不仅要管控制,你还要管AI绘画的第二个问题:崩坏:
相信大家都或多或少听说过AI画手的问题吧?
为了避免有些人要睡着了,我先炫一下技:
这是别人的AI画手:

这是我的AI画手:

(可能有人感觉我是用momoke模型控制的,我可以保证说不是,不信你可以拿我的图片放进你的AI里读取信息)
在对SDUI的使用方面来说,我可以说我比得过一半的人,即便如此,我依然放弃了它选择了ComfyUI,为什么?
AI绘画会出现这两个问题,原因很简单,因为这个画不是我们画的,而是AI画的,画的过程我们是控制不了的,我们能做的只有点一下生成。
是的,大家应该猜到问题出在哪里了————点一下生成。
我上面说SDUI把两个缺点全占了:这里我们重点说说是哪两个缺点:
1.我们只能输入一次,虽然SDUI支持不断地生成,但除非你人为地去改,否则输入的内容都是一样的,AI根据我们的提示输出的也不会差太多,就好像我们只有一条流水线,只能生产红烧牛肉面一样,但ComfyUI可以有多条流水线。
2.SDUI里面人物和环境是一起预处理的,哪怕是controlnet也一样,这种感觉就好像你生产的一包红烧牛肉面混进了香辣牛肉面的酱包,但ComfyUI可以分开处理:
这是原图:

接着我跟我的AI强调了不需要背景,背景接着白色就可以了,但这是效果:

我连手都能控制,却不能控制背景,因为SDUI并没有给我分开处理的机会,但ComfyUI给了我这个机会,刚才的测试也成功了。

我明白要说明ComfyUI的优点应该也说明一下它的工作方式,可是已经快四千字了(好累),我就简短的说明一下,详细的下次再说或者这里有:
https://github.com/comfyanonymous/ComfyUI
相比于SDUI,ComfyUI的生成方式并不是点一下鼠标就好了,你需要用节点的方式输入模型,插件(如果你要使用的话),正面和负面的提示词,图片格式,vae,然后用线把它们连接起来,就像这样:

在这个UI中,你每一次的生成并不只是生成一张图片,而是每一个应该输出图片的节点都会输出图片,对于同样一份输入数据,你可以用线把它连到不同的节点,用不同的插件和强度处理,然后生成图片输入下一个节点或者就这样输出,它就像有好几条流水线在同时工作,并且你可以通过许多模型控制一张图片,这在ComfyUI里面时允许,前面提到了lora,你可以使用多个自
己训练的小模型来同时控制图片,增强你对AI的控制。
综上,ComfyUI相比于SDUI,它可以使用多个流水线工作,用不同的处理方式处理多个不同的图片,输出不同的图片,如果工作量很大,你只需要调整后节点然后出去玩一整天,而不用像SDUI一样坐在电脑前这张跑完了,换个输入数据换个处理方式再跑另一张。
同时,它也支持多个模型和插件(包括controlnet),输入数据同时控制一张图片,满足你对视角,动作,场景细节的要求,甚至得益于它的高效率和搞控制能力,许多AI动画爱好者狂喜,尽管这些人数量不多,因为要做到这些,需要和我差不多的对AI的控制能力,基本没人会研究的。(所以在ComfyUI面世时,很多大佬都在狂喜,但却有很多声音觉得AI退化了,不会画画,又不去学,只会让AI画,又懒得钻研........)
虽然cComfyUI很厉害,但它其实只是把AI工作的过程透明化了,它的本质还是SDAI,我也一直很感谢SDai。

1. 高斯过程

高斯过程(Gaussian Process)是一种假设训练数据来自无限空间且各特征都符合高斯分布(高斯分布又称“正态分布”)的有监督学习。

高斯过程是一种概率模型,在回归或分类预测都以高斯分布标准差的方式给出预测置信区间估计。

随机过程

高斯过程应用于机器学习已有数十年历史,,它来源于数学中的随机过程(Stochastic Process)理论。随机过程是研究一组无限个随机变量内在规律的学科。

如果把每次采样的目标值
\(y\)
都看成一个随机变量,那么单条采样就是一个随机分布事件的结果,
\(N\)
条数据就是多个随机分布采样的结果,而整个被学习空间就是无数个随机变量构成的
随机过程
了。

Q: 为何将采样看成随机变量?

A: 一、所有数据的产生就是随机的;二、数据的采集有噪声存在。因此不可能给出精确值的预测,更合理的是给出一个置信区间。

无限维高斯分布

高斯分布或者说正态分布的特点:

  • 可标准化:一个高斯分布可由均值
    \(\mu\)
    和标准差
    \(\sigma\)
    唯一确定,用符号
    \(\sim N(\mu,\sigma)\)
    表示。并且任意高斯分布可以转化用
    \(\mu=0\)

    \(\sigma=1\)
    标准正态分布表达。
  • 方便统计:高斯分布约67.27%的样本落在
    \((\mu-\sigma,\mu+\sigma)\)
    ,约95%的样本落在
    \((\mu-2\sigma,\mu+2\sigma)\)
    ,约99%的样本落在
    \((\mu-3\sigma,\mu+3\sigma)\)
  • 多元高斯分布(Multivariate Gaussian):
    \(n\)
    元高斯分布描述
    \(n\)
    个随机变量的联合概率分布,由均值向量
    \(<\mu_1,\mu_2,\cdots\mu_n>\)
    和协方差矩阵
    \(\sum\)
    唯一确定。其中
    \(\sum\)
    是一个
    \(n\times n\)
    的矩阵,每个矩阵元素描述
    \(n\)
    个随机变量两两间的协方差。

协方差(Covariance)用于衡量两个变量的总体误差。度量各个维度偏离其均值的程度。协方差的值如果为正值,则说明两者是正相关的(从协方差可以引出“相关系数”的定义),结果为负值就说明负相关的,如果为0,也是就是统计上说的“相互独立”。方差就是协方差的一种特殊形式,当两个变量相同时,协方差就是方差了。协方差公式如下:

\[Cov(X,Y)=E[(X-\mu_{x})(Y-\mu_{y})]
\]

\(E\)
为期望;
\(X\)

\(Y\)
为两个变量;
\(\mu_x\)

\(\mu_y\)
分别代表
\(X\)

\(Y\)
均值。

期望值不等于平均值。期望值是衡量一个随机变量的中心趋势的加权平均数,是计算该变量的所有可能值,其中权重是每个值发生的概率。均值是一种特定类型的期望值,计算方法为变量的所有值除以值的总数之和。因此,虽然平均值是计算期望值的一种方法,但它不是唯一的方法,而且这两个术语是不可互换的。

  • 和与差:设有任意两个独立的高斯分布
    \(U\)

    \(V\)
    ,那么它们的和
    \(U+V\)
    一定是高斯分布,它们的差
    \(U-V\)
    也一定是高斯分布。
  • 部分与整体:多分高斯分布的条件分布任然是多元高斯分布,也可理解为多元高斯分布的子集也是多元高斯分布。

上文说过:高斯过程可被看成无限维的多元高斯分布,那么机器学习的训练过程目标就是学习该无限维高斯分布的子集,也就是多元高斯分布的参数:均值向量
\(<\mu_1,\mu_2,\cdots\mu_n>\)
和协方差矩阵
\(\sum\)

协方差矩阵的元素表征两两元素之间的协方差,如果用核函数计算两者,便使得多元高斯分布也具有表征高维空间样本之间关系的能力。此时协方差矩阵可表示为:

\[\sum=K_{XX}=
\begin{bmatrix}
k(x_1,x_2)&\cdots& k(x_1,x_N)\\
\vdots&\cdots&\vdots\\
k(x_N,x_1)&\cdots&k(x_N,x_N)
\end{bmatrix}
\]

其中
\(K_{XX}\)
表示样本数据特征集
\(X\)
的核函数矩阵;
\(k()\)
表示所选核函数;
\(x_1,x_2,\cdots x_n\)
等是单个样本特征向量。同
\(SVM\)
一样,此处核函数需要指定形式,常用的包括:径向基核、多项式核、线性核等。在训练过程中可以定义算法自动寻找核的最佳超参数。

设样本目标值
\(Y\)
,被预测的变量
\(Y_*\)
,由高斯分布的特型可知,由训练数据与被预测数据组成的随机变量集合仍然符合多元高斯分布,即:

\[\begin{pmatrix}
Y\\
Y_*
\end{pmatrix}\sim N
\begin{pmatrix}
\begin{pmatrix}
u\\
u_*
\end{pmatrix},
\begin{pmatrix}
K_{XX}&K_{X_*X}\\
K_{XX_*}&K_{X_*X_*}
\end{pmatrix}
\end{pmatrix}
\]

其中
\(u_*\)
是代求变量
\(Y_*\)
的均值,
\(K_{X_*X}\)
是样本数据与预测数据特征的协方差矩阵,
\(K_{X_*X_*}\)
是预测数据特征的协方差矩阵。

由完美多元高斯特型可知
\(Y_*\)
满足高斯分布
\(N(u_*,\sum)\)
,可直接用
公式求得
该分布的超参数,即预测值的期望值和方差:

\[\begin{cases}
u_*=K^T_{X_*X}K^{-1}Y \\
\sum=K_{X_*X_*}-K^T_{X_*X}K^{-1}K_{XX}
\end{cases}
\]

与其他机器学习模型不同的是:高斯过程在预测中仍然需要原始训练数据,这导致该方法在高维特征和超多训练样本的场景下显得运算效率低,但因此高斯过程才能提供其他模型不具备的
基于概率分布的预测

对于白噪声的处理,就是在计算训练数据协方差矩阵
\(K_{XX}\)
的对角元素上增加噪声分量。因此协方差矩阵变为如下形似:

\[\sum=K_{XX}=\begin{bmatrix}
k(x_1,x_1)&\cdots&k(x_1,x_N)\\
\vdots&&\vdots \\
k(x_N,x_1)&\cdots&k(x_N,x_N)
\end{bmatrix}=\alpha
\begin{pmatrix}
1&\cdots&0\\
\vdots&&\vdots\\
0&\cdots&1
\end{pmatrix}
\]

其中,
\(\alpha\)
是模型训练者需要定义的噪声估计参数。该值越大,模型抗噪声能力越强,但容易产生拟合不足。

在机器学习中,噪声是指真实标记与数据集中的实际标记间的偏差1,也就是数据本身的不确定性或随机性。白噪声是一种特殊的噪声,它具有以下特点:

  • 白噪声是独立同分布的,也就是说每个样本的噪声都是相互独立且服从同一分布的。
  • 白噪声的均值为零,也就是说每个样本的噪声都不会对真实标记产生系统性的偏移。
  • 白噪声的方差为常数,也就是说每个样本的噪声都具有相同的波动程度。
  • 白噪声和其他类型的噪声相比,更容易处理和分析,因为它不会引入额外的复杂性或相关性。

Python中使用高斯模型


sklearn.gaussian_process.kernels
中以类的方式提供了若干核函数,常用的如下表:

核函数 描述
ConstantKernels 常数核,对所有特征向量返回相同的值,即模型忽略了特征数据信息。
DotProduct 点积核,返回特征向量点积,也就是线性核。
RBF 径向基核,把特征向量提升到无限维以解决非线性问题。

此外,使用如下类,还允许不同核之间进行组合

组合核 描述
Sum(k1,k2) 用两个核分别计算后将模型相加
Product(k1,k2) 两个核分别运算后,结果相乘
Exponentiation(k,exponent) 返回核函数结果的指数运算结果,即
\(k^{exponent}\)

GaussianProcessRegressor

GaussianProcessClassifier
分别表示python中的高斯过程回归模型和高斯过程分类模型。

与其他模型不同的是:它们的预测函数
predict()
有两个返回值,第一个为
预测期望值
,第二个为
预测标准差
。此外,以下为几个高斯过程特有的初始化参数:

参数 描述
kernel 核函数对象,即
sklearn.gaussian_process.kernels
中类的实例。
alpha 为了考虑样本噪声在协方差矩阵对角量增加值,可为数值(应用在所有对角线元素),也可以是一个向量(分别应用在每个对角元素上)。
optimizer 可以是一个函数,用于训练过程中优化核函数超参数
n_restarts_optimizer optimizer被调用的次数,默认为1

更详细内容可查阅官方文档

以下是对一个非线性函数
\(y=x\times sin(x)-x\)
的训练预测。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.gaussian_process import GaussianProcessRegressor
from sklearn.gaussian_process.kernels import RBF
from sklearn.gaussian_process.kernels import Product
from sklearn.gaussian_process.kernels import ConstantKernel as C


def f(X):  # 原函数
    return X * np.sin(X) - X


X = np.linspace(0, 10, 20).reshape(-1, 1)  # 训练20个训练样本
y = np.squeeze(f(X) + np.random.normal(0, 0.5, X.shape[1]))  # 样本目标值,并加入噪声
x = np.linspace(0, 10, 200)  # 测试样本特征值
# 定义两个核函数,并取它们的积
kernel = Product(C(0.1), RBF(10, (1e-2, 1e2)))

# 初始化模型:传入核函数对象、优化次数、噪声超参数
gp = GaussianProcessRegressor(kernel=kernel, n_restarts_optimizer=3, alpha=0.3)
gp.fit(X, y)  # 训练

y_pred, sigma = gp.predict(x.reshape(-1, 1), return_std=True)  # 预测

fig = plt.figure()  # matplotlib进行绘图
plt.plot(x, f(x), 'r:', label=u'$f(x) = x\,\sin(x)-x$')
plt.plot(X, y, 'r.', markersize=10, label=u'Observations')
plt.plot(x, y_pred, 'b-', label=u'Prediction')

# 填充(u-2σ,u+2σ)的置信区间
plt.fill_between(
    np.concatenate([x, x[::-1]]),
    np.concatenate([y_pred-2*sigma, (y_pred+2*sigma)[::-1]]),
    alpha=0.3,
    fc='b',
    label=r"95% confidence interval"
)
plt.legend(loc='lower left')
plt.show()

可以看到整体测试样本的真实虚线与测试的实线基本一致,即使有偏差但都在95%的置信区间内。

参考文献

[1]刘长龙. 从机器学习到深度学习[M]. 1. 电子工业出版社, 2019.3.

1、Golang中死锁的触发条件

1.1 书上关于死锁的四个必要条件的讲解

发生死锁时,线程永远不能完成,系统资源被阻碍使用,以致于阻止了其他作业开始执行。在讨论处理死锁问题的各种方法之前,我们首先深入讨论一下死锁特点。

必要条件:

如果在一个系统中以下四个条件同时成立,那么就能引起死锁:

  1. 互斥:至少有一个资源必须处于非共享模式,即一次只有一个线程可使用。如果另一线程申请该资源,那么申请线程应等到该资源释放为止。
  2. 占有并等待:—个线程应占有至少一个资源,并等待另一个资源,而该资源为其他线程所占有。
  3. 非抢占:资源不能被抢占,即资源只能被线程在完成任务后自愿释放。
  4. 循环等待:有一组等待线程 {P0,P1,…,Pn},P0 等待的资源为 P1 占有,P1 等待的资源为 P2 占有,……,Pn-1 等待的资源为 Pn 占有,Pn 等待的资源为 P0 占有。

我们强调所有四个条件必须同时成立才会出现死锁。循环等待条件意味着占有并等待条件,这样四个条件并不完全独立。

图示例:

线程1、线程2都尝试获取对方未释放的资源,从而会一直阻塞,导致死锁发生。

1.2 Golang 死锁的触发条件

看完了书上关于死锁的介绍,感觉挺清晰的,但是实际上到了使用或者看代码时,自己去判断是否会发生死锁却是模模糊糊的,难以准确判断出来。所以特意去网上找了些资料学习,特此记录。

golang中死锁的触发条件:

死锁是当 Goroutine 被阻塞而无法解除阻塞时产生的一种状态。注意:for 死循环不能算在这里,虽然空for循环是实现了阻塞的效果,但是实际上goroutine是处于运行状态的。

1.3 golang 中阻塞的场景

1.3.1 sync.Mutex、sync.RWMutex

golang中的锁是不可重入锁,对已经上了锁的写锁,再次申请锁是会报死锁。上了读锁的锁,再次申请写锁会报死锁,而申请读锁不会报错。

写写冲突,读写冲突,读读不冲突。

func main() {
	var lock sync.Mutex
	lock.Lock()
	lock.Lock()
}   
//报死锁错误
func main() {
	var lock sync.RWMutex
	lock.RLock()
	lock.Lock()
}
//报死锁错误
func main() {
	var lock sync.RWMutex
	lock.RLock()
	lock.RLock()
}
//正常执行
1.3.2 sync.WaitGroup

一个不会减少的 WaitGroup 会永久阻塞。

func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	wg.Wait()
  //报死锁错误
}
1.3.3 空 select

空 select 会一直阻塞。

package main

func main() {
	select {
	
	}
}
//报死锁错误
1.3.4 channel

为 nil 的channel 发送、接受数据都会阻塞。

func main() {
	var ch chan struct{}
	ch <- struct{}{}
}
//报死锁错误

无缓冲的channel 发送、接受数据都会阻塞。

func main() {
	ch := make(chan struct{})
	<- ch
}
//报死锁错误

channel 缓冲区满了的,继续发送数据会阻塞。

2、死锁案例讲解

2.1 案例一:空 select{}

package main

func main() {
	select {
	
	}
}

以上面为例子,select 语句会 造成 当前 goroutine 阻塞,但是却无法解除阻塞,所以会导致死锁。

2.2 案例二:从无缓冲的channel接受、发送数据

func main() {
	ch := make(chan struct{})
	//ch <- struct{}{} //发送
	<- ch //接受
	fmt.Println("main over!")
}

发生原因:

上面创建了一个 名为:ch 的channel,没有缓冲空间。当向无缓存空间的channel 发送或者接受数据时,都会阻塞,但是却无法解除阻塞,所以会导致死锁。

解决方案:边接受边读取

package main
 
// 方式1
func recv(c chan int) {
	ret := <-c
	fmt.Println("接收成功", ret)
}
func main() {
	ch := make(chan int)
	go recv(ch) // 启用goroutine从通道接收值
	ch <- 10
	fmt.Println("发送成功")
}
 
// 方式2
func main() {
   ch := make(chan int,1)
   ch<-1
   println(<-ch)
}

2.3 案例三:从空的channel中读取数据

package main

import (
	"fmt"
	"time"
)

func request(index int,ch chan<- string)  {
	time.Sleep(time.Duration(index)*time.Second)
	s := fmt.Sprintf("编号%d完成",index)
	ch <- s
}

func main() {
	ch := make(chan string, 10)
	fmt.Println(ch,len(ch))

	for i := 0; i < 4; i++ {
		go request(i, ch)
	}

	for ret := range ch{ //当 ch 中没有数据的时候,for range ch 会发生阻塞,但是无法解除阻塞,发生死锁
		fmt.Println(len(ch))
		fmt.Println(ret)
	}
}

发生原因:

当 ch 中没有数据的时候,就是从空的channel中接受数据,for range ch 会发生阻塞,但是无法解除阻塞,发生死锁。

解决办法:当数据发送完了过后,close channel

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func request(index int,ch chan<- string)  {
	time.Sleep(time.Duration(index)*time.Second)
	s := fmt.Sprintf("编号%d完成",index)
	ch <- s

	wg.Done()
}

func main() {
	ch := make(chan string, 10)
	for i := 0; i < 4; i++ {
		wg.Add(1)
		go request(i, ch)
	}

	go func() {
		wg.Wait()
		close(ch)
	}()

	LOOP:
		for {
			select {
			case i,ok := <-ch: // select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句
        if !ok {
          break LOOP
        }
				println(i)
			default:
				time.Sleep(time.Second)
				fmt.Println("无数据")
			}
		}
}

2.4 案例四:给满了的channel发送数据

func main() {
	ch := make(chan struct{}, 3)

	for i := 0; i < 4; i++ {
		ch <- struct{}{}
	}
}

发生原因:

ch 是一个带缓冲的channel,但是只能缓冲三个struct,当channel满了过后,继续往channel发送数据会阻塞,但是无法解除阻塞,发生死锁。

解决办法:读取channel中的数据

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	ch := make(chan struct{}, 3)
	
	go func() {

		for {
			select {
			case i, ok := <- ch:
				wg.Done()
				fmt.Println(i)
				if !ok {
					return
				}
			}
		}
	}()

	for i := 0; i < 4; i++ {
		wg.Add(1)
		ch <- struct{}{}
	}

	wg.Wait()
}

3、总结

最重要的是记住golang中死锁的触发条件:
当 goroutine 发生阻塞,但是无法解除阻塞状态时,就会发生死锁
。然后在使用或者阅读代码时,再根据具体情况进行分析。

channel异常情况总结:

注意:
对已经关闭的channel再次关闭,也会发生panic。

以上就是我对死锁的思考,有不对的地方恳请指出,谢谢。

vue2异步加载之前说过,vue3还是之前的方法,只是把 i18n.setLocaleMessage改为i18n.global.setLocaleMessage

但是本文还是详细说一遍:

为什么需要异步加载语言包

主要还是缩小提代码包,没有按需加载前,语言包内容太多

a.jpg

好几屏幕全部是,虽然从webpack-analysis 看图里面占比可以忽略不计

按语言异步加载语言包

一次加载所有翻译文件是过度和不必要的。

因为可能一直用中文,那么就不会用到英文的数据,就没必要去加载。只在请求的时候去加载它

改动前代码

import { createI18n } from 'vue-i18n';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import cookies from '@/utils/cookies';
import chineseJson from '../lang/zh-cn.json';
import englishJson from '../lang/en.json';
//****n
const currentLang = cookies.get('blueking_language') || 'zh-cn';
if (currentLang === 'en') {
  dayjs.locale('en');
} else {
  dayjs.locale('zh-cn');
}
const i18n = createI18n({
  locale: currentLang,
  fallbackLocale: 'zh-cn', // 设置备用语言
  silentFallbackWarn: true,
  silentTranslationWarn: true,
  globalInjection: true,
  allowComposition: true,
  messages: {
    en: { ...englishJson },
    'zh-cn': { ...chineseJson },
    //****n
  },
});
export default i18n;

这个文件n多,堆叠起来体积也不少

改动后

import { createI18n } from 'vue-i18n';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
import cookies from '@/utils/cookies';
// import chineseJson from '../lang/zh-cn.json';
// import englishJson from '../lang/en.json';
export type LangType = 'zh-cn'|'en';
const currentLang: LangType = cookies.get('blueking_language') as LangType || 'zh-cn';
// 初始化加载fallbackLocale 语言包,但是图表平台首先加载框架,无需放到框架里面去加载
/* const messages = {
  // en: { ...englishJson },
  'zh-cn': { ...chineseJson },
};*/
const i18n = createI18n({
  locale: currentLang,
  fallbackLocale: 'zh-cn', // 设置备用语言
  silentFallbackWarn: true,
  silentTranslationWarn: true,
  globalInjection: true,
  allowComposition: true,
  // messages,
});

export  function changLang(langs: LangType) {
  if (currentLang === 'en') {
    dayjs.locale('en');
  } else {
    dayjs.locale('zh-cn');
  }
  cookies.set('blueking_language', langs);
  loadLanguageAsync(langs);
  // window.location.reload();
}
export function setI18nLanguage(lang: LangType) {
  i18n.global.locale = lang;
  return lang;
}
export function loadLanguageAsync(lang: LangType) {
  return import(/* webpackChunkName: "lang-request" */`../lang/${lang}.json`).then((langfile) => { // 动态加载对应的语言包
    i18n.global.setLocaleMessage(lang, langfile);
    return setI18nLanguage(lang);   // 返回并且设置
  });
}
changLang(currentLang);
export default i18n;

这样就可以了

注意事项

  • 由于是异步加载,比如初始化只加载 fallbackLocale ,代码中注释的部分

  • vue3使用vue-i18n 9.x ,相关方法在i18n.global.xxx

但是这个加载包还是有些打,需要进一步拆分

按模块或路由加载语言包

这个优化有很多措施

拆分模块

之前的语言包全部是在一个json文件里面。第一个,json无法tree-shake 树摇 掉不用代码。

如果是ts,首先第一个按页面、功能 分成一个个 对象。虽然不用tree-shake。

但是可以通过组合得到不同的js。

然后在路由钩子里面,按需注入。loadLanguageAsync

参考文章:

vueI18n 多语言文件按需加载:
https://blog.csdn.net/yujin0213/article/details/119137798

vue 多语言 vue-i18n 按需加载,异步调用
https://www.cnblogs.com/chenyi4/p/12409074.html

十分钟入门前端最佳的语言国际化方案
https://zhuanlan.zhihu.com/p/144717545


转载
本站
文章《
vue2升级vue3:vue-i18n国际化异步按需加载
》,
请注明出处:
https://www.zhoulujun.cn/html/webfront/ECMAScript/vue3/8930.html

学习操作系统原理最好的方法是自己写一个简单的操作系统。


在上一讲中我们向屏幕打印字符串“GrapeOS”用了十几行汇编代码,如果要输出的字符比较多,这种方法太繁琐了。本讲我们将打印字符串封装成一个函数,使用时就方便多了。

一、mbr7.asm

mbr7.asm代码如下:

org 0x7c00 ;如果没有该行将无法正确打印要显示的字符串。

;初始化段寄存器。
mov ax,cs
mov ds,ax ;ds指向与cs相同的段。
mov ax,0xb800
mov es,ax ;本程序中es专用于指向显存段。

;打印字符串:"GrapeOS boot start."。
mov si,boot_start_string
mov di,80 ;在屏幕第2行显示。
call func_print_string

stop:
hlt
jmp stop 

;打印字符串函数。
;输入参数:ds:si,di。
;输出参数:无。
;ds:si 表示字符串起始地址,以0为结束符。
;di 表示字符串在屏幕上显示的起始位置(0~1999)。
func_print_string:
mov ah,0x07 ;ah表示字符属性,0x07表示黑底白字。
shl di,1 ;乘2(屏幕上每个字符对应2个显存字节)。
.start_char: ;以点开头的标号为局部标号,完整形式是 func_print_string.start_char,但在同一个全局标号func_print_string内部不需要写完整形式。
mov al,[si]
cmp al,0
jz .end_print
mov [es:di],ax ;将字符和属性放到对应的显存中。
inc si
add di,2
jmp .start_char
.end_print:
ret

boot_start_string:db "GrapeOS boot start.",0

times 510-($-$$) db 0
db 0x55,0xaa

下面我们来编译运行,看看效果:

从上面QEMU的截图中看到,我们在屏幕第二行成功打印出了字符串“GrapeOS boot start.”,符合预期。

二、拓展讲解伪指令org

如果我们将代码中的第一行“org 0x7c00”删除掉,会怎么样呢?请看下面实验截图:

从上面QEMU截图中可以看到,屏幕第二行显示出了一个奇怪的字符,这并不是我们想要显示的字符串“GrapeOS boot start.”。原因是代码“org 0x7c00”表示本程序将来会加载到地址为0x7c00的内存处,这样在汇编器汇编时标号“boot_start_string”才能计算出正确的地址。如果没有“org 0x7c00”,汇编器默认会认为本程序将来会加载到地址为0x0的内存处,标号“boot_start_string”计算出的地址会在内存的前512个字节中,这明显是错误的地址,打印出来的字符串自然也是错的。
下面我们用反汇编验证一下:

从上面的反汇编截图可以看到,此时标号“boot_start_string”被计算为0x29。
我们将程序中的“org 0x7c00”恢复,重新汇编后反汇编,截图如下:

从上面的反汇编截图可以看到,此时标号“boot_start_string”被计算为0x7c29,这才是正确的。


本讲视频版地址:
https://www.bilibili.com/video/BV1qT411D7oV/
本教程代码和资料:
https://gitee.com/jackchengyujia/grapeos-course
GrapeOS操作系统QQ群:643474045