2017 小结

上一次写年终总结大概在三四年前,因为老博客数据没有备份的缘故,具体是什么时候也记不清了。

BTW,其实我也不是很记得一开始为什么要在春节期间而不是元旦时写上一年的总结。可能和往常元旦时还未放假有关。

17年总体上还算符合个人发展的预期:

上半年接了原来主站投稿工具的锅,和另外一个同事花了一个月多,完全从头开始硬生生的做了一个新版本的投稿工具,还带着新版本要求的压制功能。后续又迭代了两三个版本,后面最新的 v2.0 甚至都已经开始支持简单的视频编辑。

虽然因为业务上的调(zhan)整(dui),8月份开始不再迭代新的功能,但是这三四个月的时间也刷了不少 HTTP uplaod,multithreading,architecture design 等相关经验。

这段时间的后期,还抽了一些时间,重写了项目整套 HTTP API 网络通信层,基本上擦干净了原来某个能力不行的前同事堆的一坨屎,还了16年底的一个心愿。

这一通重写又刷了不少设计架构的经验,同时对 HTTP 协议有了更深的理解,毕竟用的 chromium net 实在是太 building block 了。不过同时又一次证明了:绝知此事要躬行 以及 汝欲学诗,功夫在诗外 的真理。

下半年开始,因为业务调整,基本只有直播姬一个项目,并且因为自己还是侧重技术,所以对业务需求不是太感冒,项目上主要还是挑了一些技术债清理,解决一些没人愿意做的差事。

因为对接的专项产品还算靠谱的缘故,直播姬自身的迭代也一直在小步前进,甚至年底时做了一次大更新,版本跳上了 v2.0。

与此同时自己也小刷了一把管理(product & people)的经验,发现自己虽然不是什么管理奇才,但也至少耳聪目明,悟的快,学的快,当年在北京时老领导的各种饭桌谈话中展现方法论也逐渐纳入了自己的技能包。

Once again,相比下,我还是更喜欢技术,技术无论多复杂,比起管理的人而言,远远没有那么多不确定性。

接近年底时部门发生了一次大震荡,一轮 reorg 后,老领导从主站调到了直播任技术总监,那会儿我大概能猜到高层对这块业务带来的营收期望有多高,以及,对原来业务贡献的不满;以及 —— 未来部门将有大变化,这大概也应该是 18 年的部门主旋律。

国庆前后经历了一次校招,被发配到了武汉和广州。上一次经历校招是四年前,而这次我的角色变成了面试官。

参与校招想来只有一个好处:练习自己辨人的能力。或许未来某一天这个能力会变成我的基本要求之一。

不过参加了这次校招,面了不少 985/211 的应届毕业生之后,我至少发现当年没有读研究生的决定是非常正确的,毕竟我(当年)已经没有资本再荒废另一个三年了。

17年年底一直到2月初,和一个前同事帮另外一个前同事做了一个 demo,以方便他去争(hu)取(you)创业投资,中间断断续续占用了一些私人时间,不过最后还好是按照预期做出来了。虽然我对这事儿没有太大想法,但是万一突然那天变成了一块额外收入也说不定呢~

回想生活上,3月份捡了小区一只大概7个月的流浪橘猫,起了个名字叫 Turing,喂养了两个月成功从一开始 2KG 不到涨到了 4KG 多,遗憾是5月12号那天打算带他去打第二针疫苗的时候,他从猫包里挣脱出来,头也不回的一路跑进了小区北门的一片草丛里。

我找了一下午,还是没有发现他的踪迹,也为此伤心了几天。

四天后,在豆瓣上看到有人发了小奶猫的领养帖子,按捺不住心中的冲动,当天就联系了发帖人,带回了一只小奶猫,后来媳妇儿给起了个名字叫小石榴

小石榴一开始身体不是很好,又是拉稀又是腹胀,精心照顾了一个月,小家伙才慢慢开始茁壮成长,并一发不可收拾…

后来又过了一个月,发帖人联系我,问我能不能在领养最后剩下的两只奶猫,我心一横就答应了,于是就有了后来的 小荷花(花花)和 小天(黑黑)。

犹记得花花和黑黑刚来到家时,小石榴护着自己食物的样子,让人捧腹。你们可都是一个娘胎出来的亲生姊妹啊!

混熟后,我和三只喵喵开始了没羞没臊的生活,直到 —— 三只喵越长越大我开始感到有压力后把黑黑送给了游戏部的一个同事。

虽然很久没有打听黑黑的近况了,但是我还是衷心希望她能够健康成长,和她两个快要变成猪的姐姐一样 :)

由于提早给石榴和花花做了绝育手术,因此她们直接躲过了第一次发情,她们至今都开心得和一个100公斤的姑娘一样~

最后展望一下 18 年(再不结束我要睡着了…),希望今年能顺利转服务端,继续自己的技术追求之路;同时喵喵们也健康快乐的成长。

最后的最后,希望能早日搬进杭州的房子,每天和媳妇儿在沙发上看看电影拌拌嘴,撸着石榴和花花。

使用 Winsock Extension API 的正确姿势

所谓的 Winsock Extension API 指的是微软专门额外添加,由应用层 mswsock.dll 导出的函数集,包括 AcceptEx(), DisconnectEx() .etc

使用这部分 API 的原因是它们通常支持更新更牛X的特性,且几乎大多数都是异步函数。

但是因为这部分是应用层函数,直接链接 mswsock.dll 会导致每个函数调用内部都会进行一次由系统完成的动态的函数地址确定,相当于 GetProcAddr(),由此引发性能问题。

由此衍生出,首先使用 WSAIoctl() 获取这部分 extension API 的函数地址,然后利用函数地址调用的做法。

注:性能开销这部分可参考 这里。回答引用的 MSDN Magazine 上的文章已经不可访问了,算是一个遗憾。

然而关于这块包括 MSDN 上都是一些不详尽的片段,或者根本没提到。

例如我在学习这部分东西时,一个最大的问题就是 WSAIoctl() 函数无论如何需要一个 socket handle,然而这个需要和我们的目的其实没有关系。

围绕这个 socket handle 就可以催生出各种问题,但是 MSDN 上甚至没有对这个做哪怕一些解释…

通过翻查汇总各种相关的代码片段,以及结合 MSDN 上描述的内容,个人总结出了一个推荐的做法:

  1. 使用一个全局的管理者来保存你需要的扩展函数指针。这里全局的管理者可以是一个 singleton,或者用 namespace 包裹的一些全局对象
  2. 在初始化时机创建一个 TCP socket,利用这个 socket 去调用 WSAIoctl(),拿到扩展函数地址
  3. 经过上一部之后,外界使用者就可以和使用普通的 API 一样使用这几个扩展函数

至于是不是要回收刚才创建的 socket,这个看自己喜好…

参考部分代码:

namespace {

ScopedSocketHandle sock;

template<typename P>
void GetExtensionFunctionPointer(P& pfn, GUID guid)
{
    DWORD recv_bytes = 0;
    auto rv = WSAIoctl(sock.get(), SIO_GET_EXTENSION_FUNCTION_POINTER, &guid, sizeof(guid), &pfn,
                       sizeof(pfn), &recv_bytes, nullptr, nullptr);
    ENSURE(CHECK, rv == 0)(rv)(WSAGetLastError()).Require();
}

}   // namespace

namespace winsock_ctx {

LPFN_ACCEPTEX AcceptEx = nullptr;
LPFN_DISCONNECTEX DisconnectEx = nullptr;

void Init()
{
    WSADATA data {0};
    auto result_code = WSAStartup(MAKEWORD(2, 2), &data);
    ENSURE(THROW, result_code == 0)(result_code).Require();

    sock.reset(socket(AF_INET, SOCK_STREAM, 0));
    ENSURE(CHECK, !!sock)(WSAGetLastError()).Require();

    GetExtensionFunctionPointer(AcceptEx, WSAID_ACCEPTEX);
    GetExtensionFunctionPointer(DisconnectEx, WSAID_DISCONNECTEX);

    sock.reset();

    printf("-*- Windows Socket Library Initialized -*-\n");
}

void Cleanup()
{
    WSACleanup();

    printf("-*- Windows Socket Library Cleaned -*-\n");
}

}   // namespace winsock_ctx

