开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

14 ZGC垃圾收集器 一、ZGC概要 二、ZGC的内存模

发表于 2021-11-08

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」

一、ZGC概要

1.1 ZGC研究资料

这块要详细拿出来说一下。

  1. ZGC官网介绍:wiki.openjdk.java.net/display/zgc…
  2. ZGC文档(pdf,讲解详细):cr.openjdk.java.net/~pliden/sli…

这两篇文章,详细介绍了ZGC。第二个是pdf文档,可以下载下来详细看。

1.2 ZGC支持的平台

这个可以在官方文档(wiki.openjdk.java.net/display/zgc…)上看到,目前jdk11目前只支持linux。

JDK11只支持Linux64位,到jdk4开始支持windows和macOS。

1.3 ZGC的目标

我们来看看ZGC都有哪些目标,目标也就是ZGC存在的意义。

  1. 支持更大的堆内存空间。多大算是更大呢?以前都是GB级别的,现在到TB级别。在jdk11版本最大可以支持到4T的内存。(这里不是硬盘是4T,是内存,重点强调,内存。)未来一二十年的场景都涵盖到了。
  2. 支持更短的GC停顿时间,目标是10ms以内:这个更短的GC停顿时间是STW的时间。这一点就更厉害了,支持的内存打了,但是STW停顿时间却短了。
  3. 为未来的 GC 功能奠定基础. 奠定了未来垃圾收集器的一个基石。如果未来还要再升级GC,那么就会以ZGC底层算法为标准进行研究。之前我们垃圾收集都是以CMS为基石,比如G1就是在CMS的基础上发展而来的。ZGC也是在CMS的基础上发展。
  4. 吞吐量降低不超过15%:STW的时间控制的越短,那么吞吐量就会越低。这里ZGC的目标是,控制吞吐量降低不超过15%。

通常我们GC的时间是1000ms,但是为了降低GC收集时STW的时间,我们控制STW的时间为10ms以内,产生的后果可能是GC整体时间边长了。原来是1000ms,现在变成了1150ms。这样吞吐量就降低了。

1.4 ZGC启动的命令

  1. 启动ZGC并打印日志:-XX:+UseZGC -Xmx -Xlog:gc
  2. 启动ZGC并打印详细日志:-XX:+UseZGC -Xmx -Xlog:gc

1.5 ZGC的特点:

ZGC不分代了。

我们以前分代的原因是什么?因为大部分对象都有朝生夕死的特点。所以朝生夕死的对象可能就要求尽快被清理掉。如果不分代了,结果就是,可能更频繁的触发gc,因为里面有很多朝生夕死的对象。然后每次gc,都会遍历所有的对象

ZGC为什么部分带了呢?因为分代实现起来比较麻烦,作者就先实现了一个比较简单可用的单代版本,后续优化可能会分代。

二、ZGC的内存模型

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

where字句常用的查询条件

发表于 2021-11-08

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」

WHERE 子句

WHERE 子句用于规定选择的标准。

如需有条件地从表中选取数据,可将 WHERE 子句添加到 SELECT 语句。

语法:

1
sql复制代码SELECT 列名称 FROM 表名称 WHERE 列 运算符 值

where字句常用的查询条件

查询条件 谓词
比较 = , > , < , >= , <= , (<> , !=不等于)
确定范围 BETWEEN AND(相当于闭集合[BETWEEN,AND]) , NOT BETWEEN AND(不属于闭集合[BETWEEN,AND]的范围)
确定集合 IN , NOT IN
字符匹配 LIKE NOT LIKE 通配符 % ,_
多重运算(逻 辑运算) AND, OR , NOT
空值 IS NULL , IS NOT NULL

使用实例及说明

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
50
51
sql复制代码-- where字句常用的查询条件及用法
-- 比较 = , > , < , >= , <= , (<> , !=不等于)
-- 用法 修改id为1的学生的姓名为狂铁
UPDATE `student` SET `name`='狂铁' WHERE id=1;
-- 修改年龄不等于3的学生的姓名
UPDATE `student` SET `name`='虞姬' WHERE age <>3;
-- 查询studeng表中性别为女的学生ID
SELECT id
FROM student
WHERE sex='女';
-- 查询年龄小于等于4的学生的id和姓名
SELECT id ,`name`
FROM student
WHERE age >=4;


-- 确定范围 BETWEEN AND(相当于闭集合[BETWEEN,AND]) ,NOT BETWEEN AND(不属于闭集合[BETWEEN,AND]的范围)
-- 查询年龄在4到六岁的学生的id和姓名
SELECT id,`name` FROM student WHERE age BETWEEN 4 AND 6;
-- 查询年龄不在3到7岁的学生的所有信息
SELECT * FROM student WHERE age NOT BETWEEN 3 AND 7;


-- 确定集合,和
SELECT age FROM student WHERE IN(4,5,6)
SELECT age FROM student WHERE age=3 OR age=4 OR age=5


-- 字符匹配 LIKE NOT LIKE 通配符 %(代表任意长度的字符串) ,_(代表任意单个字符)
-- 查询姓名为两个字且第一个字为'狂'的学生的所有信息
SELECT * FROM student WHERE `name` LIKE '狂_';
-- 查询姓名第一个字为'狂'的学生的所有信息
SELECT * FROM student WHERE `name` LIKE '狂%'
-- 查询姓名第二个字为'狂'的学生的所有信息
SELECT * FROM student WHERE `name` LIKE '_狂%'
-- 查询姓名第一个字不为'狂'的学生的所有信息
SELECT * FROM student WHERE `name` NOT LIKE '狂%'
-- 如果用户查询的字符串本身就含有通配符,这时就要用ESCAPE'\'(换码字符)对通配符转义
-- 格式 WHERE `name` LIKE `字符串` ESCAPE'\';


-- 空值 IS NULL , IS NOT NULL
-- 查询地址为空的所有学生的信息
SELECT * FROM student WHERE `address` IS NULL;


-- 多重运算(逻辑运算) AND, OR , NOT
-- 查询姓名为两个字且第一个字为'狂'且年龄大于4的学生的所有信息
SELECT * FROM student WHERE `name`LIKE '狂_' AND age>4;
-- 查询姓名为两个字且第一个字为'狂'和第一个字为'虞'的学生的所有信息
SELECT * FROM student WHERE `name`LIKE '狂_' OR `name` LIKE '虞_';

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

「Rust 流程控制」for

发表于 2021-11-08

「这是我参与11月更文挑战的第 6 天,活动详情查看:2021最后一次更文挑战」


程序按理说是从上到下执行,除非你不希望这样。Rust有一套有用的流控制机制来促进这一点。本节将简要介绍其基本原理。

For

可以说是迭代操作中核心的核心语法。

for循环是Rust中迭代的核心。迭代值的集合,包括迭代可能有无限多的值的集合。其基本形式:

1
2
3
rust复制代码for item in container {
// ...
}

这种基本形式使得容器中每个连续的元素都可以作为 item。通过这种方式,Rust以一种易于使用的高级语法模拟了许多动态语言。然而,它也有一些隐患。

反常的是,一旦块结束,再次访问容器就变得无效了。即使容器变量仍然在本地范围内,它的生命周期也已经结束。Rust假定一旦 Block 结束,就不再需要容器了。

当你想在以后的程序中重复使用容器时,可以使用引用。当不使用引用时,Rust假定不再需要容器。要给容器添加一个引用,请在它前面加上这个(&),如本例所示:

1
2
3
rust复制代码for item in &container {
// ...
}

如果你需要在循环过程中修改item,你可以通过包含mut关键字来使用一个可变的引用:

1
2
3
rust复制代码for item in &mut collection {
// ...
}

作为一个实现细节,Rust的for循环结构被编译器扩展为方法调用。如下表所示,这三种形式的for分别映射到不同的方法。

简写 转换写法 访问性
for item in collection for item in IntoIterator::into_iter(collection) Ownership
for item in &collection for item in collection.iter() Read-only
for item in &mut collection for item in collection.iter_mut() Read-write

当局部变量不在一个块内使用时,按照惯例,你会使用下划线(_)*。*将这种模式与 exclusive range syntax (n..m)和 inclusive range syntax (n..=m)结合起来使用,可以清楚地看出其目的是为了执行一个固定次数的循环。下面是一个例子:

1
2
3
rust复制代码for _ in 0..10 {
// ...
}

在许多编程语言中,通过使用一个临时变量来循环处理事情是很常见的,该变量在每次迭代结束时都会被递增。传统上,这个变量被命名为i(代表索引)。如下:

1
2
3
4
5
rust复制代码let collection = [1, 2, 3, 4, 5];
for i in 0..collection.len() {
let item = collection[i];
// ...
}

上面代码可以通过编译。在不能通过对集合中的项目进行直接迭代的情况下,这也是必要的。然而,一般来说,这是不可取的。手动方法会带来两个问题:

性能

用collection[index]语法索引值会产生运行时的边界检查费用。也就是说,Rust检查索引当前是否存在于集合中的有效数据。当直接在集合上进行迭代时,这些检查是没有必要的。编译器可以使用编译时分析来证明非法访问是不可能的。

安全性

随着时间的推移,定期访问集合会带来它已经改变的可能性。直接在集合上使用for循环可以让Rust保证集合不被程序的其他部分所触及。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

NestJS 快速上手指南

发表于 2021-11-08

本文主要内容是 NestJS 快速上手指南,不包含详细的 API 罗列和能力介绍,如有需要可以随时查阅官方文档:docs.nestjs.com。

简介

NestJS 是一个基于 Node.js 的后端框架

为什么使用 NestJS

它提供了一个完善的、开箱即用的后端架构。支持 TypeScript、OOP(Object Oriented Programming)、FP(Functional Programming)、FRP(Functional Reactive Programming)。支持依赖注入,可以创建高度可测试、易扩展、松散耦合、易维护的后端程序。

核心概念

  • Controller

直接处理请求和返回响应的部分,使用 class 和 decorators 构建。Class 让我们使用对象编程,装饰器可以让 Nest 构建路由映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
js复制代码// cat.controller.ts

import { Controller, Get } from '@nestjs/common';



@Controller('cats')

export class CatsController {

@Get()

findAll(): string {

return 'This action returns all cats';

}

}
  • Services

负责数据存储和获取的部分,同样使用 class 和 decorators 构建。这里的 @Injectable() 赋予了 CatService 可以被 Nest IoC 容器管理的特性。

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
ts复制代码// cat.service.ts

import { Injectable } from '@nestjs/common';

import { Cat } from './interfaces/cat.interface';



@Injectable()

export class CatsService {

private readonly cats: Cat[] = [];



create(cat: Cat) {

this.cats.push(cat);

}



findAll(): Cat[] {

return this.cats;

}

}
  • Provider

provider 是可以作为依赖被注入的东西,Nest 中几乎所有东西都可以被当作 provider,比如上面代码中的 CatsService,就是一个 provider。

Provider 在 Controller 中消费。

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
ts复制代码// cat.controller.ts

import { Controller, Get, Post, Body } from '@nestjs/common';

import { CreateCatDto } from './dto/create-cat.dto';

import { CatsService } from './cats.service';

import { Cat } from './interfaces/cat.interface';



@Controller('cats')

export class CatsController {

constructor(private catsService: CatsService) {}



@Post()

async create(@Body() createCatDto: CreateCatDto) {

this.catsService.create(createCatDto);

}



@Get()

async findAll(): Promise<Cat[]> {

return this.catsService.findAll();

}

}

Provider 还需要在 Module 中注册,之后才可以被 Nest 注入到相应的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ts复制代码// cat.module.ts

import { Module } from '@nestjs/common';

import { CatsController } from './cats/cats.controller';

import { CatsService } from './cats/cats.service';



@Module({

controllers: [CatsController],

providers: [CatsService],

})

export class AppModule {}
  • Modules

使用 @Module 注解的类,被 Nest 用来组织应用程序的结构, 解析模块、provider 关系和依赖。

根据 SOLID 原则,我们将相关部分放到一起,一个模块的文件结构大概会是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
md复制代码src

|--cats

| |--interfaces

| | |--cat.interface.ts

| |--cats.controller.ts

| |--cats.module.ts

| |--cats.service.ts

|

|--app.module.ts

|--main.ts

app.module.ts 是应用程序的根模块,必须存在,负责组织内部其它模块,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ts复制代码// app.module.ts

import { Module } from '@nestjs/common';

import { CatsModule } from './cats/cats.module';



@Module({

imports: [CatsModule],

})

export class AppModule {}
  • 中间件 Middleware

略

  • 异常过滤器 Exception filters

Nest 自带的异常捕获器,负责处理代码中未处理的异常。

比如未处理的全局异常返回、参数校验失败返回,都会经过内置的过滤器返回固定的格式。我们也可以使用过滤器自定义处理全局异常返回的格式,同时记录错误日志。

  • 管道 Pipe

主要做一些数据格式转换和校验的工作。

转换和校验其实可以在路由处理器方法中完成,但是为了单一责任原则,Nest 专门设计了 pipe 来处理此类工作。

1
2
3
4
5
6
7
8
9
ts复制代码// 格式转换

@Get(':id')

async findOne(@Param('id', ParseIntPipe) id: number) {

return this.catsService.findOne(id);

}

参数校验

Nest 使用内置的 ValidationPipe 和第三方包 class-validator 做参数校验

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
ts复制代码// 全局开启校验

async function bootstrap() {

const app = await NestFactory.create(AppModule);

app.useGlobalPipes(new ValidationPipe());

await app.listen(3000);

}

bootstrap();



// controller 中定义参数类型

@Post()

create(@Body() createUserDto: CreateUserDto) {

return 'This action adds a new user';

}
  • 守卫 Guards

基于一些条件判断一个请求是否可以被路由处理器处理,主要用于权限校验。

  • 拦截器 Interceptors

可以在路由函数执行之前或之后做一些事情。比如,全局成功返回的固定格式,访问日志等。

以上便是 NestJS 的一些核心概念,看完后对 NestJS 应该有了一个整体的了解,再结合对应部分的文档,便可以熟练掌握 NestJS 的使用。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

性能分析之快速从压力趋势定位到具体代码

发表于 2021-11-08

「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战」


前言

对于做性能分析的来说,快速定位的能力非常重要。层层剥离,不要跳跃。
最近在项目中看到不少人做性能问题的解决是全靠蒙的。比如疯狂加线程数什么的。

问题现象

最近在测试的一个系统中出现了一个现象是这样的。
(这次不打算再很细的一步步讲了,只说整体的思路,因为使用到的操作的手段在我之前的文章中都有说明)。

先看下几个图。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

从上面的图就可以知道这个系统明显是有性能瓶颈(要是有人问为啥,我觉得肯定不是做性能的)。

出现这种情况怎么办呢?当然是要知道后端的哪个地方有问题,于是,我查了每一个相关的系统,看到如下信息。

系统级别信息:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

软中断,软中断,软中断。接着可以找应用的麻烦了。再接着看。切换到线程之后,就看到了如下这些东西。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

然后呢,就是老思路了。打印栈信息,拿出来瞄一眼。

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

这个问题的原因,其实就是一个内存参数配置错误的问题。不提也罢。

在性能问题中,有很多时候我们都在为这样的人为的过失折腾来折腾去。

总结

本文就是为了说明这个分析的思路,所有用到的工具都在我之前的文章中提到了。

对于性能分析来说,思路的清晰比直接上手去做,要重要得多,重要得多得多。
不然我们就一直在不断地浪费自己和别人的时间。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Linux 磁盘空间被吃掉了?这样排查不背锅!

发表于 2021-11-08

在服务器运维过程中,我们时常会遇到这样的情况,收到服务器磁盘空间告警

)

和告警信息一致,接着我们就是要找到导致磁盘空间满的目录或文件

如何找到占用空间大的目录或文件?

一种比较笨的方法是,在根目录下,通过du -hs命令,列出各目录所占空间大小

)

之后再用同样的方法继续到对应目录下去找

再相对高效一点的方法是通过du的-d参数,或–max-depth,设置查询的目录深度,目录深度增加,所查询的目录,展示出来会很多,这个时候可以通过grep进行过滤

1
2
bash复制代码du -h -d 2|grep [GT] |sort -nr
du -h --max-depth=2|grep [GT] |sort -nr

通过这样的方式,可以搜出以G或者T为单位的占用磁盘空间的大目录,并排序

或者可以通过find来查询

1
bash复制代码find / -type f -size +1G -exec du -h {} \;

从效率上来说,find要比du要更快速、灵活

通过这两种方法,我们可以快速找到占用磁盘空间的罪魁祸首

你以为就这么简单?很多时候,你会发现,通过find或du查半天,发现所有加起来的占用空间,和df看到的磁盘空间占用,相差很大,就比如我上面的两张图

通过df查看,磁盘使用37G,但是在根目录下通过du -hs 查看,总共加起来差不多10G,没有隐藏目录,那空间被谁吃了?

很明显,有空间被已删除文件占用,文件删除了,但是资源没释放

之前介绍过一个很好用的命令:lsof,我们可以通过以下命令去查看

lsof +L1

)

从结果可以看出,有一个28G左右的大日志文件,删除了,但是空间没释放,这是很常见的一种情况

对应的解决方法就是,重启tomcat应用,释放空间

磁盘空间莫名被吃?

还有一种经常有人问的问题,就是,通过df查看到的磁盘

)

会发现,Used和Avail加起来不够Size,莫名被吃掉一部分

其实这是Linux文件系统的一种安全策略,它默认会为root用户保留5%的磁盘空间,留作紧急情况使用。这样能保证有些关键应用(比如数据库)在硬盘满的时候有点余地,不致于马上就 crash

我们可以通过tune2fs修改预留空间的比例

tune2fs -m 1 /dev/vda1

通过下图可以看到前后对比

)

这样被吃掉的空间,就吐出来了

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SQL基础查询语句

发表于 2021-11-08

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战。

SQL语句中,查询是使用最多的操作,SQL不仅能够查询表中的数据,还可以返回算术运算、表达式的结果等,接下来就一起了解一下基本的查询语句。

基础SELECT语句

查询指定字段

语法格式:

1
sql复制代码SELECT <字段名>,... FROM <表名>;

在语句中可以指定多个字段,结果会根据指定的字段进行显示。

例如,在users用户表中查询用户id、用户名、昵称、性别信息:

1
sql复制代码SELECT user_id,user_name,nick_name,sex FROM users;

查询全部字段

查看表中的全部字段可以使用星号”*“表示,例如,以下语句查询users用户表中的所有数据:

1
sql复制代码SELECT * FROM users;

“*“代表所有字段,数据库在解析该语句时,会使用表中的字段名进行扩展,根据实际情况将”*“换成user_id、user_name、nick_name、sex、mobile、email等表的字段。


设定别名

使用AS关键字可以为列设定别名。

1
sql复制代码SELECT user_id AS id,user_name AS 用户名称,nick_name AS 昵称,sex AS 性别 FROM users;

常数的查询

SELECT语句中不仅可以书写列名,而且还可以书写常数,如下:

1
2
sql复制代码SELECT 100;
SELECT '用户';

表达式的查询

1
sql复制代码SELECT 98%100;

去重

SELECT语句中可以使用DISTINCT关键字去除查询结果中的重复记录,例如,去除user_name重复的数据:

1
sql复制代码SELECT DISTINCT user_name FROM users;

注意: DISTINCT对NULL是不进行过滤的,即返回的结果中是包含NULL值的;

当DISTINCT应用到多列的时候,应用范围是其后面跟的所有字段,而且DISTINCT只能放到所有字段的前面,也就是第一个列名之前。

1
sql复制代码SELECT DISTINCT user_name,nick_name FROM users;

条件查询

SELECT语句通过WHERE子句来查询符合指定条件的记录,WHERE子句要紧跟在FROM子句之后。

1
sql复制代码SELECT <字段名>,... FROM <表名> WHERE <条件表达式>;

单条件查询

  • 查询性别为男性的用户:
1
sql复制代码SELECT * FROM users WHERE sex='男';
  • 查询年龄小于等于24的用户:
1
sql复制代码SELECT * FROM users WHERE age<=24;
  • 查询用户id不是3的用户:
1
sql复制代码SELECT * FROM users WHERE NOT user_id=3;

在第3个例子中使用了NOT运算符,在条件前加NOT就代表否定这个条件,查找这个条件以外的记录。

多条件查询

  • 查询年龄小于等于24或者性别为男性的用户:
1
sql复制代码SELECT * FROM users WHERE age<=24 OR sex='男';
  • 查询年龄小于等于24并且性别为男性的用户:
1
sql复制代码SELECT * FROM users WHERE age<=24 AND sex='男';

上面使用了多条件查询,条件能同时成立AND运算符,条件只能成立一个用OR运算符。

指定范围查询

  • 查询用户id在(2,3,7,8)范围内的用户:
1
sql复制代码SELECT * FROM users WHERE user_id IN (2,3,7,8);

IN在WHERE子句中规定多个值,IN后跟圆括号,括弧内可以有一个或多个值,值之间由逗点分开,值可以是数字或者字符。

  • 查询用户id在10-15之间的用户:
1
sql复制代码SELECT * FROM users WHERE user_id BETWEEN 10 AND 15;

BETWEEN ... AND指定介于两个值之间的数据范围,这些值可以是数值、文本或者日期。

模糊查询

LIKE关键字用于SQL的模糊查询,用于对搜索字符串进行模式匹配。

语法格式:

1
sql复制代码字段名 LIKE pattern

匹配模式:

  • %:百分号匹配零个、一个或多个字符
  • -:下划线符号匹配单个字符
模式 含义
LIKE ‘a%’ 匹配以A开始的字符串,如abc、ab
LIKE ‘%y’ 匹配以y结尾的字符串,如aay、xy
LIKE ‘%mn% 匹配包含mn的字符串,如amnb、lmn
LIKE ‘a_‘ 匹配以a开始,后面只有一个字符的数据,如ay、ab
LIKE ‘_y’ 匹配以y结尾,前面只有一个字符的数据,如ay,xy

例如:

查找用户昵称包含tigeriaf的数据:

1
sql复制代码SELECT * FROM users WHERE nick_name LIKE '%tigeriaf%';

原创不易,如果小伙伴们觉得有帮助,麻烦点个赞再走呗~

最后,感谢女朋友在工作和生活中的包容、理解与支持 !

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go如何保证并发安全(四) Go内存模型

发表于 2021-11-08

这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战

Go内存模型

软件(编译器)或硬件(CPU)系统可以根据其对代码的分析结果,一定程度上打乱代码的执行顺序,以达到其不可告人的目的(提高 CPU 利用率) from 曹大

  • 可见性:GO 内存模型阐明了一个goroutine对某变量的写入,如何才能保证被另一个读取该变量的goroutine监测到。
  • 事件的发成次序:程序在修改被多个goroutine同时访问的数据时必须序列化该访问。

要序列化访问,需要通过channel,或其它像sync和sync/atomic包中的同步原语来保护数据。

Happens Before(事件的发生次序)

定义

若事件e1发生在e2之前,那么我们就说e2发生在e1之后。换言之,若e1既未发生在e2之前,又未发生在e2之后,那么我们就说e1与e2是并发的。

解释

在单个goroutine中,事件发生的顺序即为程序所表达的顺序。

仅在不会改变语言规范对goroutine行为的定义时,编译器和处理起才会对读取和写入的顺序进行重新排序。

由于存在重新排序,一个goroutine监测到的执行顺序可能与另一个goroutine监测到的不同。例如,若一个goroutine执行a=1;b=2;,另一个goroutine可能监测到b的值先与a更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
diff复制代码单goroutine的情形:
-- w0 ---- r1 -- w1 ---- w2 ---- r2 ---- r3 ------>
​
所有 r/w 的先后顺序都是可比较的。
​
双Go程的情形:
-- w0 -- r1 -- r2 ---- w3 ---- w4 ---- r5 -------->
-- w1 ----- w2 -- r3 ---- r4 ---- w5 -------->
​
单goroutine上的事件都有先后顺序;而对于两个goroutine,情况又有所不同。即便在时间上 r1 先于 w2 发生,
但由于每条goroutine执行时长都像皮筋一样伸缩不定,因此二者在逻辑上并无先后次序。换言之,即二者并发。
对于并发的 r/w,r3 读取的结果可能是前面的 w2,也可能是上面的 w3,甚至 w4 的值;
而 r5 读取的结果,可能是 w4 的值,也能是 w1、w2、w5 的值,但不可能是 w3 的值。
​
​
双goroutine交叉同步的情形:
-- r0 -- r1 ---|------ r2 ------------|-- w5 ------>
-- w1 --- w2 --|-- r3 ---- r4 -- w4 --|------->
​
现在上面添加了两个同步点,即 | 处。这样的话,r3 就是后于 r1 ,先于 w5 发生的。
r2 之前的写入为 w2,但与其并发的有 w4,因此 r2 的值是不确定的:可以是 w2,也可以是 w4。
而 r4 之前的写入的是 w2,与它并发的并没有写入,因此 r4 读取的值为 w2。

同步

初始化

程序的初始化运行在单个goroutine中,但该goroutine可能会创建其它并发运行的goroutine。

若包p导入了包q,则q的init函数会在p的任何函数启动前完成。

函数main.main会在所有的init函数结束后启动。

goroutine的创建

go语句会在当前goroutine开始执行前启动新的goroutine。

The go statement that starts a new goroutine happens before the goroutine’s execution begins.

这个不是很理解要表达的是什么意思(通过go关键字创建一个新的goroutine的动作发生在goroutine执行之前?)

1
2
3
4
5
6
7
8
9
10
go复制代码var a string
​
func f() {
print(a)
}
​
func hello() {
a = "hello, world"
go f()
}

goroutine的销毁

goroutine无法确保在程序中的任何事件发生之前退出。(在不使用同步原语保护的情况下)

1
2
3
4
5
6
go复制代码var a string
​
func hello() {
go func() { a = "hello" }()
print(a)
}

例如:goroutine无法确保在打印前完成对变量a的赋值,因为对a进行赋值后并没有任何同步事件,因此无法保证被其它goroutine监测到(可以使用channel来解决)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

「Generics 解密」2-3

发表于 2021-11-08

「这是我参与11月更文挑战的第 6 天,活动详情查看:2021最后一次更文挑战」


step 1

Map类型有一个极其灵活的类型定义,一个没有边界的泛型。新方法也没有边界,但它只在std::iter中可用。

让我们把这个应用到我们的Connector上:

1
2
3
4
5
6
7
8
9
10
rust复制代码struct Connector<I, F> {
points_iter: I,
op: F,
}

impl<I, F> Connector<I, F> {
fn new(points_iter: I, op: F) -> Self {
Connector { points_iter, op }
}
}

step 2

一般来说,std::iter类型在其内部迭代器上调用next并很容易改变其行为。其中许多类型,如Take::next,都是用非常简单的代码完成的。

标准库还告诉我们,impl block 是我们应该施加 trait bounds 的地方,因为这是严格意义上的必要的地方。请注意,迭代器类型有一个相关的类型叫做Item。语法 <Item = Something> 将其与通用类型区分开来。

这取代了我们简单地从Vec中抓取项目的旧代码。这将适用于任何能够创建迭代器的类型,其中Item是Point:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
rust复制代码impl<Id, I, F> Iterator for Connector<I, F>
where
I: Iterator<Item = Point<Id>>,
F: Fn(Point<Id>, Point<Id>) -> Line<Id>,
{
type Item = Line<Id>;

fn next(&mut self) -> Option<Self::Item> {
let point_a = self.points_iter.next()?;
let point_b = self.points_iter.next()?;
let line = (self.op)(point_a, point_b);

Some(line)
}
}

step 3

迭代器有产生其他迭代器的方法。如果一个例子还不够,可以在 Iterator trait 中还可以找到几十个其他的例子。

首先,我们需要改变Connect trait,因为它的方法会返回一个Connector,这一点已经被改变了。我们重写了Connector以使其更加灵活,所以让我们对Connect做同样的事情。我们将继续采用将 trait bounds 从定义中移出并移入实现的方式:

1
2
3
rust复制代码trait Connect<I, F> {
fn connect(self, op: F) -> Connector<I, F>;
}

我们不能给Iterator添加方法,它不是我们代码的一部分。但是我们可以使用一个非常有用的技巧:我们将为每个实现了Iterator的类型实现我们的Connect方法。

1
2
3
4
5
6
7
8
rust复制代码impl<I, Id, F> Connect<Id, F> for I
where
I: Iterator,
{
fn connect(self, op: F) -> Connector<I, F> {
Connector::new(self, op)
}
}

这被称为自动实现。标准库经常使用它们。一个众所周知的例子是,为一个类型实现Display会给它一个ToString的自动实现。

