Senlin's Blog


  • 分类

  • 归档

  • 标签

  • 关于

理解 C++ 的 Memory Order

发表于 2017-12-04   |   分类于 并发编程   |   阅读次数

为什么需要 Memory Order

  如果不使用任何同步机制(例如 mutex 或 atomic),在多线程中读写同一个变量,那么,程序的结果是难以预料的。简单来说,编译器以及 CPU 的一些行为,会影响到程序的执行结果:

  • 即使是简单的语句,C++ 也不保证是原子操作。
  • CPU 可能会调整指令的执行顺序。
  • 在 CPU cache 的影响下,一个 CPU 执行了某个指令,不会立即被其它 CPU 看见。

  原子操作说的是,一个操作的状态要么就是未执行,要么就是已完成,不会看见中间状态。例如,在 C++11 中,下面程序的结果是未定义的:

1
2
3
4
int64_t i = 0; // global variable
Thread-1: Thread-2:
i = 100; std::cout << i;

  C++ 并不保证i = 100是原子操作,因为在某些 CPU Architecture 中,写入int64_t需要两个 CPU 指令,所以 Thread-2 可能会读取到i在赋值过程的中间状态。


  另一方面,为了优化程序的执行性能,CPU 可能会调整指令的执行顺序。为阐述这一点,下面的例子中,让我们假设所有操作都是原子操作:

1
2
3
4
5
6
7
int x = 0; // global variable
int y = 0; // global variable
Thread-1: Thread-2:
x = 100; while (y != 200)
y = 200; ;
std::cout << x;

  如果 CPU 没有乱序执行指令,那么 Thread-2 将输出100。然而,对于 Thread-1 来说,x = 100;和y = 200;这两个语句之间没有依赖关系,因此,Thread-1 允许调整语句的执行顺序:

1
2
3
Thread-1:
y = 200;
x = 100;

  在这种情况下,Thread-2 将输出0或100。


  CPU cache 也会影响到程序的行为。下面的例子中,假设从时间上来讲,A 操作先于 B 操作发生:

1
2
3
4
int x = 0; // global variable
Thread-1: Thread-2:
x = 100; // A std::cout << x; // B

  尽管从时间上来讲,A 先于 B,但 CPU cache 的影响下,Thread-2 不能保证立即看到 A 操作的结果,所以 Thread-2 可能输出0或100。


  从上面的三个例子可以看到,多线程读写同一变量需要使用同步机制,最常见的同步机制就是std::mutex和std::atomic。然而,从性能角度看,通常使用std::atomic会获得更好的性能。
  C++11 为std::atomic提供了 4 种 memory ordering:

  • Relaxed ordering
  • Release-Acquire ordering
  • Release-Consume ordering
  • Sequentially-consistent ordering

  默认情况下,std::atomic使用的是 Sequentially-consistent ordering。但在某些场景下,合理使用其它三种 ordering,可以让编译器优化生成的代码,从而提高性能。

Relaxed ordering

  在这种模型下,std::atomic的load()和store()都要带上memory_order_relaxed参数。Relaxed ordering 仅仅保证load()和store()是原子操作,除此之外,不提供任何跨线程的同步。
  先看看一个简单的例子:

1
2
3
4
5
6
std::atomic<int> x = 0; // global variable
std::atomic<int> y = 0; // global variable
Thread-1: Thread-2:
r1 = y.load(memory_order_relaxed); // A r2 = x.load(memory_order_relaxed); // C
x.store(r1, memory_order_relaxed); // B y.store(42, memory_order_relaxed); // D

  执行完上面的程序,可能出现r1 == r2 == 42。理解这一点并不难,因为编译器允许调整 C 和 D 的执行顺序。如果程序的执行顺序是 D -> A -> B -> C,那么就会出现r1 == r2 == 42。


  如果某个操作只要求是原子操作,除此之外,不需要其它同步的保障,就可以使用 Relaxed ordering。程序计数器是一种典型的应用场景:

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
#include <cassert>
#include <vector>
#include <iostream>
#include <thread>
#include <atomic>
std::atomic<int> cnt = {0};
void f()
{
for (int n = 0; n < 1000; ++n) {
cnt.fetch_add(1, std::memory_order_relaxed);
}
}
int main()
{
std::vector<std::thread> v;
for (int n = 0; n < 10; ++n) {
v.emplace_back(f);
}
for (auto& t : v) {
t.join();
}
assert(cnt == 10000); // never failed
return 0;
}

