0%

BusyWaitableCondition是忙寻版的可等待条件

WaitableCondition是睡眠版的可等待条件

两者实现相同功能,只是忙寻版会占用一个cpu core,而睡眠版会陷入睡眠在需要时才被唤醒继续执行。

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

#include <iostream>
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <thread>
#include <future>

using namespace std;

class BusyWaitableCondition {
public:
void Wait() {
while(!_flag);
}

void Notify() {
_flag = true;
}

void Reset() {
_flag = false;
}

private:
std::atomic<bool> _flag = false;
};

class WaitableCondition {
public:
void Wait() {
std::unique_lock<std::mutex> lock(_mutex);
if(!_flag) {
_cv.wait(lock, [this](){ return _flag; });
}
}

void Notify() {
do {
std::lock_guard<std::mutex> lock(_mutex);
_flag = true;
} while(false);
_cv.notify_all();
}

void Reset() {
std::lock_guard<std::mutex> lock(_mutex);
_flag = false;
}

private:
std::mutex _mutex;
std::condition_variable _cv;
bool _flag = false;
};

int main() {
WaitableCondition c1;
auto fut1 = std::async(std::launch::async, [&]{
c1.Wait(); // 等待条件1满足才继续执行
cout << "Hello 1" << endl;
});
auto fut2 = std::async(std::launch::async, [&]{
c1.Wait(); // 等待条件1满足才继续执行
cout << "Hello 2" << endl;
});

this_thread::sleep_for(chrono::seconds(1));
cout << "1" << endl;
cout << "2" << endl;
cout << "3" << endl;
cout << "4" << endl;
cout << "5" << endl;
c1.Notify(); // 通知条件1满足了

return 0;
}

上面的例子中有定义Reset方法,但是没有演示,它的作用是将条件设置为否,以便可以再次等待,然后可以重新触发。

如果只需要等待一个一次性事件而不需要Reset方法那么可以用std::promise+std::future进行更简单的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class WaitableEvent {
public:
WaitableEvent(): _fut(_promise.get_future()) {

}

void Wait() const {
_fut.wait();
}

void Notify() {
_promise.set_value();
}

private:
std::promise<void> _promise;
std::future<void> _fut;
};

sscanf是C语言库函数,作用是从字符串中进行格式化解析。

例如在读文本文件时先用fgets把整行读入buffer再用sscanf进一步解析buffer的内容。

头文件

1
#include <stdio.h>
1
#include <cstdio>

函数原型

1
int sscanf(const char *str, const char *format, ...);

参数

  • str – 以’\0’结尾的字符串,解析的数据源。
  • format – 格式化字符串,描述解析的规则
  • args… – 对应format参数,提供需要读取的变量的地址

返回值

返回读取到的参数的个数。

当没有读入任何数据时,可能是因为走到了字符串的结尾,也可能是因为当前将要读入的字符串无法转换为指定的格式。

如果因为读到了字符串结尾则返回EOF,即-1,如果是因为其他原因没有读入任何参数,则返回0。

样例

1
2
3
4
5
6
int i;
float f;
double ff;
char buf[128];
char ch;
sscanf("123 3.14 2.71828 \t\r\n Hello x", "%d %f %lf %s %c", &i, &f, &ff, buf, &ch);

说明

%d表示读取一个整数

%f表示读取一个浮点数

%lf表示读取一个双精度浮点

%s表示读取一个字符串

%c 表示读取一个字符

