Boost【2】ScopeExit

需求

  • 资源有很多种,每种都封装一套,还是挺累的!对于比较少使用或者一个程序很可能只会用一次的资源,我们不想封装!

  • Golang 的 defer 真香!

解决

利用 RAII 特性,封装个 ScopeGuard!或者直接用 Boost.ScopeExit。

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
30
31
32
33
34
35
// umu/scope_exit.hpp
#pragma once

#include <functional>

// C++11

namespace umu {
class ScopeGuard {
public:
explicit ScopeGuard(std::function<void()> on_exit_scope)
: on_exit_scope_(on_exit_scope), dismissed_(false) {}

~ScopeGuard() noexcept {
if (!dismissed_) {
on_exit_scope_();
}
}

void Dismiss() { dismissed_ = true; }

private:
std::function<void()> on_exit_scope_;
bool dismissed_;

// noncopyable
ScopeGuard(ScopeGuard const&) = delete;
ScopeGuard& operator=(ScopeGuard const&) = delete;
};
} // end of namespace umu

#define SCOPEGUARD_LINENAME_CAT(name, line) name##line
#define SCOPEGUARD_LINENAME(name, line) SCOPEGUARD_LINENAME_CAT(name, line)
#define ON_SCOPE_EXIT(callback) \
umu::ScopeGuard SCOPEGUARD_LINENAME(EXIT, __LINE__)(callback)

范例

Windows 上使用 socket 必须先调用 WSAStartup 初始化 WinSock 环境。

常规封装版

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <winsock2.h>

#pragma comment(lib, "ws2_32.lib")

class WinSock {
public:
WinSock() : error_code_(WSAEFAULT) {}

int Initialize(WORD version_requested = WINSOCK_VERSION) {
assert(WSAEFAULT == error_code_);
return error_code_ = ::WSAStartup(version_requested, &wsa_data_);
}

bool GetWsaData(WSADATA& wsa_data) {
if (NO_ERROR == error_code_) {
wsa_data = wsa_data_;
return true;
}
return false;
}

int GetErrorCode() { return error_code_; }

~WinSock() {
if (NO_ERROR == error_code_) {
::WSACleanup();
}
}

private:
int error_code_;
WSADATA wsa_data_;
}

int main(int argc, char* argv[]) {
WinSock winsock;
int error_code = winsock.Initialize();
if (NO_ERROR != error_code) {
std::cerr << "Initialize() failed with " << error_code << '\n';
return error_code;
}

// test codes
SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (INVALID_SOCKET != s) {
closesocket(s);
std::cout << "OK\n";
} else {
error_code = WSAGetLastError();
if (WSANOTINITIALISED == error_code) {
std::cerr << "WSANOTINITIALISED\n";
} else {
std::cerr << "socket() failed with " << error_code << '\n';
}
}

return error_code;
}

Boost.ScopeExit 版

现在是 2020 年 9 月,建议使用 cpp17,所以抛弃 BOOST_SCOPE_EXIT + BOOST_SCOPE_EXIT_END,使用 cpp11 的 BOOST_SCOPE_EXIT_ALL。

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
30
31
32
#include <winsock2.h>

#include <boost/scope_exit.hpp>

#pragma comment(lib, "ws2_32.lib")

int main(int argc, char* argv[]) {
WSADATA wsa_data = {};
int error_code = ::WSAStartup(WINSOCK_VERSION, &wsa_data);
if (NO_ERROR != error_code) {
std::cerr << "WSAStartup() failed with " << error_code << '\n';
return error_code;
}
// 类似 Golang 的 defer
BOOST_SCOPE_EXIT_ALL(&) { ::WSACleanup(); };

// test codes
SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (INVALID_SOCKET != s) {
closesocket(s);
std::cout << "OK\n";
} else {
error_code = WSAGetLastError();
if (WSANOTINITIALISED == error_code) {
std::cerr << "WSANOTINITIALISED\n";
} else {
std::cerr << "socket() failed with " << error_code << '\n';
}
}

return error_code;
}

参考

https://www.boost.org/doc/libs/1_74_0/libs/scope_exit/doc/html/BOOST_SCOPE_EXIT_ALL.html

诗盗·浪

《#诗盗#·浪》:天风扬手捉浪脚,惊涛扯蛋扶大雕。四十四桥日光浴,渔人看书学吹箫。

注解

鼓浪屿旅游记!改编自唐代杜牧的《寄扬州韩绰判官》:

青山隐隐水迢迢,秋尽江南草未凋。
二十四桥明月夜,玉人何处教吹箫?

Boost【1】安装

软件环境

  • Windows 10

  • VS2019