Release-Acquire ordering

  在这种模型下,store()使用memory_order_release,而load()使用memory_order_acquire。这种模型有两种效果,第一种是可以限制 CPU 指令的重排:

  • 在store()之前的所有读写操作,不允许被移动到这个store()的后面。
  • 在load()之后的所有读写操作,不允许被移动到这个load()的前面。

  除此之外,还有另一种效果:假设 Thread-1 store()的那个值,成功被 Thread-2 load()到了,那么 Thread-1 在store()之前对内存的所有写入操作,此时对 Thread-2 来说,都是可见的。
  下面的例子阐述了这种模型的原理:

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
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
std::atomic<bool> ready{ false };
int data = 0;
void producer()
{
data = 100; // A
ready.store(true, std::memory_order_release); // B
}
void consumer()
{
while (!ready.load(std::memory_order_acquire)) // C
;
assert(data == 100); // never failed // D
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}

  让我们分析一下这个过程:

  • 首先 A 不允许被移动到 B 的后面。
  • 同样 D 也不允许被移动到 C 的前面。
  • 当 C 从 while 循环中退出了,说明 C 读取到了 B store()的那个值,此时,Thread-2 保证能够看见 Thread-1 执行 B 之前的所有写入操作(也即是 A)。
阅读全文 »

Kubernetes 入门指南

发表于 2017-11-27   |   分类于 虚拟化   |   阅读次数

Pod 的介绍

  在 Kubernetes 中,Pod 是最小的调度单元,Pod 其实就是一个容器组,它可以包含一个或多个容器(Pod 翻译为豆荚,一个豆荚可以有多个豆子)。对于一个 Pod 来说,它会运行在某个宿主机上,当需要调度的时候,Kubernetes 会将 Pod 作为一个整体进行调度。
  由于整个 Pod 是运行在一个宿主机上的,所以 Pod 中的所有容器共享这些资源:

  • 所有容器共享同一个 IP。
  • 所有容器共享同一个 hostname。
  • 当某个 Volume 被挂载到 Pod 的文件系统时,这个 Volume 可以被多个容器共享。
  • 容器之间可以通过 System V IPC 或 POSIX 消息队列进行通信。

  思考一下,什么样的容器才适合放到同一个 Pod 中呢?

Pod 的配置

  可以使用 yaml 配置文件去描述 Pod,并写清楚我们希望 Pod 达到的状态:

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
ports:
- containerPort: 80

  使用下面的命令可以创建 Pod,用于运行 Nginx:

1
$ kubectl apply -f pod-nginx.yaml

  也可以获取 Pod 的运行状态:

1
2
3
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 8m

  如果想要获取 Pod 中所有容器的运行状态,可以使用:

1
$ kubectl get pods nginx -o json

  如果想获得 Pod 的更多详细信息(包括所有容器的信息,以及和 Pod 相关的事件),可以使用:

1
$ kubectl describe pods nginx

  删除 Pod 时,可以根据 Pod 的名称删除指定的 Pod:

1
$ kubectl delete pods/nginx

  也可以根据 yaml 配置文件删除 Pod:

1
$ kubectl delete -f pod-nginx.yaml

Pod 的调试

  在运行上面的 Nginx Pod 之后,如何判断 Nginx 正常工作呢?Kubernetes API 提供了端口转发的功能,可以在另一个终端执行下面的命令:

1
$ kubectl port-forward nginx 8000:80

  上面的命令会将localhost:8000的请求转发到 Nginx Pod 的80端口。可以验证一下:

1
$ curl localhost:8000

  如果没有差错,这个命令会输出 Nginx 首页的内容。


  另一种有效的调试手段,就是进入到容器内部去实地考察:

1
$ kubectl exec -it nginx bash

  如果 Pod 中有多个容器,可以使用-c参数指定具体的容器:

1
$ kubectl exec -it nginx bash -c nginx

  当然,更加经常使用的调试手段,就是观察容器的输出日志:

1
$ kubectl logs nginx

Pod 的健康检查

  默认情况下,Kubernetes 会开启进程的健康检查:如果 Kubernetes 检测到容器里面的进程退出了,那么它就会重启这个容器。
  只有进程级别的健康检查是远远不够的,设想一下,假设程序发生死锁了,此时进程仍然在运行,但是它已经不能正确输出响应了。为解决这个问题,还需要在应用层实现健康检查。
  Kubernetes 提供了三种类型的健康检查:

  • HTTP 健康检查:向容器的 HTTP 接口发起 HTTP 请求,如果返回的状态码在 [200, 400) 之间,就认为请求是成功的。
  • TCP 健康检查:与容器的某个 port 建立 TCP 连接,如果连接建立成功,则说明容器是健康的。
  • Exec 健康检查:在容器里面执行一个命令,如果返回 0 则认为是成功的,否则失败。

  需要注意的是,健康检查是容器级别的,而不是 Pod 级别的。这意味着一个 Pod 里面的每个容器,都需要配置不同的健康检查规则。
  下面的例子使用的是 HTTP 健康检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.7.9