在format中空格表示跳过任意多的空白字符(空白字符包括' ', '\t','\r','\n'

%d %f %lf %s这些格式自带跳过空白字符即使前面没有空格。而%c不会自动跳过空白字符。

高级用法

指定读取字符串的长度

1
2
char buf[5];
sscanf("Hello", "%4s", buf);

我们的buf只有5个字节大小,考虑到sscanf读取的字符串会在结尾加上’\0’,所以我们最多读取4个自己,可以在%s中间加上希望读取的长度。

这同样适用于%d %f %lf仍然表示最多读取的字符串长度,只是会在读取后会将字符串转换成相应类型的数据。

丢弃读取内容

%后面加上*可以丢弃当前%表示的格式读取到的内容,即后面的参数列表不需要传入接收此项的数据地址。

1
2
char buf[5];
sscanf("Hello", "%*4s %s", buf);

丢弃了4个字符,再跳过任意多空白字符,然后读入一个字符串,所以这时读入的buf里只有"o"

读取指定的字符

通过%[]指定读取的字符,可逐个列出或通过-指定范围,也可以指定多个范围。

1
2
3
char buf1[128];
char buf2[128];
sscanf("helloWorld", "%[a-z]%s", buf1, buf2);

读取除指定字符外的字符

%[...]类似,通过%[^...]可以反向指定需要读取的字符,即除了什么字符以外的所有字符。

非常适合读取用某种字符分割的字符串。

1
2
3
char buf1[128];
char buf2[128];
sscanf("Hello,World", "%[^,]%s", buf1, buf2);

获取当前读取到的位置

通过在format中需要确定的位置插入%n获取

1
2
3
4
5
char buf1[128];
int n1;
char buf2[128];
int n2;
sscanf("Hello,World", "%[^,]%n%s%n", buf1, &n1, buf2, &n2);

n1 = 5, n2 = 11,注意%n实际上不是表示前一个%读入了多少字符,而是表示当前已经读到的位置,但是如果有需要我们可以通过这个参数算出每个参数读入了多少字符。

综合应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const char* p = "Hello,World,123.45,, Hello world   ,ttt";
int n;
char buf[128] = {0};
int r;
while(*p != '\0' && (r = sscanf(p, "%127[^,]%n", buf, &n)) != EOF) {
if(r == 0) {
buf[0] = '\0';
n = 0;
}
printf("%s\n", buf);
p += n;
if(sscanf(p, "%*c%n", &n) == EOF) break;
p += n;
}

这里演示了读入以,分割的多字段的情况。其中有一点需要说明,while条件里的sscanf我们是期望读当前位置到下一个,前的字符串,而当r==0时,说明当前就指在下一个,上,此时读入了0个参数,而buf中会保留这之前的内容,所以将其手动置空,而n也同样没有被修改,所以我们也将其置0,以便后面的输出和移动可以用相同的代码统一处理。

通过这种方式sscanf完全可以替代strtok,而且避免了strtok使用了内部静态变量的带来的线程安全问题。

CMakeList.txt中,LINK_DIRECTORIES和rpath-link都是用来指定链接库路径的,但是有区别,看来具体例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 设置库路径
LINK_DIRECTORIES(
${PROJECT_ROOT_DIR}/libs/kTrade/libs
${PROJECT_ROOT_DIR}/libs/kTrade/libs/ama_3_9_8 # (1)
)

# ...省略若干...

TARGET_LINK_LIBRARIES(AmaMD
dl Engine rt pthread
kTradeUtils
amaquote g3log
ama
-Wunused-function
-Wl,-rpath,'$ORIGIN'/libs/Ama,-rpath,'$ORIGIN'/libs,-rpath-link,${PROJECT_ROOT_DIR}/libs/kTrade/libs/ama_3_9_8 # (2)
)

在注释(1)处的目录下面有libama.so库和其依赖的若干.so
如果只加注释(1)处的路径而不加注释(2)处的-rpath-link参数,则会报错找不到通过ama间接依赖的一系列.so的问题,
如果只加注释(2)处的-rpath-link参数而不加注释(1)处的路径,则会报错找不到ama库本身的问题。

解决方案是要么两处都加上,要么只加(1)处的路径,但是需要在TARGET_LINK_LIBRARIES中写出所有通过ama间接依赖到的库名字,这样就都会通过(1)处的路径找到它们并使它们参与到链接中。
总之,直接链接的库只会通过(1)处指定的路径查找,而间接链接到的库只会通过(2)处指定的路径进行查找。

有时我们想查看C/C++代码对应的汇编代码,可以通过gcc/g++的编译选项-S来生成,但这样的代码可读性较差。

可以增加-fverbose-asm来增加可读性

1
g++ -O0 -o temp.s temp.cpp -std=c++17 -g -S -fverbose-asm

还有另一个方法,先生成目标文件再用objdump进行反汇编,我发现这样生成的汇编代码可读性更好。

1
g++ -O0 -o temp temp.cpp -std=c++17 -g && objdump -S -t -D temp > temp.s

之前写过在Python中监视卡死崩溃退出并打印卡死处的调用堆栈

在此记录一下C++的版本,不过没有在代码层面实现堆栈打印,可以通过core dump和gdb来查看崩溃时的堆栈

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
// WatchDog.h
#pragma once

#include <cstdint>
#include <mutex>
#include <thread>
#include <atomic>
#include <condition_variable>

class WatchDog {
public:
WatchDog(int timeout=10, bool echo=false); // seconds
~WatchDog() { stop(); }
void stop();
void kick();

private:
void dog();
void bark();

private:
const int _timeout;
const int _echo;
std::atomic<int64_t> _last_kicked_ts;

std::mutex _mutex;
bool _stopped; // protected by _mutex
std::condition_variable _cond; // protected by _mutex

std::thread _dog;
};

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// WatchDog.cpp
#include "WatchDog.h"
#include <iostream>

using namespace std;

namespace {

int64_t get_gmtime_us(){
std::chrono::system_clock clock;
return std::chrono::duration_cast<std::chrono::microseconds>(
clock.now().time_since_epoch()).count();
}

} // namespace


WatchDog::WatchDog(int timeout, bool echo)
: _timeout(timeout)
, _echo(echo)
, _last_kicked_ts(get_gmtime_us())
, _stopped(false)
, _dog(&WatchDog::dog, this) {
}


void WatchDog::stop() {
do {
std::unique_lock<std::mutex> lock(_mutex);
_stopped = true;
_cond.notify_one(); // wake up the dog
} while(false);

try {
_dog.join();
}
catch (...) {
// it's ok, could already be dead
}
}

void WatchDog::kick() {
_last_kicked_ts = get_gmtime_us();
}

void WatchDog::dog() {
std::unique_lock<std::mutex> lock(_mutex);
while (true) {
if (_stopped) return;

int64_t ts = get_gmtime_us();
if (ts - _last_kicked_ts > _timeout * 1000000) {
bark();
}

if (_echo) {
std::cout << "Successful dog check"
<< " [ts] " << ts
<< " [last_kicked_ts] " << _last_kicked_ts << std::endl;
}

// wake up when notified, or every N seconds
int n = std::max(_timeout / 3, 1);
_cond.wait_for(lock, std::chrono::seconds(n));
}
}

void WatchDog::bark() {
if (_echo) {
std::cout << "\n!!!!! WATCH DOG FAILURE TRIGGERED !!!!!" << std::endl;
}
abort();
}

kTrade是一个交易柜台中间层,将不同券商提供的不同交易柜台封装成统一接口,以include+lib的方式提供统一的对接方案,交易系统只需要对接kTrade提供的统一接口,由kTrade处理各个柜台的区别。

QuoteApi

kTrade QuoteApi架构图

TradeApi

kTrade TradeApi架构图

iperf3用于测试两台主机间的带宽,主要参数如下:

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
-p, --port #,Server 端监听、Client 端连接的端口号; 
-f, --format [kmgKMG],报告中所用的数据单位,Kbits, Mbits, KBytes, Mbytes;
-i, --interval #,每次报告的间隔,单位为秒;
-F, --file name,测试所用文件的文件名。如果使用在 Client 端,发送该文件用作测试;如果使用在 Server 端,则是将数据写入该文件,而不是丢弃;
-A, --affinity n/n,m,设置 CPU 亲和力;
-B, --bind ,绑定指定的网卡接口;
-V, --verbose,运行时输出更多细节;
-J, --json,运行时以 JSON 格式输出结果;
--logfile f,输出到文件;
-d, --debug,以 debug 模式输出结果;
-v, --version,显示版本信息并退出;
-h, --help,显示帮助信息并退出。
Server 端参数:
-s, --server,以 Server 模式运行;
-D, --daemon,在后台以守护进程运行;
-I, --pidfile file,指定 pid 文件;
-1, --one-off,只接受 1 次来自 Client 端的测试,然后退出。
Client 端参数
-c, --client ,以 Client 模式运行,并指定 Server 端的地址;
-u, --udp,以 UDP 协议进行测试;
-b, --bandwidth #[KMG][/#],限制测试带宽。UDP 默认为 1Mbit/秒,TCP 默认无限制;
-t, --time #,以时间为测试结束条件进行测试,默认为 10 秒;
-n, --bytes #[KMG],以数据传输大小为测试结束条件进行测试;
-k, --blockcount #[KMG],以传输数据包数量为测试结束条件进行测试;
-l, --len #[KMG],读写缓冲区的长度,TCP 默认为 128K,UDP 默认为 8K;
--cport ,指定 Client 端运行所使用的 TCP 或 UDP 端口,默认为临时端口;
-P, --parallel #,测试数据流并发数量;
-R, --reverse,反向模式运行(Server 端发送,Client 端接收);
-w, --window #[KMG],设置套接字缓冲区大小,TCP 模式下为窗口大小;
-C, --congestion ,设置 TCP 拥塞控制算法(仅支持 Linux 和 FreeBSD );
-M, --set-mss #,设置 TCP/SCTP 最大分段长度(MSS,MTU 减 40 字节);
-N, --no-delay,设置 TCP/SCTP no delay,屏蔽 Nagle 算法;
-4, --version4,仅使用 IPv4;
-6, --version6,仅使用 IPv6;
-S, --tos N,设置 IP 服务类型(TOS,Type Of Service);
-L, --flowlabel N,设置 IPv6 流标签(仅支持 Linux);
-Z, --zerocopy,使用 “zero copy”(零拷贝)方法发送数据;
-O, --omit N,忽略前 n 秒的测试;
-T, --title str,设置每行测试结果的前缀;
--get-server-output,从 Server 端获取测试结果;
--udp-counters-64bit,在 UDP 测试包中使用 64 位计数器(防止计数器溢出)。

用例

服务端

1
iperf3 -s -p 30088

客户端

1
iperf3 -c server_ip -p 30088

找到C:\Program Files\Sublime Text 3\Packages\C++.sublime-package
复制到一个有写权限的目录,重命名为C++.sublime-package.zip
打开压缩包找到C Single File.sublime-buildC++ Single File.sublime-build

分别对应的C和C的单文件构建命令,以C为例,打开编辑,找到其中的

1
"shell_cmd": "g++ ***"

有两条,应该一条是编译一条是编译并运行,分别增加需要的编译参数即可,比如在g++的后面加上--std=c++17
把修改更新进压缩包,重命名回C++.sublime-package并覆盖回原来的位置即可。

使用gdb启动程序

1
gdb <可执行文件名>

启动后执行运行命令run可以简写为r

程序就会从入口开始运行,也可以带上命令行参数运行run [参数1] [参数2] ...

参看当前函数调用堆栈

1
backtrace

可以简写为bt,在程序崩溃后查看当前崩溃的位置和调用堆栈,这估计是gdb最常用的命令。

GDB常用的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
i b  // info breakpoint
b 10 // set breakpoint at line 10
b func // set breakpoint at function "func"
b A::A // 设置成员函数的断点
b func(int) // 设置函数断点时指定函数类型,以便在有重载时区分
c // continue
p x // print the value of x
i ar // info args 查看函数参数的值
i lo // info locals 查看所有局部变量的值
bt // backtrace 查看调用栈
f 1 // 设置栈针到level 1,断点停的地方是level 0,level 1就是上一层,根据bt返回的结果设置,切换栈针后再用i ar和i lo查看当前栈针的变量信息
i thr // info thread 查看线程信息
thr 2 // thread 2 切换当前线程到id:2,根据i thr的返回结果设置
q // quit GDB

配置core dump

如果程序不是通过gdb启动运行的我们也想查看它崩溃时的调用堆栈,则可以通过core dump文件,它会保留崩溃时的现场。首先我们确保运行的可执行文件无论是Debug版还是Release版应该携带了调试符号,即编译选项中加入了-g或-ggdb。然后通过下面的方式启用core dump文件生成:

确保apportsystemd-coredump两个服务存在

1
2
dpkg -l | grep apport
dpkg -l | grep systemd-coredump

如果不存在则安装

1
2
apt install apport 
apt install systemd-coredump

如果存在则确保在运行

1
2
systemctl status apport
systemctl status systemd-coredump.socket

如果没有运行则启动

1
2
systemctl start apport
systemctl start systemd-coredump.socket

默认的core dump文件会生成在/var/lib/systemd/coredump/目录下,为了方便调试我们修改到可执行文件同目录

运行程序前在shell窗口或者shell脚本内先执行

1
2
3
ulimit -c unlimited
export COREDIR=$(pwd)
echo "$COREDIR/core.%e.%p" | sudo tee /proc/sys/kernel/core_pattern

之后运行的程序崩溃时就会在同目录生成core dump文件。文件名是core.<可执行文件名>.<进程ID>

使用core dump

所谓使用core dump,就是通过gdb和core dump文件恢复崩溃时的现场,以便查看崩溃时的函数调用堆栈或者变量值。

用法:

1
gdb <可执行文件名> <core dump文件名>

然后就可以使用

1
bt

查看崩溃现场的调用堆栈了。

主动生成运行中进程的core dump

这种需求主要发生在进程卡死,想知道卡在何处时。

1
gcore -o <core dump文件名> <pid>

可以主动生成core dump文件,这个操作不会杀死进程,如果有需要可手动杀死。

然后参照[使用core dump](#使用core dump)的步骤去查看调用堆栈就可以了

C++11引入了新的随机数生成器mt19937

mt是因为这个伪随机数产生器基于Mersenne Twister算法。
19937是因为产生随的机数的周期长,可达到2^19937-1

虽然周期很长,但这是在种子固定后依次生成2^19937-1个数才能保证不重复,实际的应用是需要重启或者多开的,每次重启种子会重设,迭代次数又重新开始,如果我们把种子设成固定值显然每次生成的序列和前一次都会是重复的,如果我们用时间作为种子,这样可以让每次的序列都从一个随机位置开始迭代,但是这样也就无法保证本次周期内生成的数不会和之前的某次生成的重复,或许不巧的情况下生成了10个就重复了,也是有极小概率的。

为了解决这个问题,我们可以更进一步的,在用时间作为随机种子的基础上再在前面拼接当前时间,比如拼接“%Y%m%d%H%M%S”精确到秒,因为这串时间必然是递增的,即使后面不巧重复了,拼接后的整体也是唯一的,因此只要确保在一秒内没有重复即可,而在一秒内除非程序重启,否则由mt19937的周期保证不会重复。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <chrono>
#include <sstream>
#include <random>
#include <mutex>
#include <iomanip>

inline
std::string gen_uuid() {
static std::mutex s_mutex;
std::lock_guard lock(s_mutex);
static std::mt19937 seed_engine(
std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock().now().time_since_epoch()).count()
);
std::stringstream ss;
int64_t t = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock().now().time_since_epoch()).count();
auto tm = std::localtime(&t);
std::uniform_int_distribution<std::size_t> random_gen;
std::size_t value = random_gen(seed_engine);
ss << std::put_time(tm, "%Y%m%d%H%M%S") << std::hex << std::setw(16) << std::setfill('0') << value;
return ss.str();
}

注意到函数整体用mutex加了锁,首先我不确定random_gen(seed_engine)是否线程安全,其次我确定std::localtime不是线程安全的,因为其返回一个std::tm结构体的指针,可能是指向std::localtime内的静态成员变量,我不清楚为什么STL这样设计这个函数。

进一步改进方式,如果考虑到程序多开,可以给每个程序一个ID,在生成时传递gen_uuid(app_id)然后拼接到生成的随机字符串中。

Python版本,后面通过{:016x}截断16位16进制数是为了和C++生成的保持长度一致

1
2
3
4
5
6
7

import datetime
import uuid

def gen_uuid():
return "{}{:016x}".format(datetime.datetime.now().strftime("%Y%m%d%H%M%S"), (uuid.uuid4().int&(0xFFFFFFFFFFFFFFFF)) )