V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX  ›  kuanat  ›  全部回复第 9 页 / 共 14 页
回复总数  262
1  2  3  4  5  6  7  8  9  10 ... 14  
298 天前
回复了 rockyliang 创建的主题 Go 编程语言 关于 golang 官网一段代码的疑惑
我来尝试用最直白的方式解释一下吧,

原文 The Go Memory Model 是严谨准确的,只是涉及到专业术语,没有相应的理论背景很难理解。

第一个容易误解的概念是 guaranteed 。编程语言定义规范( spec ),编译器负责在目标平台上完成实现。保证( guaranteed )的意思就是,在满足特定条件的情况下,无论编译器如何完成,都要确保最终的结果符预期。

看上去和什么都没说一样对吧?结合相反的概念就明白了,这个相反的概念叫 undefined 。也就是 C 时代大名鼎鼎的 undefined behavior 那个 undefined 。因为编程语言没明确要求,编译器想怎么干就怎么干,也可以什么都不干。

所以 guaranteed/undefined 的实质区别就是,前者以确定的方式干了点什么,从而保证了结果和预期的一致性。至于具体干了什么后面再说。

接下来的概念叫内存模型( memory model ),这是一组对规范( spec )的抽象而形成的规则。毕竟代码写起来可以五花八门,但内在逻辑是相似且有限的。但是如何用形式化( formal )的方式来精准定义这些逻辑呢?

从最简单的例子开始,假如只有单线程,那么执行顺序就是代码的书写顺序。如果是多线程呢?(这里还是取最简单的情形来简化分析)

思考一下就会发现,不确定性的来源主要是两类:一类是对于同一(共享)变量(内存)的读/写并行操作,另一类是写/写操作。这些的潜在冲突被叫做竞态( race )。

设想最简单的读写冲突场景,读和写操作分别位于两个独立的并行执行序列中。如果预期的结果是先读后写,那什么都不用做,这两个指令不会相互影响;但是反过来,预期先写后读,且要保证( guaranteed )读到写的结果,就需要编译器做点什么了。

这里“读到写的结果”有个专门的名词叫可见性( visibility ),编译器具体怎么做不重要,重要的是编译器做了一件事,在写操作完成读操作发生之前,使得写操作的结果对于读操作可见。(要么共享一份副本,要么两份独立的副本但同步成为相同的结果)

最后的任务就是,编程语言在规范( spec )中定义,在什么情况下编译器需要保证可见性( visibility )。即判断开发者按照直觉写出的代码,它的预期执行逻辑应该是什么样的。

这背后的代码逻辑关系被定义成了一个特殊的术语 happens-before ,满足 w(rite) happens-before r(ead) 的情形下,就要保证 w 的结果对 r 可见。这里其实是个反向的过程,既然某些代码和逻辑按照开发者的预期应该满足可见性,那就定义它要满足 happens-before 关系。(这里为了方便理解,对定义进行了简化)

这是最容易被误解的术语,它和指令执行的先后顺序无关,不是时间上的先后概念。(关于这一点如果不理解,建议从基础开始学习内存模型理论。)编译器的工作从来都不是对指令执行流程进行排序,只要能保证可见性,指令顺序是不影响预期结果的。

这个逻辑关系比较重要的特性是它具有传递性。我这里用 A <= B 代表 A happens-before B ,那么 A <= B 且 B <= C 可以推导出 A <= C 。另一点比较重要的是,happens-before 强制要求编译器保证可见性,但是可见性并不能反推出 happens-before 关系。

最终的结果就是,Golang 的设计者希望,当开发者用 XXX 的方式写代码的时候,编译器能产生符合预期的结果。于是就定义 XXX 这些写法,就是 happens-before 关系,编译器必须对其可见性结果做出保证。Golang 规范中定义了几种 happens-before 关系,这里就不一一列举了。

--------手动分割线--------

如果重新表述一下示例代码的问题,就是判断 [1] `a = "hello, world"` 和 [4] `print(a)` 是否满足 happens-before 关系,[2] `done = true` 和 [3] `for !done {}` 是否满足 happens-before 关系,从而判定可见性。

很遗憾根据 Golang 的规范,这里仅有 [1] <= [2] 和 [3] <= [4],不存在其他 happens-before 关系。(读 The Go Memory Model 原文可以看到采用 channel 通信后,比如把 [2] 和 [3] 分别替换为 `c <- 0` 和 `<-c`,由于语言规范层面上建立了两个 goroutine 相关指令间的 happens-before 关系,有 [2] <= [3],从而依靠传递性保证了 [1] <= [4],于是保证了不会有死循环,赋值也总能得到正确的输出)

既然不存在 happens-before 关系,那么编译器就没有必要保证 `done = true` 的结果对于 `for !done {}` 可见。缺少了这一层保证,那编译器的某种实现使得 `for !done {}` 一直读到 `done = true` 可见之前的结果也就不奇怪了。因为 `done = true` 对于 `done` 的操作可能完全不会让 `for !done {}` 知道。

--------手动分割线--------

看到这里你肯定会感到疑惑,为什么 Golang 不对前面这种情况做精确定义?

我的理解是,这个思路 C 当年用过了,然后发现太复杂且不符合直觉的情形太多,于是抽象了诸如 mutex/semaphore 等等原语来降低开发者的心智负担,帮助写出正确的代码。结果并没有那么理想,理解这个背景对于大多数非科班的开发者要求还是太高了。Golang 走相同的路不会有更好的结果。

Golang 的设计目标中很重要的一点是简单。希望即便开发者完全不懂内存模型,也能凭直觉写出正确的并行代码。原文的示例非常明显了,不要用 C 的思路去写并行代码,而是用 Golang 提供的 channel 机制,新机制有着严格且准确的 happens-before 逻辑,同时符合直觉。
用 rust 重新实现一遍很不错,手动点赞。

只是 https://github.com/mitmproxy/mitmproxy 已经很成熟了,用的是 python 开发所以很方便直接用 python 脚本接管请求处理。不清楚 rust 要怎么支持一个脚本引擎来做类似的工作比较合适。
春节期间我把开源可以部署的本地 LLM 都试了一下,没有量化单纯体感,7B 这个参数尺度上,DeepSeek Coder 效果是最好的。
能做成网络请求的方式是最好的,即使是内网也可以临时用穿透方式来测试。

如果因为实时性或者与其他模块集成没办法,那就编译成 pyd/so ,如果是 so 的话记得移除符号表,pyd 应该是默认移除的。

做一点简单的限时逻辑,不要有明显的特征,尽量以 silent crash 的形式来处理。这样一般防老板是够用了。有应对措施的大概率就能自己做了。
301 天前
回复了 zhoudaiyu 创建的主题 Linux 各位公司生产环境用的是什么版本的内核?
既然有选择的空间,那就肯定不是类似于嵌入式环境定制这样的场景了。

这个问题不如反过来思考,什么时候会考虑换内核?

因为绝大多数情况下,选择内核版本是在选择发行版之后的事情,也就是说几乎不会主动去换内核。

- 某个版本内核爆出严重安全漏洞

- 需要调节某个编译期配置参数(开启功能支持或性能优化)

- 闭源驱动模块限制内核版本

一旦限制了内核版本可选范围,那么就尽量选择改范围内的 lts 版本,然后就基本保持不变了。版本本质上是内核功能集合的数值化表述,不需要新功能(或者性能优化)就不需要更新。

目前还在维护的 lts 版本有 4.19/5.4/5.10/... 等版本,基本上应该满足各种需求了。使用 lts 版本的原因是,出现严重漏洞的时候,相关补丁也会 backport 到所有 lts 版本上。

另外发行版一般不会直接使用某版本 mainline 内核,而是有自己的一套补丁,用以配合自家发行版的默认功能。但是发行版的版本基本上会匹配同版本号的 mainline 版本,所以还是建议尽量选择严肃的发行版,以便尽快获得安全补丁。


------手动分割线------

尽管 CentOS 已经停止维护很久了,但还是有人会因为各种原因选择它。对于这种情况,不仅仅要关注内核问题,还要关注 glib 等版本的问题。

相应的开发、测试或者 staging 环境几乎都要退化到比较低的版本。比较新的版本就不存在这些问题。
我提一个可能会被遗漏的细节,微服务环境里做日志,一定要有全链路的 trace 信息,不然出了问题也很难还原出错的场景。

前面有人讲了读写分离,写基本都是本地,然后通过后台 daemon 定期汇总到日志服务器上。

原始日志汇总存储之后,建议再定期根据 trace 重新组织一下(比如数据库视图,或者直接用文档类型存储)以方便查询。一般来说,日志信息往往会比生产数据的容量还大,如果用到了再去查可能会很慢。
301 天前
回复了 onichandame 创建的主题 程序员 local first 应用前景咋样
某种程度上,现在软件商业化形式是和 local first 理念相悖的,所以做这个的很少。

从另一个角度,我个人认为开发 local first 应用的技术难度也很大,笼统地说实现 local first 就面临分布式系统的同步问题。这个问题没有最优解,只有妥协,当你追求用户体验的时候,留给你的技术选择就非常少了。
paddleocr 内存分配管理有问题是个很长时间的问题了。我印象官方 github 有几个 issues 就是讨论这个的,而且跨了多个版本,你可以去查一下。

之前有说是因为框架缓存的原因,有人说不是。我之前遇到这个问题也是通过重启来解决,尝试读了一下 c++ 代码部分还是太复杂了,没有解决的精力和能力。
306 天前
回复了 sofukwird 创建的主题 程序员 浏览器爬虫再进化
如果我没理解错的话,这个实现的是挺厉害的,只是应用场景非常受限。

从功能上说,相当于被注入的页面向外提供 http 代理,该代理会复用浏览器环境,也就继承了相关 cookies 。

使用这个方案,相比基于 webdriver/cdp 的方式,可以省去提取 cookies 、模拟登录的过程,通过人工在图形界面上操作一次即可。

比较容易想到的局限在于:

- 网站有多处或者基于访问频率的人机检测,就需要大量人工介入(甚至难以介入,因为通过代理触发的人机检测并不会显示在浏览器界面上)

- 目标接口需要额外访问参数,一般反爬措施都会利用 vmp 混淆 js 代码来生成可以被服务器后端验证的动态参数

如果我说错了还请 OP 指正一下,因为我确实想不到更好的应用场景了。
字体渲染这个话题一两句话说不清楚,我自己的笔记大概有五六千字,如果有空闲我会考虑专门发个帖子。

这里回复主要是想表达一个观点:苹果的渲染效果是可以在 Linux 环境近似复制的。

我这里举两个例子,需要配合 #23 @agagega 的截图对比来看。

由于我这里没有苹方字体,不过看过效果之后就知道不影响了。我这里的环境是 Gnome+Firefox 200% 缩放。

第一张是我日常的效果

https://i.imgur.com/E7QDq7p.png

第二张是贴近苹果的效果(通过调整参数还可以更像)

https://i.imgur.com/3FNIPYO.png



----------手动分割线----------

Windows 那个就不做对比了,我说下 #23 macOS/KDE 和我这里的区别。

首先忽略标题里粗体,那个是字重的关系,其次忽略英文和数字,因为它们 fallback 到了不同的字体上。

如果你把图片保存下来放大看,那么你会发现,KDE 的字体渲染是有彩色边缘的,说明它使用了 subpixel 抗锯齿。而 macOS 实际上是用的 grayscale 抗锯齿。

接下来关注标题下面“XX 小时 XX 分钟”的那个“时”字,苹果系统那个“时”和日常里写法是不太一致的,右边“寸”的横非常靠下,而 KDE 版本就比较正常。这是因为 macOS 的渲染引擎会无视 hinting ,而 KDE 依赖的底层 FreeType 在 KDE 的默认值是 Slight Hinting ,当苹方字体本身没有 hinting 信息时,会按照算法在垂直方向上做自动 hinting 修正。

所以 Linux 环境里,如果通过配置 fontconfig ,设置 hinting=false hintstyle=hintnone ,基本上可以获得接近 90% 的苹果效果。也就是我这里图一的例子。

如果仔细看还是会发现,苹果的渲染标题下面“XX 小时 XX 分钟前”里面的汉字,明显要颜色偏深一些。实际上,如果设置了暗色模式,这个差别会更明显。

这里涉及到一个叫 stem darkening 的概念,在“正确”渲染的前提下,额外对灰度进行加深,以匹配人类视觉的非线性感知。

所谓“正确”是指,通过 alpha composing 将字符前景与背景合成,这个计算是在线性空间里完成,然后通过 gamma 矫正转换到输出空间。这个过程在 Win/macOS 上都是默认支持的,然而 Linux 缺少 gamma 矫正这一步。

可以通过 FreeType 的参数来强行启用 stem darkening ,就如我第二张图的效果。实际效果是轻微的加粗,对于灰度显示的部分影响比较明显。只是从“反映字体设计意图”的角度上说,这个渲染逻辑是不正确的。但考虑到追求渲染效果往往是因为设备 DPI 不够,所以正确与否反倒不那么重要了。
`Run or raise`
https://github.com/CZ-NIC/run-or-raise
https://extensions.gnome.org/extension/1336/run-or-raise/

PS
不依赖扩展的方式就是楼上说的 Super+数字,上面的插件里也提到了。

再 PS
tiling wm 环境里这个功能非常好实现,可以借鉴一下思路。绑定快捷键就用 Gnome 自带的 Keyboard shortcuts ,执行一个 bash -c "command" 命令。这个命令脚本用来判定运行程序和判定焦点,然后通过某个接口去操作窗口管理器。X11 的话,wmctrl 就可以。Wayland 需要通过 D-Bus 调用。
318 天前
回复了 flx413 创建的主题 程序员 QUIC 实践疑问
基于你这个描述,我提供一个方向,可能和我之前遇到的丢包问题是一样的。

并发加载资源的时候,NWListener 用单个 NWConection 连接会丢包,用多个 NWConnection 来处理,有可能就正常了。

一般化的测试方法是用 quic-interop-runner 来跑一下,看看是不是协议不同实现的问题。估计你用的服务端实现肯定在列表里,就是需要额外写个 ios 的客户端。

如果要定量测的话,可以配合 wireshark/mitmproxy ,这俩新版本都可以简单处理 quic 协议了。
Go 的 context 是 1.7 版本引入给 net/http 服务的,用来解决信号和取消问题,传 value 只是顺带的,同时特别强调了线程安全的问题。名字用了 context 但是语义上确实只有上文。所以当你真正需要上下文的时候 context 包是不够的。

一般中间件解决这个问题的思路是自定义 context ,其实我不太喜欢 gin 的方式,我个人的偏好是类似

```
type MyContext struct {
ctx context.Context
// custom field
key string
}
```
这样的方式。然后实现 Context 的接口方法,写几个 wrapper 就可以完成对 context.Context 的兼容,不影响原本 net/http 的信号取消机制。

剩下的就是语法层面的封装了,需要实现一组方法,比如从 context.Context 衍生出子 MyContext:
```
func DeriveMyContext(ctx context.Context) *MyContext {
myCtx, _ := ctx.Value(MyCtxKey).(*MyContext)
return myCtx
}
```
此处用接口断言是根据 context 的设计,value 通过自定义类型模拟命名空间,防止 key 冲突。

结合起来就是 `context.WithValue(context.Background(), key, value)` 中的 kv 对,实际上就是通过 context.Value 传递了一个特定的 key ,这个 key 等价于指向 MyContext 的指针,和你的思路是一致的。

这样中间件所有涉及的 context 都通过一个 MyContext 的结构共享上下文,如果涉及到多线程可以加 Mutex 锁。

反正 Go 在传递 context 这件事上已经一条道走到黑了,比如 1.21 标准化的 slog 日志库也可以接受 context ,稍微封装下也可以直接用。
2024-01-15 17:17:45 +08:00
回复了 bocchi1amos 创建的主题 Python 为什么 Python 会有.venv 虚拟环境的概念?
@cnt2ex #63

我的描述是基于自己的记忆和理解,不一定就是正确的。

我原本是想要通过 __all__ 这个例子来体现,基于文件系统的 import path 对今天包管理机制造成的困扰。

在当时 windows/dos/Macintosh 的文件系统上,import * 遍历出来的文件名是大小写不固定的,也就导致了引入的符号表大小写不固定。于是设计了 __all__ 机制,因为 __all__ 定义的大小写是可控的。开发者可以选择 opt in 。

__all__ 机制是需要手动维护的,也不能阻止用户显示地手动引入。如果今天重新设计一门语言,__all__ 是完全不必要的,可以通过 public/private 关键词,也可以通过某种 name convention 来实现。

并不是说基于文件系统的 Package 组织形式不好,而是说这是一个选择。Python 选择了简洁,但是要在别的地方(包管理)付出代价。__all__ 机制就是这个代价之一。


回到虚拟环境的问题上,现在假想一下 Python 依旧使用基于文件系统的 Package 组织形式,但是在路径名中包含版本号。

现在 Python 就面临几种选择,要么以 import xyz==1.1.1 的形式使用,要么 Python 自己实现一个版本选择机制然后维持 import xyz 的向后兼容。

第一种方案几乎没有人选的。因为一旦要升级依赖,需要显示地在源代码里面做修改。但是这个方案的改版非常多见,比如 npm 的 package.json 就是集中把 xyz==1.1.1 放到了一起。

(看起来很完美对吧,但是当时没有人意识到这个 NP 问题会成为日后的麻烦。这种组织方式从机制上就不允许引入同一个包的不同版本,所以 NP 问题退化为 P 问题的途径就少了一个。)

第二种方案,考虑到时间节点,不是一个好的选择。但这个路线是近十年来以软件工程为导向的语言的首选。

这里需要明确区分 Package 的组织形式和 import path 的实现,前者是一种 specification 规范,后者是一个实现方式。Python 完全可以从规范层面要求所有的包名都带版本号,然后 import hook 的实现无视它,固定使用版本号最大或者最小,甚至文件最后修改信息最新或者最旧的版本。


虚拟环境解决的是:在 Package 组织形式是基于文件系统、且 Package 规范不包含版本号的前提下,提供一种在同一个系统中,安装同一个包多个不同版本的可能性。

如果 Package 规范要求包名带版本号,那么“安装”这个行为是不受限制的,也就不需要虚拟环境了。至于安装好了之后,使用时“选择“哪个版本,这是另一个事情。

技术层面上,我猜 Python 转向 npm 类似的包管理方式是没有太大难度的,PEP 621 规范了 pyproject.toml ,也就是前面说的方案一,楼上 frostming 开发的 PDM 就是这么做的。现在的问题是 Python 的 import hook 本身并不支持路径名带版本号的,所以还是需要再套一层虚拟环境,让 Python 只能访问到特定的包。

说得再具体一点,包管理器在文件系统里,以包名+版本号的形式,集中管理所有的包。虚拟环境指定了 $PATH ,在当前环境中的包软链接到实际的带版本号的包,然后软连接的名字不包含版本号。包管理器额外维护一个 pyproject.toml 记录当前所用的包,同时负责计算依赖。在此环境内的 Python 解释器和源码都不需要做任何改变,完全向后兼容。
2024-01-13 20:18:19 +08:00
回复了 bocchi1amos 创建的主题 Python 为什么 Python 会有.venv 虚拟环境的概念?
@flyingghost 60

我没有听说过类似的方案,如果让我评论的话,我认为两个方向都不合适,一个是结合了版本号之后命中率很

另一个方面,一个包 A 如果同时依赖 B 和 C ,我猜测不存在一个多项式算法,可以从 B 和 C 依赖计算出 A 的依赖。(需要 backtrack 到所有依赖重新计算)导致这样的缓存中根本上不可用。
2024-01-13 16:39:08 +08:00
回复了 bthulu 创建的主题 Vim 各位用 vim 键位的, 是怎么解决 esc 频繁按键的问题的呢?
我的映射是 capslock 短按是 esc 长按是 ctrl 。
2024-01-13 16:36:11 +08:00
回复了 bocchi1amos 创建的主题 Python 为什么 Python 会有.venv 虚拟环境的概念?
@cnt2ex #57

这里我说得太简略了。

在设计这个 import 机制的 1.x 版本,要实现 import * 需要去文件系统里遍历有哪些模块。底层文件系统可能会对某个名为 xyz 的模块,返回 XYZ/Xyz/xyz 几个不同的结果。当时对于这个问题的解决方案是设计 __all__ 让维护者自己声明是哪一个。

之后的 PEP 才明确了模块名应该( should )全小写,import 的所有符号都是小写。再之后 __all__ 才成为一种工程上的控制机制。
2024-01-13 01:33:03 +08:00
回复了 Goooooos 创建的主题 Java 吐槽下 Google 开源的组件
@Knife42 #8

自我定位是 long time lurker 所以没有什么社交账号。

v2ex 发帖的初衷是想在 AI 生成内容的时代留下一点有价值的东西。
2024-01-12 17:29:07 +08:00
回复了 bocchi1amos 创建的主题 Python 为什么 Python 会有.venv 虚拟环境的概念?
@frostming #54

我又看了一遍自己写的东西,其实不太准确,我这重新描述一下。

版本选择之所以是 NP 问题,源于好几个假设。其中有几个假设是没法改变的,能够改变的还有两个:

- 一个包可以声明自己依赖零个或多个特定版本的包。
- 不允许同时选择同一个包的两个不同版本。

第一条过于严格了。如果把特定版本换成一个范围,这个问题就简单多了。第二条也可以适当放宽,和第一条一样,都是建立在 semantic versioning 主动向后兼容的假设之上。虽然不能引入同一个包的 1.X.X 和 1.Y.Y ,但是可以同时引入 1.X.X 和 2.Z.Z 。

这两个条件放宽之后,NP 问题就不存在了。但是对 APT/RPM/Node/Python 这些起步较早的社区来说,这两个要求依然太难了。

至于主包覆盖依赖的限制,这是用来解决菱形依赖的,不是规避 NP 问题的核心措施。还是之前的例子,只要保证 C 符合 semantic versioning 向后兼容就好了。这个措施实际体现的是解决问题的思路转变。

具体的来源比较分散记不清了,我凭印象做个总结。第一个做 NP 规避的应该是 Rust/Cargo 。之后 Go 做了一个叫 Dep 的类 Cargo 实现,实践了一段时间,吸取经验教训,重做之后形成了正式的 Go mod 方案。Rust/Go 用相同的方法解决菱形依赖,但是基础思路是不一样的。这一点从 Rust 总是选择最高版本,而 Go 总是选择最低版本能看出来。

这里想象一个场景:A==1.1.0 依赖 B==1.5.0 ,之后 B 发布 B==1.6.0 ,由于 B 引入了非兼容改变,导致 A==1.1.0 无法依赖 B==1.6.0 构建。这个时间节点,按照 Rust 的设计,所有依赖 A 的用户(包括依赖 A 的老用户)都会受到影响,而 Go 这边只有同时依赖 A==1.1.0 和 B==1.6.0 的新用户(老用户不受影响)才会受影响。

之后 A/B 至少有一方要打补丁发新版。Rust 认为要么 A 在 B 没有提供补丁的情况下,主动发新版声明不兼容 B==1.6.0 ,要么 B 发新版修复对 A 的支持。实际上在开源社区这两个事情都是很难的。Go 的设计是除非用户主动升级,都会保持作者发布时的最低依赖版本,(毕竟发布那个时刻的版本依赖几乎是都可以构建的)这样就给 A/B 争取了非常多的修复时间。

Rust 的设计者更希望为开发者提供最好的体验,希望一己之力解决所有问题。Go 的设计思路是依靠社区合作,依赖所有人主动去帮助解决对自己来说比较容易,而别人不好解决的问题。所以你能看到,Go 官方一直不遗余力地推动向后兼容,因为 Go 的整套实现逻辑都强依赖整个社区对于兼容性规范的共识。



最后赞美 pnpm ,感谢!
2024-01-12 16:19:25 +08:00
回复了 hankli 创建的主题 程序员 试一下用 VersionFox 替代 asdf-vm?
@hankli #7

我一开始没注意到 VersionFox 是跨全平台的。为了兼容 Windows/PS 的话,确实单可执行文件比脚本靠谱。(我觉得理智一点的开发者,在 Windows 环境不用 WSL ,也应该用 MSYS2 吧哈哈)
1  2  3  4  5  6  7  8  9  10 ... 14  
关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1955 人在线   最高记录 6679   ·     Select Language
创意工作者们的社区
World is powered by solitude
VERSION: 3.9.8.5 · 38ms · UTC 12:02 · PVG 20:02 · LAX 04:02 · JFK 07:02
Developed with CodeLauncher
♥ Do have faith in what you're doing.