boost.fiber

说到进程、线程,大部分接触过程序的人可能都听说过,倒是 fiber(纤程)更少为人知一点,而且每个地方谈到这个概念的时候又会有一些细微的差别,所以我们这里仅仅尝试罗列一些特性,然后来看看到底有些什么部分大家是一致的。

概念

先看看 wikipedia 上面的说法

In computer science, a fiber is a particularly lightweight thread of execution.

Like threads, fibers share address space. However, fibers use cooperative multitasking while threads use preemptive multitasking. Threads often depend on the kernel’s thread scheduler to preempt a busy thread and resume another thread; fibers yield themselves to run another fiber while executing.

wikipedia

也可以去看看比如 boost.fiber 的文档,

Boost.Fiber provides a framework for micro-/userland-threads (fibers) scheduled cooperatively. The API contains classes and functions to manage and synchronize fibers similiarly to standard thread support library.

Each fiber has its own stack….

Control is cooperatively passed between fibers launched on a given thread. At a given moment, on a given thread, at most one fiber is running….

In effect, fibers provide a natural way to organize concurrent code based on asynchronous I/O. Instead of chaining together completion handlers, code running on a fiber can make what looks like a normal blocking function call. That call can cheaply suspend the calling fiber, allowing other fibers on the same thread to run. When the operation has completed, the suspended fiber resumes, without having to explicitly save or restore its state. Its local stack variables persist across the call.

Boost.Fiber

同一个作者也提交给标准委员会一个提议,这里面谈到

Because of name clashes with coroutine from coroutine TS, execution context from executor proposals and continuation used
in the context of future::then(), fiber, as suggested by some committee members, has been chosen – filament, tasklet,
lightfiber or coop_task are possible alternative names.

The minimal API enables stackful context switching without the need for a scheduler. The API is suitable to act as building-
block for high-level constructs such as stackful coroutines as well as cooperative multitasking (aka user-land/green threads
that incorporate a scheduling facility).

p0876r0

另外 Meta / Facebook 也提供了一个实现 folly::fibers

Fibers (or coroutines) are lightweight application threads. Multiple fibers can be running on top of a single system thread. Unlike system threads, all the context switching between fibers is happening explicitly. Because of this every such context switch is very fast (~200 million of fiber context switches can be made per second on a single CPU core).

folly::fibers implements a task manager (FiberManager), which executes scheduled tasks on fibers. It also provides some fiber-compatible synchronization primitives.

folly::fibers

我们这里小结一下:

  • fiber 是用户级别的 thread:线程的调度器一般是 OS 提供,同时是 preemptive(被动失去执行的 context);而纤程的调度不依赖 OS,同时是协作式(主动的提供位置允许其他纤程执行)
  • fiber 也许可以不需要 scheduler,但是 boost 也好、folly 也好都是提供了 scheduler,这使得他们可以与其他的消息系统整合
  • 在较低抽象层上,fiber 可以用来实现 coroutine(这更多的是从 programming construct 上定义的一种形式),也可以提供一套类似 thread 之间同步的工具帮助使用 fiber 的用户能更方便的写相关的程序
  • fiber 提供 concurrency,但是并不提供 parallelism;thread 提供两者(如果只有一个 core 就会退变成 concurrency):stackoverflow 有个有意思的比方,一个人需要在一天里面做两件事情,拿护照、做演讲稿,所谓的 concurrency 指的是多件任务可以在重叠的时间里开始、执行、完成,因此这个人这两件事可以先去发证机关排上队(可能要好几个小时),然后开始准备演讲稿,等到被人叫号的时候处理护照的事物,最后空闲下来继续处理演讲稿;而 parallelism 指的是让两个人去做这两件事
  • 这也说明,如果是一些 cpu bound 的任务,通常 parallel threads 可以增进不少效率;而如果是一些 io bound 的任务,比 thread 更有效的 fiber 会完成得更好

boost.fiber 的实现

如果单看 boost::fibers::fiber 的 API,我们会发现它与 thread(如 std::thread)非常的接近

  • fiber 构造函数与线程类似,析构一个尚未结束的 fiber 也需要 join / detache(为啥没提供个 jfiber?)
  • 可以通过一个枚举类 launch 指定该构造的 fiber 是等候 scheduler 调度才执行(post,默认)还是取代 caller fiber(dispatch)立即执行
  • 可以通过 get_id 方法拿到 fiber 的 id
  • boost::this_fiber 这个 namespace 提供了一些 utility,比如 get_id、sleep_for/_until、yield(这会将当前 fiber 挂起)

提供了几个简单的 scheduling 算法(round_robin、work_stealing、shared_work),这可以使用 boost::fibers::use_scheduling_algorithm<>() 模板设置,他们都实现了 boost::fibers::algo::algorithm 接口。