完整的 sample 可以看这里

在 Windows 上获取崩溃的模块名和模块内地址偏移

如果应用程序存在多个模块,那么有很大的可能通过 SetUnhandledException() 安装的崩溃处理函数和发生崩溃的地址不在一个模块内,因此直接在 crash handler 里使用当前模块地址是错误的做法。

一个可行的方法是,利用崩溃时的上下文信息 EXCEPTION_POINTERS,拿到触发崩溃的地址 EIP/RIP,然后利用 Windows 7 新增的 GetMappedFileNameW() 拿到某个地址所在的模块的完整设备路径。

接下来通过简单的字符串处理就可以获得模块名。

剩下的就是模块内地址偏移。

因为 Windows 程序的 DLL 在加载时很有可能因为 preferred base address 被占用了导致 DLL 的符号地址需要先经过一次 rebase;再考虑 ASLR 的影响,崩溃时的 EIP/RIP 基本上是不能拿来作为崩溃标识的,因为同一处崩溃的地址可能每次运行都会变。

解决方案是通过 GetModuleHandleW() 拿到模块实际的加载地址,然后再和前面的崩溃地址做一次 distance 运算就可以拿到了模块内地址偏移。

这样,name+offset 基本上可以作为 key 用来做崩溃信息的聚合。

Monthly Read Posts in Jan 2018

C++17 constexpr everything (or as much as the compiler can)

名字不明觉厉结果是入门级读物,有点水…


Thread’s Stack

Windows 线程栈内存的布局,包括 guard page 来提示栈溢出的一些细节


下面是一些 Talk

allocator Is to Allocation what vector Is to Vexation - Andrei Alexandrescu - CppCon 2015

只能说不明觉厉,因为学不到什么有用的东西…

CLANG C2 for Windows - Jim Radigan - CppCon 2015

演讲者似乎是这个项目的主导,并且实现了 MSVC 的 coroutine。

期待什么时候 clang/c2 可以作为 MSVC 的默认配置…

另:演讲者长得有点像 house of cards S4 里的 Mark Usher,很 charming

Applying functional programming in code design - Michał Dominiak - CppCon 2015

很糟糕的一个 talk,难以想象这么有意思的 topic,talk 全程居然没有一行代码的展示….

不过里面提到的 node-based data structures 适合用来实现 functional programming 的一些基础设施,我一开始完全是懵逼的,直到看了 Seven Concurrency in Seven Weeks 里的第一章还是第二章,才意识到核心是什么。可见这个 talk 是多么的不友好。

C++11, 14, 17 Atomics - the Deep Dive - Michael Wong - CppCon 2015

作者是 IBM compiler team 的 director,说话有点意思。

另外这个 talk 的 PPT 值得研究一下,尤其是涉及到 happens-before / synchronized-with .etc 这类同步语义的内容


因为一月份快接近年底了,事情比较多,也很杂,所以自己的学习进度明显被拖慢了,希望从2月份开始能够迅速回到正常节奏。

正确地初始化 std::atomic_flag

根据标准文档的要求,std::atomic_flag 只有利用 ATOMIC_FLAG_INIT 初始化之后,才获得一个确定的初始状态。

现在假设我们要自己实现一个 spin-lock,那么只需要利用 std::atomic_flag 实现一个 spin-mutex:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SpinMutex {
public:
SpinMutex()
: flag_(ATOMIC_FLAG_INIT)
{}

void lock() {
while (flag_.test_and_set(std::memory_order_acquire)) {}
}
 
void unlock() {
flag_.clear(std::memory_order_release);
}
 
private:
std::atomic_flag flag_;
};

事实上,知名著作 C++ Concurrency in Action 1st Edition 中介绍 std::atomic_flag 时用的也是一模一样的代码。

然而,这段代码在比较新的 C++ 编译器上会编译失败。

VS2017 直接提示失败,Clang 3.9 出现一个警告。

问题的根源是,标准委员会对:如何时用 ATOMIC_FLAG_INIT 初始化 std::atomic_flag 存在分歧和反复,导致 C++ 11 和 C++ 14 在这个点上存在不一致。

