代码手工艺人

Joey 写字的地方

I recently went looking for an analytics tool for an app I’m developing. There are plenty of options out there, but most are either too heavyweight or raise privacy concerns. After some research, Aptabase caught my eye — it’s privacy-first, uses no unique user identifiers, fully complies with GDPR/CCPA, comes with a clean and intuitive dashboard, and offers over 10 SDKs covering most major frameworks. Best of all, it supports self-hosting, so your data stays entirely under your control.

The official self-hosting repository makes it look simple — just clone, tweak a few configs, run docker compose up -d, and you’re done. But the actual deployment process had quite a few gotchas, and many others in the Issues have run into similar problems. Here’s what I learned, hoping it saves you some trouble.

Read more »

最近在为自己开发的 App 寻找数据统计工具。市面上的选择不少,但大部分要么太重,要么在隐私方面让人不太放心。调研了一圈之后,Aptabase 吸引了我的注意 —— 它主打隐私优先,不使用任何用户唯一标识符,完全符合 GDPR、CCPA 等法规要求,而且自带的 Dashboard 简洁直观,提供了超过 10 种 SDK,基本覆盖了主流的开发框架。更重要的是,它支持 Self-Host,数据完全掌握在自己手里。

官方提供了 Self-Hosting 仓库,看起来很简单 ——clone 下来、改改配置、docker compose up -d 就完事了。但实际部署过程中还是踩了不少坑,Issue 里也有很多人遇到了类似的问题。这里把我的经历整理出来,希望能帮到后来人。

Read more »

最近一年读了不少书和杂志,有电子版也有实体书,收获还是蛮多的,主要偏技术一些,希望 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 »
0%