需要注意的是同步工具不能与 thread 混用,因为线程的同步往往是跨 core 的,而 fiber 是在同一个 core 上执行的。

  • boost::fibers::mutex 提供了互斥锁,尽管多数情况下对某些共享资源的访问是 exclusive 的,还是存在需要保护 critical resource 不被几个 fiber 访问
  • boost::fibers::condition_variable(_any) 提供了与 mutex 一起使用方便检查数据 availability 的情况
  • boost::fibers::barrier 使得多个 fiber 能“同时”被唤醒(其实是一个接一个)
  • channel 是一个更好用的数据分享机制(incremental),它是一个 typed queue(又细分为 buffered 与 unbuffered,后者在没有 producer / consumer 任意一方的时候写入或者读出都会导致 block),方便一个 fiber 将数据写入,另一个将数据读出来
  • future / promise 与 packaged_task 与线程的 API 类似

如果没理解错,fiber 调用 blocking IO 后,scheduler 是可以执行另外的 fiber 的,那对于一些其他的情况

  • 一个 async API 通常通过一个 api + callback(数据 ready 后如何处理),但这个 API 会立即返回(看有没有 error),因此常见的做法是使用 promise / future 结构
  • 某些 non-blocking io 的 API 是理解返回结果或者 EWOULDBLOCK(错误,表示这个时候拿不到),可以用一个 while loop 来 poll,失败了通过 yield 让其他 fiber 有机会执行

如果还有另一个 event manager / system(常见的如 UI 系统会有一个自己的 event loop),我们需要在它的 main loop 中调用 this_fiber::yield 来寻求一个给 fiber scheduler 执行的机会。

一个例子

我们这里仅仅想看看调用一个 blocking call 的情况是否会导致 fiber 的挂起。

#include <boost/fiber/all.hpp>
#include <chrono>
#include <iostream>
#include <string_view>

using namespace std::chrono_literals;

void exp1_work(std::string_view msg) {
  std::cout << "Experiment 1: entering " << msg << "\n";
  for (int i = 0; i < 3; ++i) {
    std::cout << "Fiber id = " << boost::this_fiber::get_id() << ": started "
              << i << "th time\n";
    boost::this_fiber::sleep_for(200ms);
  }
  std::cout << "Experiment 1: leaving " << msg << "\n";
}

void exp1() {
  std::cout << "Experiment 1\n";
  std::cout << "Experiment 1: firing up fibers, has_ready_fibers = "
            << boost::fibers::has_ready_fibers() << "\n";
  boost::fibers::fiber f1(boost::fibers::launch::post, exp1_work, "foo");
  std::cout << "Experiment 1: fiber 1, joinable = " << f1.joinable() << "\n";
  boost::fibers::fiber f2(boost::fibers::launch::post, exp1_work, "bar");
  std::cout << "Experiment 1: fiber 2, joinable = " << f2.joinable() << "\n";
  std::cout << "Experiment 1: joining\n";
  f1.join();
  f2.join();
  std::cout << "Experiment 1: ending\n";
}

void exp2() {
  std::cout << "Experiment 2\n";
  std::cout << "Experiment 2: firing up fibers, has_ready_fibers = "
            << boost::fibers::has_ready_fibers() << "\n";
  boost::fibers::fiber f1([]() {
    std::cout << "Experiment 2: entering f1 before sleep\n";
    sleep(1);
    std::cout << "Experiment 2: leaving f1\n";
  });
  boost::fibers::fiber f2([]() {
    boost::this_fiber::yield();  // make sure f1 goes first
    std::cout << "Experiment 2: entering f2 after first yield\n";
    boost::this_fiber::yield();  // make sure f1 goes first
    std::cout << "Experiment 2: f2 will not run before f1 end\n";
  });
  std::cout << "Experiment 2: fiber 2, joinable = " << f2.joinable() << "\n";
  std::cout << "Experiment 2: joining\n";
  f1.join();
  f2.join();
  std::cout << "Experiment 2: ending\n";
}

int main(int argc, char* argv[]) {
  exp1();
  exp2();
  return 0;
}

我这里实在是 mac 里面编译的,因为 boost 是通过系统的 clang 编译的,似乎这里不能使用 gcc 编译连接,比如编译后找不到 sleep_for 对应的二进制(库里面也许默认的某些东西不一样?比如 chrono 默认实现的类名),即便跳过这个有问题的函数,一到 fiber 的构造函数也会崩溃。换回 clang 后正常,编译命令为

c++ -std=c++20 -I/opt/local/libexec/boost/1.81/include -L/opt/local/libexec/boost/1.81/lib -lboost_context-mt -lboost_fiber-mt app.cpp -o app

执行结果

Experiment 1
Experiment 1: firing up fibers, has_ready_fibers = 1
Experiment 1: fiber 1, joinable = 1
Experiment 1: fiber 2, joinable = 1
Experiment 1: joining
Experiment 1: entering foo
Fiber id = 0x7fddc0047f00: started 0th time
Experiment 1: entering bar
Fiber id = 0x7fddc0067f00: started 0th time
Fiber id = 0x7fddc0047f00: started 1th time
Fiber id = 0x7fddc0067f00: started 1th time
Fiber id = 0x7fddc0047f00: started 2th time
Fiber id = 0x7fddc0067f00: started 2th time
Experiment 1: leaving foo
Experiment 1: leaving bar
Experiment 1: ending
Experiment 2
Experiment 2: firing up fibers, has_ready_fibers = 1
Experiment 2: fiber 2, joinable = 1
Experiment 2: joining
Experiment 2: entering f1 before sleep
Experiment 2: leaving f1
Experiment 2: entering f2 after first yield
Experiment 2: f2 will not run before f1 end
Experiment 2: ending

需要注意的是一个系统级的 api(如 sleep)因为并没有 fiber aware,因此不可能在这里面给 f2 让路,同理如果我们使用 fopen/fread 这些文件 IO,也会有类似的问题。我们需要在 boost.asio 里面重新审视一下如何利用 fiber / coroutine 等用户级别可控的 concurrency。

boost.fiber

Sony 相机作为 webcam 的途径

总的来说有两种类型:

  • PC remote 的 live view
  • HDMI 的 video capture

其实 live view 绝对是滥用,因为 live 通过 usb 传递给手机或者 PC 的分辨率其实一般,实现其实也具有一定局限性:

  • 首先需要安装索尼的客户端软件 Imaging Edge
  • 然后需要使用录屏软件,推荐 obs studio
  • 然后需要一个 obs 的插件,把视频流转换为一个虚拟的摄像头(winmac
  • 在需要摄像头的软件里选择这个虚拟摄像头即可

可能最大的局限性是相机本身,竟然不是每个型号都支持 remote live view 的!见这个列表。本以为自己的 a5100 终于可以大显身手,结果并不行。

相对而言,HDMI 输出本身是为了视频的输出,只是接驳的对象是显示设备而不是一个 video stream,因此存在视频采集卡这么个东西,原先火线之类的也有,现在 HDMI video capture card 非常多,需要注意的大约两个东西:

  • video passthrough,某些采集卡允许视频流采集后仍然可以传递到下一个设备,这样一来可以一边输出到屏幕一边采集,这个 passthrough 也有分辨率的限制的,建议购买带 4k/30p passthrough 能力
  • video capture,这个分辨率很好理解了,只是便宜的卡往往算力只够处理 1080p,要想录入 4k,价格似乎没有低于 100$ 的

这种设备往往提供了 USB 和主机接驳,直接作为摄像头可以供其他应用程序使用。这就是得额外花硬件的钱了,不过算比较通用的策略。

Sony 相机作为 webcam 的途径

KeePass

终于开始了 KeePass 的迁移。原因大概是最早的单机版本策略在现在有点过时,随着 NAS 的加入,我们可以更加安全的共享自己的密码,从而在多个不同的设备上访问。这里又回到了多年以前在 Windows 里面就知道的 KeePass,这么多年了,似乎它还是这个领域不多的佼佼者。

我们这里提供两个数据访问方式:

  • Synology Drive:它可以选择单向或者双向同步,如果有的设备永远只读,那么可以选择单向同步;通过 Drive 多设备访问时,应该创建一个独立的目录作为单独的 sync task 存在,尽量不要使用比较黑科技的想法(比如 symlink 或者 mount –bind),这样多个设备就能比较容易的同步这一小部分文件
  • WebDAV,这需要在 NAS 上安装 WebDAV 服务器,开通 https 后通过 router 的 port forwarding 将其暴露在公网上,不少 KeePass 的客户端如果不能从 Synology Drive 获得,还是可以通过 WebDAV 完成同步的

Windows

Windows 的 KeepPass 是原生支持,功能最丰富,而群晖在 Windows 上的 CloudStation 客户端 Synology Drive 也是存在一个比较稳定的版本的,我们通过 drive 从 NAS 上 two way sync 密码文件夹到本地,然后再使用 KeepPass 打开该文件即可,可以修改文件(sync)也可以选择覆盖。

Mac

同样存在 Synology Drive,客户端可以选择 MacPass 这个免费的程序。不支持 WebDAV,但是也可以考虑使用 WebDAV 的 mount。

Android

通过 Keepass2Android Password Safe 这个免费的客户端通过 WebDAV 访问 NAS 上的密码文件。

iOS

通过 Synology Drive 与 KeepPass Touch 实现。后者可以选择 import 然后可以打开 iCloud 或者 drive 里面的文件,这时打开 drive 里面的即可。但是似乎免费版本有些限制,比较猥琐。

Linux

Synology Drive 也提供了 Fedora 和 Ubuntu 的 package,自用的 debian 可以使用 Ubuntu 搞定。客户端有 mono 的 KeePass 和 Qt 的 KeepPassXC,差别似乎不大,前者应该跟 Windows 下面的是一样的

KeePass