代码手工艺人

Joey 写字的地方

最近一年读了不少书和杂志,有电子版也有实体书,收获还是蛮多的,主要偏技术一些,希望 23 年能扩宽一下阅读范围。

技术书籍

技术书籍偏 C++ 以及一些底层的技术,英文为主。 选英文版主要原因有两个吧,一是有些书没有中文版,即便有,翻译的也很晦涩难懂,我倒不怪罪译者的水平,技术书籍确实比较难翻译得平易近人。(打比方说我比较敬仰的 C++ 骨灰级程序员 侯捷老师 的技术水平肯定是一流的,但是翻译的书也是很晦涩,可读性比较..)

  1. Effective Modern C++

    Scott Meyer 著作,每次读都会有新的收获,技术点讲的非常的细,比如关于 std::move 和 universal reference 就花了一章来介绍,各种想不到的 case. C++ 真的是了解的越多,就发现不了解的更多。 建议阅读英文版。

    Read more »

Blog 此前一直是跑在自己的东京服务器上,这个服务器上跑着我的 blog 以及一些自用的服务,因为更新并不频繁,所以直接起了本地的 hexo server,然后 nginx 反向代理一下,当然还反代了其他的几个服务。

但是最近考虑把服务器给退掉,所以 blog 的托管就成了一个问题。简单做了下调研,国内的云厂商基本都有,但是麻烦的是域名和备案。做了一些调研,最终考虑托管到 Cloudflare Pages 上,有以下几个优势:

Read more »

上篇文章 「Address Sanitizer 基本原理介绍及案例分析」里我们简单地介绍了一下 Address Sanitizer 基础的工作原理,这里我们再继续深挖一下深层次的原理。

从上篇文章中我们也了解到,对一个内存地址的操作:

1
2
*address = ...;  // 写操作
... = *address; // 读操作
Read more »

Address Sanitizer 介绍

LLVM 提供了一系列的工具帮助 C/C++/Objc/Objc++ 开发者检查代码中可能的潜在问题,这些工具包括 Address Sanitizer,Memory Sanitizer,Thread Sanitizer,XRay 等等,功能各异。

本篇主要介绍可能是最常用的一个工具 Address Sanitizer,它的主要作用是帮助开发者在运行时检测出内存地址访问的问题,比如访问了释放的内存,内存访问越界等。

全部种类如下,也都是非常常见的几类内存访问问题。

Read more »

C++ 11 引入 lambda 之后,可以很方便地在 C++ 中使用匿名函数,这篇文章主要聊聊其背后的实现原理以及有反直觉的变量捕获机制。在阅读本文之前,需要读者对 C++ lambda 有一个简单的了解。

C++ Lambda 的函数结构

1
[capture_list](parameter_list) -> return_type {function_body}

其中,capture_list 表示捕获列表,parameter_list 表示函数参数列表,return_type 表示函数返回类型,function_body 表示函数体。下面是一个简单的 Lambda 函数示例,这里定义一个计算面积的名为 area 的 lambda。

Read more »

我们在讨论 std::shared_ptr 线程安全时,讨论的是什么?

在讨论之前,我们先理清楚这样的一个简单但却容易混淆的逻辑。 std::shared_ptr 是个类模版,无法孤立存在的,因此实际使用中,我们都是使用他的具体模版类。这里使用 std::shared_ptr 来举例,我们讨论的时候,其实上是在讨论 std::shared_ptr 的线程安全性,并不是 SomeType 的线程安全性。

那我们在讨论某个操作是否线程安全的时候,也需要看具体的代码是作用在 std::shared_ptr 上,还是 SomeType 上。

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <memory>

struct SomeType {
void DoSomething() {
some_value++;
}

int some_value;
};

int main() {
std::shared_ptr<SomeType> ptr;
ptr->DoSomething();
return 0;
}

这里例子中,如果 ptr->DoSomething () 是运行在多线程中,讨论它是否线程安全,如何进行判断呢?

首先它可以展开为 ptr.operator->()->DoSomething(),拆分为两步:

  1. ptr.operator->() 这个是作用在 ptr 上,也就是 std::shared_ptr 上,因此要看 std::shared_ptr->() 是否线程安全,这个问题后面会详细来说
  2. ->DoSomething () 是作用在 SomeType* 上,因此要看 SomeType::DoSomething () 函数是否线程安全,这里显示是非线程安全的,因为对 some_value 的操作没有加锁,也没有使用 atomic 类型,多线程访问就出现未定义行为(UB)

std::shared_ptr 线程安全性

我们来看看 cppreference 里是怎么描述的:

All member functions (including copy constructor and copy assignment) can be called by multiple threads on different instances of shared_ptr without additional synchronization even if these instances are copies and share ownership of the same object.

If multiple threads of execution access the same instance of shared_ptr without synchronization and any of those accesses uses a non-const member function of shared_ptr then a data race will occur; the shared_ptr overloads of atomic functions can be used to prevent the data race.

我们可以得到下面的结论:

  1. 多线程环境中,对于持有相同裸指针的 std::shared_ptr 实例,所有成员函数的调用都是线程安全的。
    • 当然,对于不同的裸指针的 std::shared_ptr 实例,更是线程安全的
    • 这里的 “成员函数” 指的是 std::shared_ptr 的成员函数,比如 get ()、reset ()、 operrator->() 等)
  2. 多线程环境中,对于同一个 std::shared_ptr 实例,只有访问 const 的成员函数,才是线程安全的,对于非 const 成员函数,是非线程安全的,需要加锁访问。

首先来看一下 std::shared_ptr 的所有成员函数,只有前 3 个是 non-const 的,剩余的全是 const 的:

成员函数 是否 const
operator= non-const
reset non-const
swap non-const
get const
operator*、operator-> const
operator const
use_count const
unique(until C++20) const
operator bool const
owner_before const
use_count const

我们来看两个例子
例 1:

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
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
#include <atomic>
using namespace std;

struct SomeType {
void DoSomething() {
some_value++;
}

int some_value;
};

int main(int argc, char *argv[]) {
auto test = std::make_shared<SomeType>();
std::vector<std::thread> operations;
for (int i = 0; i < 10000; i++) {
std::thread([=]() mutable { //<<--
auto n = std::make_shared<SomeType>();
test.swap(n);
}).detach();
}

using namespace std::literals::chrono_literals;
std::this_thread::sleep_for(5s);
return 0;
}

例 2:

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
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
#include <atomic>
using namespace std;

struct SomeType {
void DoSomething() {
some_value++;
}

int some_value;
};

int main(int argc, char *argv[]) {
auto test = std::make_shared<SomeType>();
std::vector<std::thread> operations;
for (int i = 0; i < 10000; i++) {
std::thread([&]() mutable { // <<---
auto n = std::make_shared<SomeType>();
test.swap(n);
}).detach();
}

using namespace std::literals::chrono_literals;
std::this_thread::sleep_for(5s);
return 0;
}

这两个的区别只有传入到 std::thread 的 lambda 的捕获类型,一个是 capture by copy, 后者是 capture by reference,哪个会有线程安全问题呢?

根据刚才的两个结论,显然例 1 是没有问题的,因为每个 thread 对象都有一份 test 的 copy,因此访问任意成员函数都是线程安全的。 例 2 是有数据竞争存在的,因为所有 thread 都共享了同一个 test 的引用,根据刚才的结论 2,对于同一个 std::shared_ptr 对象,多线程访问 non-const 的函数是非线程安全的。
这个的 swap 改为 reset 也一样是非线程安全的,但如果改为 get () 就是线程安全的。

这里我们打开 Thread Sanitizer 编译例 2(clang 下是 -fsanitize=thread 参数),运行就会 crash 并告诉我们出现数据竞争的地方。

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
==================
WARNING: ThreadSanitizer: data race (pid=11868)
Read of size 8 at 0x00016ba5f110 by thread T2:
#0 std::__1::enable_if<(is_move_constructible<SomeType*>::value) && (is_move_assignable<SomeType*>::value), void>::type std::__1::swap<SomeType*>(SomeType*&, SomeType*&) swap.h:38 (Untitled 4:arm64+0x1000061a8)
#1 std::__1::shared_ptr<SomeType>::swap(std::__1::shared_ptr<SomeType>&) shared_ptr.h:1045 (Untitled 4:arm64+0x100006140)
#2 main::$_0::operator()() Untitled 4.cpp:22 (Untitled 4:arm64+0x1000060d4)
#3 decltype(static_cast<main::$_0>(fp)()) std::__1::__invoke<main::$_0>(main::$_0&&) type_traits:3918 (Untitled 4:arm64+0x100005fc8)
#4 void std::__1::__thread_execute<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, main::$_0>(std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, main::$_0>&, std::__1::__tuple_indices<>) thread:287 (Untitled 4:arm64+0x100005ec4)
#5 void* std::__1::__thread_proxy<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, main::$_0> >(void*) thread:298 (Untitled 4:arm64+0x100004f90)

Previous write of size 8 at 0x00016ba5f110 by thread T1:
#0 std::__1::enable_if<(is_move_constructible<SomeType*>::value) && (is_move_assignable<SomeType*>::value), void>::type std::__1::swap<SomeType*>(SomeType*&, SomeType*&) swap.h:39 (Untitled 4:arm64+0x1000061f0)
#1 std::__1::shared_ptr<SomeType>::swap(std::__1::shared_ptr<SomeType>&) shared_ptr.h:1045 (Untitled 4:arm64+0x100006140)
#2 main::$_0::operator()() Untitled 4.cpp:22 (Untitled 4:arm64+0x1000060d4)
#3 decltype(static_cast<main::$_0>(fp)()) std::__1::__invoke<main::$_0>(main::$_0&&) type_traits:3918 (Untitled 4:arm64+0x100005fc8)
#4 void std::__1::__thread_execute<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, main::$_0>(std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, main::$_0>&, std::__1::__tuple_indices<>) thread:287 (Untitled 4:arm64+0x100005ec4)
#5 void* std::__1::__thread_proxy<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, main::$_0> >(void*) thread:298 (Untitled 4:arm64+0x100004f90)
...

SUMMARY: ThreadSanitizer: data race swap.h:38 in std::__1::enable_if<(is_move_constructible<SomeType*>::value) && (is_move_assignable<SomeType*>::value), void>::type std::__1::swap<SomeType*>(SomeType*&, SomeType*&)

...

ThreadSanitizer: reported 4 warnings
Terminated due to signal: ABORT TRAP (6)

从错误信息中可以清晰地看到出现的数据竞争,在 22 行,也就是调用 swap () 的行。
如果确实需要在多线程环境下对同一 std::shared_ptr 实例做 swap () 操作,可以调用 atomic 对 std::shared_ptr 的重载函数,如:

1
2
3
template< class T >
std::shared_ptr<T> atomic_exchange( std::shared_ptr<T>* p,
std::shared_ptr<T> r);

C++ 中使用 std::shared_ptr 智能指针不当有可能会造成循环引用,因为 std::shared_ptr 内部是基于引用计数来实现的, 当引用计数为 0 时,就会释放内部持有的裸指针。但是当 a 持有 b, b 也持有 a 时,相当于 a 和 b 的引用计数都至少为 1,因此得不到释放,RAII 此时也无能为力。这时就需要使用 weak_ptr 来打破循环引用。

通过 weak_ptr 来避免循环引用

来看一个比较典型的 delegate/observer 的场景:

Read more »

本系列的第一篇 中介绍了 AArch64 的基础指令、进程内存布局以及基础栈操作 等。本文该系列的第二篇,主要聊聊函数调用,涉及到的就是 Function Call Convention. 初衷还是尽可能 “浅入深出” 地 got 到语言背后的本质,这不是一个手册,所以不是完备的.

1. 我们在聊函数调用的时候在聊什么?

至少我们应该把函数调用的几个问题搞清楚:

  1. 函数在汇编层是怎么调用的,本质是什么?
  2. 函数的参数怎么传?
  3. 返回值写到哪里?怎么传给 caller?
  4. 调用完之后,怎么返回到原来的位置?

Function Call Convention 其实就是回答这些问题的,接下里我们一一找到答案.

1.1. 函数调用本质是什么?

Read more »

1. 什么是 ARM?

正式开始之前,我们先来了解一下什么是 ARM, 以及对应的一些概念.

Wikipedia 上是这么介绍 ARM 的:

ARM (stylised in lowercase as arm, formerly an acronym for Advanced RISC Machines and originally Acorn RISC Machine) is a family of reduced instruction set computer (RISC) instruction set architectures for computer processors, configured for various environments.

ARM 是 高级 - RISC (精简指令集)- 机器 的缩写,是精简指令集架构的家族。同时 Arm Ltd. 也是开发和设计、授权这项技术的公司名称.

1.1. 有哪些指令集架构呢?(TRDR, 可跳过)

目前用的比较多的架构是 ARMv7 和 ARMv8, 这两个名字各自都是一个系列.

在 ARMv7 以及之前都是最多支持 32 位架构 (更早还有 16 位,甚至更低), 那么 32 位架构对应的 ISA 也就是指令集称为 A32. 32 位下指令的地址空间最大只有 4GB, 苹果系列的代表是 iPhone 4 使用的 A4 芯片,以及 iPhone 4s 使用的 A5 芯片.

Read more »
0%