其它系统的安装、编译过程都类似。即使您使用其它系统,本文仍然有参考意义。

1. 下载

官网:https://www.boost.org/,当前最新版本是 1.74.0。

在国内直接下载可能比较慢,您可以用掩耳下载,一般有 MB 级的速度。

2. 解压

假设解压到 D:\dev\boost_1_74_0,这个路径后面要设置为环境变量 BOOST_ROOT 的值。

最好检查目录结构,以防解压时弄错目录层级:

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
# umutech @ UMU618 in D:\dev\boost_1_74_0 [14:31:58]
$ ls

Directory: D:\dev\boost_1_74_0

Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 2020/8/11 23:28 1 boost
d---- 2020/8/11 23:26 1 doc
d---- 2020/8/11 23:28 1 libs
d---- 2020/8/11 22:57 1 more
d---- 2020/8/11 22:55 1 status
d---- 2020/8/11 22:55 1 tools
-a--- 2020/8/11 22:55 989 boost.css
-a--- 2020/8/11 22:55 6.16KB boost.png
-a--- 2020/8/11 22:55 850 boost-build.jam
-a--- 2020/8/11 22:55 18.81KB boostcpp.jam
-a--- 2020/8/11 22:55 2.39KB bootstrap.bat
-a--- 2020/8/11 22:55 10.38KB bootstrap.sh
-a--- 2020/8/11 22:55 769 index.htm
-a--- 2020/8/11 23:28 5.46KB index.html
-a--- 2020/8/11 22:55 291 INSTALL
-a--- 2020/8/11 22:55 11.65KB Jamroot
-a--- 2020/8/11 22:55 1.31KB LICENSE_1_0.txt
-a--- 2020/8/11 22:55 541 README.md
-a--- 2020/8/11 22:55 2.55KB rst.css

3. 添加 BOOST_ROOT 环境变量

  • 您可以通过图形界面配置,右击【此电脑】-【属性】-【高级系统设置】-【环境变量】。新建一个,变量名为 BOOST_ROOT,变量值为 D:\dev\boost_1_74_0。

  • 也可以使用命令行:setx BOOST_ROOT D:\dev\boost_1_74_0

下面 Powershell 命令用于检查设置是否正确:

1
2
3
# umutech @ UMU618 in D:\dev\boost_1_74_0 [14:39:22]
$ echo $env:BOOST_ROOT
D:\dev\boost_1_74_0

4. 编译 b2

直接运行 bootstrap.bat 即可,如果是非 Windows 系统,则是 bootstrap.sh。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# umutech @ UMU618 in D:\dev\boost_1_74_0 [15:02:20]
$ .\bootstrap.bat
Building Boost.Build engine

Generating Boost.Build configuration in project-config.jam for msvc...

Bootstrapping is done. To build, run:

.\b2

To adjust configuration, edit 'project-config.jam'.
Further information:

- Command line help:
.\b2 --help

- Getting started guide:
http://boost.org/more/getting_started/windows.html

- Boost.Build documentation:
http://www.boost.org/build/

5. 用 b2 编译 Boost

由于我们需要编译 Win32 和 x64 两种平台,所以给 b2 命令行加上个参数:

1
2
# umutech @ UMU618 in D:\dev\boost_1_74_0 [15:03:43]
.\b2.exe --address-model=64
非 Windows 系统,可以直接运行 ./b2

新机器一般几分钟就能编译完毕。比如 OMEN 25L 只需要 3 分钟。廉想 L490 笔记本大约 14 分钟。

6. 配置 VS 工程

6.1 配置方法一

打开一个工程的属性页,定位到 VC++ Directories。

  • Include Directories 加上 $(BOOST_ROOT)

  • Library Directories 加上 $(BOOST_ROOT)\stage\lib

如果已经有其它值,记得用 ; 隔开。

6.2 配置方法二

打开一个工程的属性页。

  • 定位到 C/C++,General,Additional Include Directories 加上 $(BOOST_ROOT)

  • 定位到 Linker,General,Additional Library Directories 加上 $(BOOST_ROOT)\stage\lib

结语

  • Boost 值得学习和使用。

  • 本文对仅用 VS 写过 Hello world 的入门级程序员友好。(高手不会看到这末尾……)

诗盗·射飞丝阿屉

《#诗盗#·射飞丝阿屉》:文十初上射飞丝,挨踢更迭,阿屉不减风采。八点一,七四八,八年等待五年吃灰,如今一刷还是牛逼。

注解

Surface RT 刷 Windows 10 Arm32 版。改编自霹雳角色疏楼龙宿的诗号:

华阳初上鸿门红,疏楼更迭,龙麟不减风采;
紫金箫,白玉琴,宫灯夜明昙华正盛,共饮逍遥一世悠然。

刷完玩玩当然是继续吃灰了……毕竟性能不行!

诗盗·去快恋

《#诗盗#·去快恋》:前年今日如梦游,神奇八哥无止休。神奇不知何处去,八哥依旧笑春秋。

注解

纪念在区块链行业工作过!改编自唐代崔护的《题都城南庄》:

去年今日此门中,人面桃花相映红。
人面不知何处去,桃花依旧笑春风。

VS2019 Makefile 型工程调用 Clang

需求

VS2019 Makefile 型工程使用 Clang 编译,要同时支持 x86 和 x64 平台。

问题

找不到 Clang 参数可以指定目标平台,如果您知道请不吝赐教。(github 账号:UMU618)

解决

同时安装这两个平台的 Clang。x86 的由 VS2019 自带,在 VS 中被表示为 "$(ClangAnalysisToolsPath)\clang.exe",宏展开后为:

1
2
3
4
5
$ &"C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\Llvm\bin\clang.exe" -v
clang version 10.0.0
Target: i686-pc-windows-msvc
Thread model: posix
InstalledDir: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\Llvm\bin

x64 是另外安装的,安装时选择把目录注册到 PATH:

1
2
3
4
5
$ clang -v
clang version 10.0.0
Target: x86_64-pc-windows-msvc
Thread model: posix
InstalledDir: C:\Program Files\LLVM\bin

结论:只需在 Win32 的平台设置使用 "$(ClangAnalysisToolsPath)\clang.exe",而 x64 则直接使用 clang(如果安装时忘记注册到 PATH,需要手动添加)。

SDL/Windows 代码分析【1】thread

1. SDL_semaphore

代码:src\thread\windows\SDL_syssem.c

别名:SDL_sem

1
typedef struct SDL_semaphore SDL_sem;

基于 WinAPI 匿名 Semaphore 封装。MaximumCount 硬编码为 32 * 1024。

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
30
31
32
33
struct SDL_semaphore
{
HANDLE id;
LONG count;
};


/* Create a semaphore */
SDL_sem *
SDL_CreateSemaphore(Uint32 initial_value)
{
SDL_sem *sem;

/* Allocate sem memory */
sem = (SDL_sem *) SDL_malloc(sizeof(*sem));
if (sem) {
/* Create the semaphore, with max value 32K */
#if __WINRT__
sem->id = CreateSemaphoreEx(NULL, initial_value, 32 * 1024, NULL, 0, SEMAPHORE_ALL_ACCESS);
#else
sem->id = CreateSemaphore(NULL, initial_value, 32 * 1024, NULL);
#endif
sem->count = initial_value;
if (!sem->id) {
SDL_SetError("Couldn't create semaphore");
SDL_free(sem);
sem = NULL;
}
} else {
SDL_OutOfMemory();
}
return (sem);
}

2. SDL_mutex

代码:src\thread\windows\SDL_sysmutex.c

基于 WinAPI CriticalSection 封装。SpinCount 硬编码为 2000,即在多处理器系统上,如果无法立刻进入临界区,则会自旋最多 2000 次,然后等待 CriticalSection 内部关联的信号量。只要在自旋过程中其它线程退出临界区,则无需进入等待状态。这么做是提高效率,自旋时当前线程还占着 CPU,如果进入等待状态,就是交出 CPU 时间片了,而 CPU 调度是个消耗型操作。

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
struct SDL_mutex
{
CRITICAL_SECTION cs;
};

/* Create a mutex */
SDL_mutex *
SDL_CreateMutex(void)
{
SDL_mutex *mutex;

/* Allocate mutex memory */
mutex = (SDL_mutex *) SDL_malloc(sizeof(*mutex));
if (mutex) {
/* Initialize */
/* On SMP systems, a non-zero spin count generally helps performance */
#if __WINRT__
InitializeCriticalSectionEx(&mutex->cs, 2000, 0);
#else
InitializeCriticalSectionAndSpinCount(&mutex->cs, 2000);
#endif
} else {
SDL_OutOfMemory();
}
return (mutex);
}

3. SDL_cond

代码:src\thread\windows\SDL_syscond.c

基于 SDL_mutex 和 SDL_sem 封装。

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
30
struct SDL_cond
{
SDL_mutex *lock;
int waiting;
int signals;
SDL_sem *wait_sem;
SDL_sem *wait_done;
};

/* Create a condition variable */
SDL_cond *
SDL_CreateCond(void)
{
SDL_cond *cond;

cond = (SDL_cond *) SDL_malloc(sizeof(SDL_cond));
if (cond) {
cond->lock = SDL_CreateMutex();
cond->wait_sem = SDL_CreateSemaphore(0);
cond->wait_done = SDL_CreateSemaphore(0);
cond->waiting = cond->signals = 0;
if (!cond->lock || !cond->wait_sem || !cond->wait_done) {
SDL_DestroyCond(cond);
cond = NULL;
}
} else {
SDL_OutOfMemory();
}
return (cond);
}

SDL_CondWaitTimeout 实现较长,本文忽略。重点是:为了避免死锁,它进入等待前,会先解锁第二个参数 mutex。如果不这么做,其它线程也要 Lock 这个 mutex 就会发生死锁。

以下代码是典型用法,线程 A 进入临界区后,SDL_CondWait(内部调用 SDL_CondWaitTimeout)会调用 SDL_UnlockMutex(lock); 使得线程 B 可以进入临界区调用 SDL_CondSignal(cond);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Typical use

// Thread A:
SDL_LockMutex(lock);
while (!condition) {
SDL_CondWait(cond, lock);
}
SDL_UnlockMutex(lock);

// Thread B:
SDL_LockMutex(lock);
condition = true;
SDL_CondSignal(cond);
SDL_UnlockMutex(lock);

4. SDL_Atomic

代码:src\atomic\SDL_atomic.c

基于 _Interlocked API 封装。此类原子操作一般底层实现都是相应平台的汇编指令(比如 x86 平台是 lock cmpxchg 之类),但在不同平台下会有不同的封装集,所以 SDL_atomic.c 里有很多平台相关的宏判断。

5. SDL_MemoryBarrier

代码:src\atomic\SDL_atomic.h

内存屏障。参考文章:Acquire and Release Semantics

在 Windows x86 环境下等价于 _ReadWriteBarrier:

1
2
3
4
5
6
7
void _ReadWriteBarrier(void);
#pragma intrinsic(_ReadWriteBarrier)
#define SDL_CompilerBarrier() _ReadWriteBarrier()

/* This is correct for the x86 and x64 CPUs, and we'll expand this over time. */
#define SDL_MemoryBarrierRelease() SDL_CompilerBarrier()
#define SDL_MemoryBarrierAcquire() SDL_CompilerBarrier()

  • Acquire semantics is a property that can only apply to operations that read from shared memory, whether they are read-modify-write operations or plain loads. The operation is then considered a read-acquire. Acquire semantics prevent memory reordering of the read-acquire with any read or write operation that follows it in program order.

  • Release semantics is a property that can only apply to operations that write to shared memory, whether they are read-modify-write operations or plain stores. The operation is then considered a write-release. Release semantics prevent memory reordering of the write-release with any read or write operation that precedes it in program order.

生硬的翻译如下:

  • 获取语义是一个属性,它只能应用于从共享内存读取的操作,无论是“读取-修改-写入”操作还是普通加载。该操作将被视为“读取获取”。获取语义可防止“读取获取”和它之后的读取或写入操作发生内存重新排序。

  • 释放语义是一个属性,它只能应用于写入共享内存的操作,无论是“读取-修改-写入”操作还是普通存储。该操作将被视为“写入释放”。释放语义可防止“写入释放”和它之前,它之前的任何读取或写入操作发生内存重新排序。

以 x86 内存模型为例说明:

  • Loads are not reordered with other loads.
  • Stores are not reordered with other stores.
  • Stores are not reordered with older loads.
  • Loads may be reordered with older stores to different locations.

因为 store-load 可以被重排,所以 x86 不是顺序一致。但是其他三种读写顺序不能被重排,所以 x86 是 acquire/release 语义。

aquire 语义:load 之后的读写操作无法被重排至 load 之前。即 load-load, load-store 不能被重排。

release 语义:store 之前的读写操作无法被重排至 store 之后。即 load-store, store-store 不能被重排。

6. SDL_TLSData

意义:TLS,即 Thread Local Storage(线程局部存储)。

代码:src\thread\SDL_thread_c.h 和 src\thread\windows\SDL_systls.c

基于 SDL_Atomic、SDL_MemoryBarrier 和 WinAPI Tls API 封装。

以下结构体包含一个析构函数的指针,非空时,SDL_TLSCleanup() 会调用它。

1
2
3
4
5
6
7
8
/* This is the system-independent thread local storage structure */
typedef struct {
unsigned int limit;
struct {
void *data;
void (SDLCALL *destructor)(void*);
} array[1];
} SDL_TLSData;

7. SDL_Thread

代码:src\thread\SDL_thread.c 和 src\thread\windows\SDL_systhread.c

创建线程的 API 是 SDL_CreateThread 和 SDL_CreateThreadWithStackSize,导出函数 SDL_CreateThread 的定义如下,记为【X】:

1
SDL_DYNAPI_PROC(SDL_Thread*,SDL_CreateThread,(SDL_ThreadFunction a, const char *b, void *c, pfnSDL_CurrentBeginThread d, pfnSDL_CurrentEndThread e),(a,b,c,d,e),return)

下面会有递归展开宏的过程。首先,用 SDL_DYNAPI_PROC 的定义:

1
2
3
4
5
#define SDL_DYNAPI_PROC(rc,fn,params,args,ret) \
static rc SDLCALL fn##_DEFAULT params { \
SDL_InitDynamicAPI(); \
ret jump_table.fn args; \
}

展开【X】得到:

1
2
3
4
static SDL_Thread* __cdecl SDL_CreateThread(SDL_ThreadFunction a, const char *b, void *c, pfnSDL_CurrentBeginThread d, pfnSDL_CurrentEndThread e) {
SDL_InitDynamicAPI();
return jump_table.SDL_CreateThread(a,b,c,d,e);
}

其中 jump_table.SDL_CreateThread 是【X】被 SDL_dynapi_procs.h 的:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* The jump table! */
typedef struct {
#define SDL_DYNAPI_PROC(rc,fn,params,args,ret) SDL_DYNAPIFN_##fn fn;
#include "SDL_dynapi_procs.h"
#undef SDL_DYNAPI_PROC
} SDL_DYNAPI_jump_table;

/* The actual jump table. */
static SDL_DYNAPI_jump_table jump_table = {
#define SDL_DYNAPI_PROC(rc,fn,params,args,ret) fn##_DEFAULT,
#include "SDL_dynapi_procs.h"
#undef SDL_DYNAPI_PROC
};

展开得到,为:

1
2
3
4
5
6
7
8
9
10
11
typedef struct {
/* ... */
SDL_DYNAPIFN_SDL_CreateThread SDL_CreateThread;
/* ... */
} SDL_DYNAPI_jump_table;

static SDL_DYNAPI_jump_table jump_table = {
/* ... */
SDL_CreateThread_DEFAULT,
/* ... */
};

【X】又被 SDL_dynapi.c 的 initialize_jumptable 函数的:

1
2
3
4
/* Init our jump table first. */
#define SDL_DYNAPI_PROC(rc,fn,params,args,ret) jump_table.fn = fn##_REAL;
#include "SDL_dynapi_procs.h"
#undef SDL_DYNAPI_PROC

展开为:

1
2
3
/* ... */
jump_table.SDL_CreateThread = SDL_CreateThread_REAL;
/* ... */

所以,调用 SDL_CreateThread 最终调用的就是 SDL_CreateThread_REAL,又由于 src\dynapi\SDL_dynapi_overrides.h 中的:

1
#define SDL_CreateThread SDL_CreateThread_REAL

所以调用的是 src\thread\SDL_thread.c 中的:

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
30
31
32
33
#ifdef SDL_PASSED_BEGINTHREAD_ENDTHREAD
DECLSPEC SDL_Thread *SDLCALL
SDL_CreateThread(int (SDLCALL * fn) (void *),
const char *name, void *data,
pfnSDL_CurrentBeginThread pfnBeginThread,
pfnSDL_CurrentEndThread pfnEndThread)
#else
DECLSPEC SDL_Thread *SDLCALL
SDL_CreateThread(int (SDLCALL * fn) (void *),
const char *name, void *data)
#endif
{
/* !!! FIXME: in 2.1, just make stackhint part of the usual API. */
const char *stackhint = SDL_GetHint(SDL_HINT_THREAD_STACK_SIZE);
size_t stacksize = 0;

/* If the SDL_HINT_THREAD_STACK_SIZE exists, use it */
if (stackhint != NULL) {
char *endp = NULL;
const Sint64 hintval = SDL_strtoll(stackhint, &endp, 10);
if ((*stackhint != '\0') && (*endp == '\0')) { /* a valid number? */
if (hintval > 0) { /* reject bogus values. */
stacksize = (size_t) hintval;
}
}
}

#ifdef SDL_PASSED_BEGINTHREAD_ENDTHREAD
return SDL_CreateThreadWithStackSize(fn, name, stacksize, data, pfnBeginThread, pfnEndThread);
#else
return SDL_CreateThreadWithStackSize(fn, name, stacksize, data);
#endif
}

可见 SDL_CreateThread 调用了 SDL_CreateThreadWithStackSize,而 SDL_CreateThreadWithStackSize 又调用 src\thread\windows\SDL_systhread.c 中的 SDL_SYS_CreateThread,因为 Windows 平台有 _beginthreadex_endthreadex,所以最后是调用 _beginthreadex

1
2
3
4
5
6
7
8
9
10
11
12
13
/* thread->stacksize == 0 means "system default", same as win32 expects */
if (pfnBeginThread) {
unsigned threadid = 0;
thread->handle = (SYS_ThreadHandle)
((size_t) pfnBeginThread(NULL, (unsigned int) thread->stacksize,
RunThreadViaBeginThreadEx,
pThreadParms, flags, &threadid));
} else {
DWORD threadid = 0;
thread->handle = CreateThread(NULL, thread->stacksize,
RunThreadViaCreateThread,
pThreadParms, flags, &threadid);
}

其中 RunThreadViaBeginThreadEx 实际上是调用 RunThread:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static DWORD
RunThread(void *data)
{
pThreadStartParms pThreadParms = (pThreadStartParms) data;
pfnSDL_CurrentEndThread pfnEndThread = pThreadParms->pfnCurrentEndThread;
void *args = pThreadParms->args;
SDL_free(pThreadParms);
SDL_RunThread(args);
if (pfnEndThread != NULL)
pfnEndThread(0);
return (0);
}

static DWORD WINAPI
RunThreadViaCreateThread(LPVOID data)
{
return RunThread(data);
}

static unsigned __stdcall
RunThreadViaBeginThreadEx(void *data)
{
return (unsigned) RunThread(data);
}

从代码可见 RunThread 调用 SDL_RunThread,而 SDL_RunThread 内部由 SDL_TLSCleanup() 来调用析构函数:

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
30
31
32
33
34
35
void
SDL_RunThread(void *data)
{
thread_args *args = (thread_args *) data;
int (SDLCALL * userfunc) (void *) = args->func;
void *userdata = args->data;
SDL_Thread *thread = args->info;
int *statusloc = &thread->status;

/* Perform any system-dependent setup - this function may not fail */
SDL_SYS_SetupThread(thread->name);

/* Get the thread id */
thread->threadid = SDL_ThreadID();

/* Wake up the parent thread */
SDL_SemPost(args->wait);

/* Run the function */
*statusloc = userfunc(userdata);

/* Clean up thread-local storage */
SDL_TLSCleanup();

/* Mark us as ready to be joined (or detached) */
if (!SDL_AtomicCAS(&thread->state, SDL_THREAD_STATE_ALIVE, SDL_THREAD_STATE_ZOMBIE)) {
/* Clean up if something already detached us. */
if (SDL_AtomicCAS(&thread->state, SDL_THREAD_STATE_DETACHED, SDL_THREAD_STATE_CLEANED)) {
if (thread->name) {
SDL_free(thread->name);
}
SDL_free(thread);
}
}
}

八哥之神前传【13】

618 年,识界

圣仙山:两位娘子,待会儿去吾姐梦里和她告别。

李心觎:为啥不在视界亲自和她告别呢?

圣仙山:她远嫁黑龙江,吾在视界又不能飞,要跑很远纳!再说吾在视界还没娶妻,多没面子呀。

李心觎:嘻嘻嘻。

李冰月:哈哈哈!姐姐,咱们是去给相公撑场面的!

李神源梦中

圣仙山:神姐!

李神源:嗯?是仙弟呀。这两位是?

圣仙山:是小弟的妻子。

李冰月、李心觎:姐姐好!

李神源:好好好。两位弟妹灵秀如仙,仙弟什么时候得此艳福,真是好大的惊喜呀!

李冰月:好说好说。相公才是灵秀如仙,我们姐妹俩还不如他呢!

李心觎:是呀,神姐,夫君才是真仙。

李神源:哎呀,我这仙弟以前老和我说,他要一心求道,不想娶妻生子,还说未来的人越来越多都不结婚的。

圣仙山:姐!现在情况有变,当今皇帝受老钧托梦,要老爹组织一群谷阳人去南荒开疆传道,同行人中有技师文人,也有流民和被大赦的罪犯,还要途径苦境各地,和三教九流打交道,我是最佳带队人选。

李神源:此去危险重重,仙弟为何不让别人去呢?

圣仙山:除了传道,吾还有别的任务,要去寻找吾心中的道州德国,一处远离天灾人祸的修道圣地。

李冰月:神姐放心,相公已经修成八识神通,这事可以安全办成。

李神源:既然如此,为姐只能祝你一切顺利。我养了一只守护灵狐,让它陪你上路吧,风餐露宿有危险时,它会警示你。

圣仙山:谢谢神姐。

识界

李心觎:神姐梦里的灵狐好可爱耶。

李冰月:是呀,咱们在识界里找一只呗!

李心觎:走。

李冰月:相公,这只叫“纯狐连玉”,你在视界如果遇到它,它会认得你的。

李心觎:连玉,暂时就陪我们吧。

少曾读仙史,知有苏耽君。流望来南国,依然会昔闻。 泊舟问耆老,遥指孤山云。孤山郴郡北,不与众山群。 重崖下萦映,嶛峣上纠纷。碧峰泉附落,红壁树傍分。 选地今方尔,升天因可云。不才予窜迹,羽化子遗芬。 将览成麟凤,旋惊御鬼文。此中迷出处,含思独氛氲。

2003 年

陈因提:圣仙山带着一群中原人来到现在你家这个地方?

圣小开:对!仙山公在路上娶了胡小玉,才有现在的我。

陈因提:胡小玉很特别吗?娶别人不一样有你?

圣小开:万一不娶呢?那我不是生不出来!

陈因提:哈哈。你真是瞎操心。我比较担心他死在路上,而你担心他不娶!

圣小开:有神通在,明显死不了呀。料事如神,有啥危险或者人际关系的问题,都能预防。

陈因提:哦,后世人神化的吧!成王败寇。

圣小开:仙山公能找到这个好地方本身就很神了,台风地震海啸全被周围其他地方挡住,几乎没天灾,还远离政治和战乱,在当时可谓世外桃源。

陈因提:这是现实,暂且信你。那么,你有遗传到这八识神通吗?

圣小开:略懂略懂!八识神通分为被动接受和主动测算,我有时候也能接受到来自未来的预警,主动测算我是不会。而且主动测算会耗费心力,即所谓招天谴。仙山公测算多次,导致 35 岁就仙逝了。胡小玉就活了 42 岁,他们女儿活到 64 岁。

陈因提:算出啥了?老天爷这么急着收他?

圣小开:回归十界。

陈因提:啥意思?

圣小开:不是老天收他,是他终于算出怎么回归十界,便抛下肉身……

陈因提:等等,说人话!

圣小开:算出怎么去死!

陈因提:切!还以为啥高深的法门……睡了睡了。

挨踢人必备 Embody

故事从 2012 年开始

这一年在 59 号大网吧办公,总感觉桌子太矮椅子太烂……心神不宁,频繁想起一个故事:

某同事很低调,从来不炫耀自己的成就,突然有一天,他说:“我太突出了!”

一问,原来是腰间盘突出……

这一年,还是哥的稣决定攒钱买一把好的椅子。

存了几个月,终于买了一把保友金豪 E-AB-HAM。那时好多同事来围观,有 HR 来说了句:“比总经理用的还好!”,也有不少小伙伴说长得像医疗用的复健椅。

**但是这把椅子在稣心里并没有地位。**当时已经有智能手机的稣,从来没有拍过它,以致如今想查具体啥时候买的,都不那么确定。只记得是花 3800 元买的,最晚不会超过 2013-01-19 这个时间!

大约的购买时间

当时还很撒逼地把能选配的全给配上……实际使用证实左手边的书架几乎用不上,以致后来把它给拆下来了!脚垫也不常用,很少在公司午睡。

说起效果,那确实比公司的乐射椅子好很多,但也不是特别舒服。网布很透气,但在公司有空调的前提下,透气并不是那么必须。反而缺点就是它并没有根据屁股的造型来支托,简单地说有点勒腿,坐久了并不舒服。所以,后来又研究起 Embody。

开始了解 Embody

后来有土豪要买 Embody,稣跟着去体验了一下。2020 年,土豪又买了第二张……

直到 2020 年才有钱买

2020-06-10 下单乐歌升降桌 E3,180*80cm。靠自动提醒功能养成坐久了站会儿的习惯,结果反而让稣发现八哥就在金豪这把椅子上!网布还是个问题,坐 8 年已经变软一些,但依然有一种压迫大腿两侧的酸麻感。

吓得稣赶紧下单,买了介个:

Embody 廉价替代品

这下终于明白形状和支撑的重要性!果断存钱买 Embody!

存了一个月多的钱,再加上脑波给的一万块人民币,终于入手 Embody!

体会

  • Embody 可以调得比金豪更矮,适合的身高范围就更大,尤其对女性友好。腿、屁股和腰都舒服。

  • 一张 Embody 价格可以买金豪 3 张以上,但是肯定是没有 3 倍的舒服,高端的东西性价比自然比大众款低的多,使用价值只能在细节上轻微地增加。好在大腿的问题真的解决了。

  • 挨踢族一天坐在椅子上的时间是远超健康上限的。稣并不赞成通过买任何东西来装逼,除非这东西是真实用,所以贵。Embody 对很多人来说,可能就是装逼,但对挨踢人绝对不是。