livenessProbe:
httpGet:
path: /index.html
port: 80
initialDelaySeconds: 5
timeoutSeconds: 1
periodSeconds: 10
failureThreshold: 3
ports:
- containerPort: 80

  下面解释一下各项配置的意义:

  • initialDelaySeconds: 5:使用 5 秒用来等待 Pod 的初始化。
  • timeoutSeconds: 1:为 HTTP 请求设置超时时间为 1 秒。
  • periodSeconds: 10:每 10 秒执行一次健康检查。
  • failureThreshold: 3:设置失败的阈值,如果超过 3 次健康检查都失败了,容器就会被重启。
阅读全文 »

使用 Google Test 测试框架

发表于 2017-10-08   |   阅读次数

如何安装

  gtest 是谷歌的 C++ 单元测试框架,下面是安装步骤:

1
2
3
4
5
6
7
$ git clone https://github.com/google/googletest.git
$ cd googletest
$ mkdir build
$ cd build
$ cmake ..
$ make
$ sudo make install

  安装 gtest 之后,要怎样在 CMake 中使用 gtest 呢?让我们用一个简单的例子演示一下,首先编写项目的 CMakeLists.txt 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cmake_minimum_required (VERSION 2.8)
project(code-for-blog)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -std=c++11 -Wall")
find_package(GTest REQUIRED)
find_package(Threads REQUIRED)
include_directories(${GTEST_INCLUDE_DIRS})
add_executable(MyTests test.cpp)
target_link_libraries(MyTests ${GTEST_BOTH_LIBRARIES})
target_link_libraries(MyTests ${CMAKE_THREAD_LIBS_INIT})
add_test(Test MyTests)
enable_testing()

  接着编写一个简单的单元测试文件 test.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <gtest/gtest.h>
#include <numeric>
#include <vector>
// 测试集为 MyTest,测试案例为 Sum
TEST(MyTest, Sum)
{
std::vector<int> vec{1, 2, 3, 4, 5};
int sum = std::accumulate(vec.begin(), vec.end(), 0);
EXPECT_EQ(sum, 15);
}
int main(int argc, char *argv[])
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

  编译好项目之后,使用命令make test就可以执行单元测试了:

1
2
3
4
5
6
7
8
9
$ make test
Running tests...
Test project /Users/senlin/my-test/build
Start 1: Test
1/1 Test #1: Test ............................. Passed 0.01 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) = 0.01 sec

阅读全文 »

使用 Google 的 glog 日志库

发表于 2017-10-07   |   阅读次数

如何安装

  glog 是一个 C++ 日志库,它提供 C++ 流式风格的 API。在安装 glog 之前需要先安装 gflags,这样 glog 就可以使用 gflags 去解析命令行参数(可以参见 gflags 安装教程)。下面是 glog 的安装步骤:

1
2
3
4
5
6
$ git clone https://github.com/google/glog.git
$ cd glog
$ mkdir build
$ cmake ..
$ make
$ sudo make install

  安装之后要怎么使用 glog 呢?如果程序是使用 CMake 构建的,那么只要在 CMakeListsx.txt 里面加上下面几行配置就可以了:

1
2
3
4
find_package (glog 0.3.5 REQUIRED)
add_executable (main main.cpp)
target_link_libraries (main glog::glog)

日志级别

  glog 支持四种日志级别,INFO、WARNING、ERROR和FATAL。默认情况下,在打印完FATAL日志之后,程序将会终止。ERROR和FATAL的日志除了会写到日志中,还会输出到 stderr。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <gflags/gflags.h>
#include <glog/logging.h>
int main(int argc, char* argv[])
{
// 解析命令行参数
gflags::ParseCommandLineFlags(&argc, &argv, true);
// 初始化日志库
google::InitGoogleLogging(argv[0]);
LOG(ERROR) << "Hello, World!";
}

阅读全文 »

使用 Google gflags 解析命令行参数

发表于 2017-10-07   |   阅读次数

如何安装

  gflags 是一个 C++ 库,用于解析命令行参数。下面是安装步骤:

1
2
3
4
5
6
7
$ git clone https://github.com/gflags/gflags.git
$ cd gflags
$ mkdir build-dir
$ cd build-dir
$ cmake ..
$ make
$ sudo make install

  安装之后要怎么使用 gflags 呢?如果程序是使用 CMake 构建的,那么只要在 CMakeListsx.txt 里面加上下面几行配置就可以了:

1
2
3
4
5
find_package (gflags REQUIRED)
include_directories (${gflags_INCLUDE_DIR})
add_executable (main main.cpp)
target_link_libraries (main gflags)

阅读全文 »
1…345…13

高性能

61 日志
13 分类
14 标签
GitHub 知乎
© 2015 - 2022
由 Hexo 强力驱动
主题 - NexT.Mist
  |   总访问量: