0%

说明

在我写这个文章摘要代码片段的时候,Hugo 还没有支持按照元素截取,因此 summary 是纯文本、没有格式。

现在 Hugo 已经实现了带格式的截取。目前内置实现和我实现的区别是:内置实现是按照词数截取的,直到词数满足要求就停止添加新的元素,我的实现是按照顶级元素数量截取的(以前用 Hexo 插件有这个功能,因此我实现的也是这个)。

由于 Hugo 没有开放给 html 模板足够的功能,很多数据结构有点大材小用,Hugo 内置实现远远快于用 html 模板的实现。在我电脑上测试是快了好几倍,也就是说这个文章摘要功能拖累了渲染速度。

实现

themes/hugo-theme-next/layouts/partials/post/body.html 的基础上修改。原本内容类似于:

{{- with .ctx -}}
{{- if or (not $.IsHome) .Params.Expand -}}
  {{ .Content }}
{{- else -}}
  {{ .Summary }}
{{ end }}

这一章主要讲线程之间的同步和信息传递,包括条件变量(condition variable)、futures、latches/barries。

条件变量

头文件是 <condition_variable>。包含 std::condition_variablestd::condition_variable_any。前者只能在 std::mutex 上使用,后者可以在所有满足 BasicLockable(lock() + unlock(),不需要 try_lock())的类型上使用。如果只需要使用 std::mutex,那么就用前者,开销可能会比后者小一点。

Tip

接口只有一处区别:std::condition_variable 可以获取 native handle,而 std::condition_variable_any 不能。

条件变量可以用来协调生产者和消费者之间的关系。

典型操作流程:

三种避免竞争情况的办法

  1. 上锁。
  2. 无锁编程。通常通过修改数据结构和 invariants(数据结构要保持的约束)来完成。
  3. 事务(software transactional memory, STM)。

使用互斥量保护临界区

std::lock_guard(可以用 CTAD)和 std::mutex API。C++11 除了 std::mutex 之外还有 std::timed_mutexstd::recursive_mutexstd::recursive_timed_mutex

不要把受保护数据的引用传出锁的保护范围外,尤其是要注意不要把它的引用传入到来历不明的其他函数中。

案例:实现线程安全的栈(top()pop() 的原子化)

接口设计上的问题:栈的 API 中 empty() 和其他方法各自原子是不够的,因为 empty() 的结果在下一个方法运行时不一定还能成立。

我们也可能想把 pop()top() 两个函数结合,将它们合并成一个原子化的功能,但又会遇到异常安全的问题(元素的构造函数抛出异常时,被 pop 的数据会丢失)。

准确的说是 conda 环境的提示重复了两次,实际上是同一个环境 env1,但显示出来是 (env1) (env1)

解决方案:

  1. 关闭 python.terminal.activateEnvironment,然后重载工作区,防止自动加载。
  2. 或者关闭 conda 的自动激活:conda config --set auto_activate_base false

我觉得 2 更方便一点,因为 vscode 调试 python 程序还是需要自动激活的。

解决 docker-compose 启动的容器无法使用 GPU

最近有人升级了服务器的 docker,我们创建容器有点问题。症状是这样:在命令行用 --gpus all 参数启动的都能正常使用 GPU(可以通过运行 nvidia-smi 命令测试),但是在 docker-compose.yml 文件中指定要使用 GPU 就不行。

之前的 docker-compose.yml 文件是这样:

version: "3.9"
services:
  my-dev:
    container_name: ${CONTAINER_NAME}
    image: ${IMAGE}
    ## -D 表示非 daemon 模式,-e 表示将日志输出到 stderr
    ## 如果有错误信息,可以用 docker compose logs 看到
    command: /usr/sbin/sshd -D -e
    restart: always
    volumes:
      - ./workspace:/workspace
      - ./data:/data
    ports:
      - "${PORT}:22"
    working_dir: /workspace
    shm_size: '8gb'
    pid: "host"
    security_opt:
      - seccomp:unconfined
    cap_add:
      - SYS_PTRACE
    deploy:
      resources:
        reservations:
          devices:
            - capabilities: [gpu]

出错原因是 devices 一栏写的不全面,要加上 count 属性才行。可能以前的 count 默认值是 all但是现在实测不写就不行

起因

同学给出如下代码,指出 std::forward_as_tuple() 的返回值不能被 auto 结构化绑定:

#include <tuple>

int main() {
    auto t1 = std::tuple{0};
    auto [a0] = t1;                      // ok
    auto t2 = std::forward_as_tuple(0);
    auto [a1] = t2;                      // error
}

修改代码尝试了几次发现,t2 只是不能被 auto 结构化绑定,可以被 auto &auto &&(万能引用)结构化绑定。查阅资料得知 std::forward_as_tuple() 返回的 std::tuple 的参数类型都是完美转发后的类型,t2 的类型是 std::tuple<int &&> 而不是 std::tuple<int>

将代码修改如下,果然 t1 也无法被结构化绑定了。

#include <tuple>

int main() {
-   auto t1 = std::tuple{0};
-   auto [a0] = t1;                      // ok
+   auto t1 = std::tuple<int &&>{0};     // don't use CTAD
+   auto [a0] = t1;                      // error
    auto t2 = std::forward_as_tuple(0);
    auto [a1] = t2;                      // error
}

template <typename F>
void timed_execute(std::string_view tag, F &&f) {
    auto start = std::chrono::steady_clock::now();
    f();
    auto end = std::chrono::steady_clock::now();
    auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
    std::printf("%-20s: ", tag.data());
    if (ns >= 1e6) {
        std::printf("%.1fms\n", ns / 1e6);
    } else if (ns >= 1e3) {
        std::printf("%.1fus\n", ns / 1e3);
    } else {
        std::printf("%zdns\n", ns);
    }
}

用法:

timed_execute("reduce", [&] {
    auto result = reduce(v.begin(), v.end(), 0);
});

2024 年 10 月 2 日:其实 duration 的 count() 不一定非得是整数。参考 https://en.cppreference.com/w/cpp/chrono/duration ,可以自己定义数据类型为浮点数的 ratio(通用单位,在 std::chrono 中自然就表示时间单位,比如 std::milli 表示 1e-3 这个比例,而 std::chrono::milliseconds 真正表示毫秒这个时间段)。例子:

#include <chrono>
#include <iostream>
 
using namespace std::chrono_literals;
 
int main()
{
    using Minute = std::chrono::duration<double, std::ratio<60, 1>>;
    std::cout << std::chrono::duration_cast<Minute>(1s).count()
              << " minutes\n";
}
// 0.0166667 minutes

教程

https://www.bilibili.com/video/BV1jwHhebE8o/ 【系统设计需要知道的延迟等级】

https://github.com/rasbt/LLMs-from-scratch LLM 原理讲解,虽然印刷和电子书籍是要付费的,但是 ipynb 版本都是免费的

http://www.uinio.com/Linux/Vim/ Vim 速查,作者是嵌入式大牛,博客也很有意思

工具

https://github.com/chen08209/FlClash 又一个基于 Clash Core 的 GUI 代理软件

三个剪贴板

Windows 一个,X selection 一个,Wayland 一个。

安装 wl-clipboard 包可以获得 wl-copywl-paste 工具,它们操作 Wayland 剪贴板。

安装 xclip 可以获得 xclip 工具,xclipxclip -i 类似 wl-copy,完成复制功能,xclip -o 完成粘贴功能。它操作的是 X selection。

现象描述

在 Windows 上复制文本,可以写入 Wayland 剪贴板和 Windows 剪贴板,不能写入 X selection。

nvim 中选中文本,用 "+y 复制到 "+ 寄存器,可以写入 Wayland 剪贴板,不能写入 Windows 剪贴板和 X selection。

2.2 向线程传递参数

向线程传递参数时最好是都复制一份,而且转换成线程启动函数期望的类型。举例:

void foo(std::string) {
  // ...
}

std::thread launch_thread() {
  char buf[1024];
  return std::thread{ foo, buf };
}

这里 foo 的参数是 std::string 类型,而 std::thread 在构造时复制的是 char * 类型的参数。等线程创建好,真正开始执行的时候,复制过来的 buf 就可能已经是悬挂引用了。

其他:想要引用可以用 std::ref,想要使用一个对象的成员函数还需要额外传递对象的地址(this 指针)。

案例:并行 accumulate 的实现

下面 parallel_accumulate 这个方法使用的是书上的朴素实现,其他测试都是调用标准库。用 Release 模式编译测试,测试在我的笔记本上进行,结果仅供参考。