Fn trait bound 也被删除了,因为Connect和Connector不再需要它了。由于选择性的灵活性,我们优雅地使我们的代码更简单,更强大。让我们看看我们的新代码的运行情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
rust复制代码fn main() {
let points = {
let mut map = std::collections::HashMap::new();

map.insert("A", Point { id: 1001 });
map.insert("B", Point { id: 1002 });
map.insert("C", Point { id: 1003 });
map.insert("D", Point { id: 1004 });
map.insert("E", Point { id: 1005 });

map
};

let mut connector = points.into_values().connect(|left, right| Line {
points: (left, right),
});

println!("This should be a line: {:?}.", connector.next());
println!("This should be a line: {:?}.", connector.next());
println!("This should be nothing: {:?}.", connector.next());
}

我们的代码比我们为Vec手动实现Connect时做的要多得多,但我们使用的行数却完全相同。想象一下,为std::collection中的每个类型手动实现Connect所需要的代码量。由于泛型的复杂性,我们只用一个代码块就能得到同样的东西,并且让编译器来填补空白。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

「Generics 解密」2-2

发表于 2021-11-08

「这是我参与11月更文挑战的第 6 天,活动详情查看:2021最后一次更文挑战」


现在除了我们的代码更干净一些,而且可能更有性能外,其实没有什么变化。

我们减少了运行时的开销,这很好。用户仍然不能用错误的类型创建一个连接器,因为他们必须使用new,而new()是在一个有 trait bound 的 impl block 中。

如果我们后来发现我们只在类似的 impl block 中调用 Connector::new,我们就可以删除这个特定的 trait bound。注意,标准库使用 pub(in crate::iter) 来确保 Map::new 不会在其他地方被误用。现在让我们来谈谈 Point Vec。

这并不是标准库中的做法。这个迭代器实际上拥有这个Vec。这不仅对使用Vec类型而不是其他集合施加了不必要的限制,而且它还拥有集合本身的所有权,而不仅仅是它的内容。这是一个初步的步骤,以确保我们理解我们在做什么。标准库在iter模块中对泛型特质的使用是高超的,但很容易模仿。在我们继续创建我们想要的确切水平的灵活性之前,这一步确保我们有正确的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
rust复制代码impl<Id, F> Iterator for Connector<Id, F>
where
F: Fn(Point<Id>, Point<Id>) -> Line<Id>,
{
type Item = Line<Id>;

fn next(&mut self) -> Option<Self::Item> {
let point_a = self.points.pop()?;
let point_b = self.points.pop()?;
let line = (self.op)(point_a, point_b);

Some(line)
}
}

Traits

trait 本身就是一个类型系统的抽象。将我们自己的trait与trait约束的泛型结合起来,是掌握的一个重要步骤。

让我们从使用trait来实现Vec类型的一个方法开始。这看起来类似于在一个迭代器上调用map或filter。用一个方法来命名一个trait,这被认为是 “最佳做法”,即以该方法命名。

所以我们的单方法trait将被命名为:Connect。这个trait将取代我们现在的 connect()。

当我们写 impl<T, U> 时,我们是在声明哪些泛型在该 impl 块的范围内。我们必须将这些泛型作为类型参数传递到 trait 中。看一下trait,我们就知道为什么了:这些类型参数告诉编译器在trait的方法中期待什么类型。现在编译器知道我们的实现正确地使用了 connect()。

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
rust复制代码trait Connect<Id, F>
where
F: Fn(Point<Id>, Point<Id>) -> Line<Id>,
{
fn connect(self, op: F) -> Connector<Id, F>;
}

impl<Id, F> Connect<Id, F> for Vec<Point<Id>>
where
F: Fn(Point<Id>, Point<Id>) -> Line<Id>,
{
fn connect(self, op: F) -> Connector<Id, F> {
Connector::new(self, op)
}
}

fn main() {
let points = vec![Point { id: 1001 }, Point { id: 1002 }];
let mut connector = points.connect(|left, right| Line {
points: (left, right),
});

print!(
"This should be a line: {:?}.\nThis should be nothing: {:?}.",
connector.next(),
connector.next()
)
}

现在我们将完成编写我们的连接器的迭代器。

我们将通过在另一个迭代器(不仅仅是一个Vec)上调用connect来创建它。让我们对标准库中的类似类型做一些观察,并将这些观察应用到我们的代码中。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…396397398…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%