并不是主角才有光环,而是有光环的才能当主角,这才是生活。——泥巴娃

  • 挨踢人就应该对自己好一点。

最后,一句话总结:要不是穷,真想再买一张!

八哥之神前传【12】

识界

李仙山,字太墟,号道识,是最后一名通过修行进入识界的人类,最为人津津乐道的是他娶了两位貌美如花的妻子:李冰月和李心觎。三人同在深山修炼,羡煞旁人。

李仙山:两位娘子,山顶那朵魔莲原来是老钧的智慧所化,吾已得其认可,纳入脑识。

李冰月:那相公还是相公吗?

李心觎:是呀,夫君不会鲸神魂裂吗?

李仙山:哈哈,不仅没有副作用,还助吾练就八识神通。

李心觎:听起来好厉害哦!有何用处呢?

李冰月:哎呀!相公的眼睛怎么流血了!

李仙山:咦?难道不能预测自己的未来?

李心觎:怎么回事也?赶紧擦擦。

李仙山:八识神通能够预知未来,吾刚刚测算视界中自己的未来,发现未来的视界大部分地方都实行一夫一妻制,吾之转世因此无法娶三个妻子。

李冰月:测算自己会破坏因果循环,必遭天谴,相公可别乱用。

李仙山:只要不测自己就没事。吾试过预测识界未来,发现识界将关闭千余年,后有邪灵借助科技的力量强行进入识界,后使视界的普通人不必修行亦可进入识界。可谓千年浩劫!

李冰月:此事非同小可,需召集众仙请示老钧。

李仙山:不必如此兴师动众,为夫一人已可召唤老钧。

李心觎:老钧居于十维之外,三千年来只见众人祈祷之法,却无人见过老钧现身。夫君真能召唤出来?

李仙山:老钧的意志无处不在,只要用八识神通与他纠缠就可以接受他的意识传播。来!

突然云雾缭绕的道州蓬山化入星辰,创世天尊老钧自虚无中发出一道引力波。

李仙山:小孙,拜见老祖,请老钧指示应对浩劫之方。

老钧:道之不传,识界将倾。回到视界,去南方寻找道州德国。

李仙山:领道旨。

老钧:天机不可泄露!切记不可再测算自己的未来。从今而后,改姓圣吧!

李仙山:这个姓小孙恐怕承受不起呀!再说不久前才从老改为李的,又改?

老钧:视界中的李伯阳会助你,让当朝皇帝赐姓。

李仙山:遵旨!

老钧消失前,喃喃道:那厮岂非汝乎?小孙杂……

回到道州蓬山。

圣仙山:两位娘子,为传道法,吾将离开中原,奔波于南荒之地,恐少有时间再入识界。

李冰月:相公是去干大事的,不用担心我们,我们会照顾好自己的。

李心觎:对呀,夫君偶尔来看我们就好。小别胜新婚,也许更好呢!

圣仙山:哈哈哈!

李冰月:姐姐,我们写几句诗送给相公吧!

李心觎:好呀。妹妹是大老婆,你先来。

李冰月:晓镜但愁云鬓改,夜吟应觉月光寒。蓬山此去无多路,青鸟殷勤为探看。

李心觎:昨夜星辰昨夜风,画楼西畔桂堂东。身无彩凤双飞翼,心有灵犀一点通。

圣仙山:好诗!好诗!都是你们未来的荥阳老乡写的!

李冰月、李心觎:哈哈哈。

圣仙山:再过两百多年义山就出生了!哈哈哈……

2003 年

陈因提:圣仙山就是你老祖宗?

圣小开:当然,所以我遗传了他的特异功能。

陈因提:哦……我有点困!你最好拿出能提神的证据。

圣小开:我偶尔也能进入识界!一般人我不告诉他。

陈因提:好啦好啦,那你说说看,你都看到啥?

圣小开:众生皆苦!

陈因提:阿弥陀佛。我裤子都脱了,你给我念经?

圣小开:耶!你怎么知道我还写过一本经书。

陈因提:拿出来,我帮你烧给你老祖宗。

圣小开:切!这本《八哥之神创世手稿》十分珍贵,里面记载着进入识界的修炼法门。

陈因提:一本九毛九?我买,明天给你一百块,免找。

圣小开:给你也好,要是我突然遭天谴,起码这绝学不会就此失传。拿去,好好保管。

陈因提:哟,字体工整,比写情书还认真!那我就收了,放心吧,我当宝一样收藏。