万能的 StackOverflow 刚好有一个相关的问题

正确的做法

正确的做法仍然是遵循标准要求的 used in the copy-initialized context,即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class SpinMutex {
public:
void lock() {
while (flag_.test_and_set(std::memory_order_acquire)) {}
}
 
void unlock() {
flag_.clear(std::memory_order_release);
}
 
private:
// Initialize to clear state.
// Don't use initialization list for `flag_`.
std::atomic_flag flag_ = ATOMIC_FLAG_INIT;
};

MISC

在 VS2017 中,可以在 member initialization list 中用 {} 而不是 () 初始化,能正常编译通过。但是同一做法在其他编译器上没有效果。

因此,在这种涉及 unspecified 的点上,尽量使用标准的做法,避免之后更换编译器踩坑。

Binding to Privately Inherited Member Functions

考虑代码

#include <functional>

class Base {
public:
    void Foo(int n)
    {
        printf("the value is %d", n);
    }
};

class Derived : Base {
public:
    using Base::Foo;
};

int main()
{
    auto f = std::bind(&Derived::Foo, std::placeholders::_1, 10);
    Derived o;
    f(&o);
    return 0;
}

编译会提示出错,错误信息类似:_Failed to specialize function template ‘unknown-type std::_Binder<std::_Unforced,void (__cdecl Base::* )(int),const std::_Ph<1> &,int>::operator ()(Unbound &&…) const’

根本原因是,即使前面使用了 using Base::Foo;, 也只是改变了这个函数在子类中的可见性,影响名字查找;并没有真的在子类中添加了这个函数,因此 &Derived::Foo 实际上获取的类型是 void (Base::*)(int),而正是这个类型影响了 std::bind() 的实例化。

因此当后面用 Derived 的实例去调用 function object 时会失败。

Stackoverflow 上有一个类似的问题,可以参考。

Workaround

(1) 子类增加一个 call-wrapper

然而大多数采用 private-inheritance 而不是 composition 的情况下,都是避免添加各种 call-wrapper

(2) 绑定 lambda 而不是函数指针

这是一个推荐的做法

(3) 强行用 reinterpret_cast<>

呃… let’s don’t be evil

Monthly Read Posts in Dec 2017

C++ 11 Concurrency: Part 1 & Part 2 & Part 3

本来想作为 C++ Concurrency in Action 的一个补充,但是发现这个 series (迄今)写得实在是太一般了…看看后续有没有改进


使用Fiddler做抓包分析

除了 auto-responder ,其他部分内容感觉都好一般


高并发性能调试经验分享

文章不短,但是核心还是调试那几板斧:

  • 尝试重现/增加重现概率
  • 脑补可能的 root cause
  • 通过实验排除

文章开头说这类问题适合筛应届生,纯属胡说八道。

首先前面说的那几点虽然是很容易回答,毕竟提纲挈领,但是实际解决过这类多线程/高并发的问题的人都知道,很多时候问题都是高度上下文相关的,换一个上下文很多之前的假设推断手段什么的都没效果,毕竟这是一个强经验相关的 domain。指望碰到一个应届生能熟练解决这类问题(停留在打嘴炮阶段不算),还不如指望哪天出现强 AI。


初探 Python 3 的异步 IO 编程
Python并发编程之协程/异步IO

口水文,还好是在地铁上看完的。


磁盘I/O那些事

实战那部分写的不错

另外,文章 url 的 desk 目测是 disk 的 typo


How Google Authenticator Works

看完想自己写一个


A Few Good Types - Neil MacIntosh - CppCon 2015

介绍了一波 array_viewstring_vew

可惜 array_view 没有进标准


All Your Tests Are Terrible - Titus Winters and Hyrum Wright - CppCon 2015

又名:单元测试的几条军规

基本覆盖了实现单元测试 case 的一些基本准则。两个演说者很逗,将相声一样

不过我其实更关心部分这个 talk 并没有涉及到,比如涉及 GUI 的交互逻辑怎么做 ut;负责模块上粒度上怎么抽诸如此类..


Futures from Scratch - Arthur O’Dwyer - CppCon 2015

一些和 future 有关实现细节