Skip to content

Commit 6d667f1

Browse files
committed
1. 创建阅读须知
2. 修改第二章空格 3. 修改第四章部分示例代码与描述 4. 更新 web 的目录 5. Gitbook 目录更新
1 parent 56d42a6 commit 6d667f1

File tree

6 files changed

+57
-13
lines changed

6 files changed

+57
-13
lines changed

.vuepress/config.js

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export default defineUserConfig({
1414
theme: hopeTheme({
1515
sidebar: [
1616
{ text: '首页', link: '/', },
17+
{ text: '阅读须知', link: tutorialPath + `README` },
1718
{ text: '基本概念', link: tutorialPath + '01基本概念', },
1819
{ text: '使用线程', link: tutorialPath + '02使用线程', },
1920
{ text: '共享数据', link: tutorialPath + '03共享数据', },

SUMMARY.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Summary
22

3-
* [介绍](README.md)
3+
* [首页](README.md)
4+
* [阅读须知](md/README.md)
45
* [基本概念](md/01基本概念.md)
56
* [使用线程](md/02使用线程.md)
67
* [共享数据](md/03共享数据.md)

md/02使用线程.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@ void test(){
674674
675675
传入可调用对象以及参数,构造 `std::thread` 对象,启动线程,而线程对象拥有了线程的所有权,线程是一种系统资源,所以可称作“*线程资源*”。
676676
677-
std::thread 不可复制。两个 std::thread 对象不可表示一个线程,std::thread 对线程资源是独占所有权。而**移动**操作可以将一个 `std::thread` 对象的线程资源所有权转移给另一个 `std::thread` 对象。
677+
std::thread 不可复制。两个 std::thread 对象不可表示一个线程,std::thread 对线程资源是独占所有权。而**移动**操作可以将一个 `std::thread` 对象的线程资源所有权转移给另一个 `std::thread` 对象。
678678
679679
```cpp
680680
int main() {

md/04同步操作.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,7 @@ std::shared_future<std::string>sf{ p.get_future() }; // 隐式转移所有权
757757
758758
阻塞调用会将线程挂起一段(不确定的)时间,直到对应的事件发生。通常情况下,这样的方式很好,但是在一些情况下,需要限定线程等待的时间,因为无限期地等待事件发生可能会导致性能下降或资源浪费。一个常见的例子是在很多网络库中的 `connect` 函数,这个函数调用是阻塞的,但是也是限时的,一定时间内没有连接到服务器就不会继续阻塞了,会进行其它处理,比如抛出异常。
759759
760-
介绍两种指定超时的方式,一种是“**时间段**”,另一种是“**时间点**”,其实就是先前讲的 [`std::this::thread::sleep_for`](https://zh.cppreference.com/w/cpp/thread/sleep_for) 与 [`std::this_thread::sleep_until`](https://zh.cppreference.com/w/cpp/thread/sleep_until) 的区别。前者是需要指定等待一段时间(比如 10 毫秒)。而后者是指定等待到一个时间点(比如到 2024-05-07T12:01:10.123)。多数函数都对两种超时方式进行处理。**处理持续时间的函数以 `_for` 作为后缀,处理绝对时间的函数以 `_until` 作为后缀**。
760+
介绍两种指定超时的方式,一种是“**时间段**”,另一种是“**时间点**”,其实就是先前讲的 [`std::this::thread::sleep_for`](https://zh.cppreference.com/w/cpp/thread/sleep_for) 与 [`std::this_thread::sleep_until`](https://zh.cppreference.com/w/cpp/thread/sleep_until) 的区别。前者是需要指定等待一段时间(比如 10 毫秒)。而后者是指定等待到一个具体的时间点(比如到 2024-05-07T12:01:10.123)。多数函数都对两种超时方式进行处理。**处理持续时间的函数以 `_for` 作为后缀,处理绝对时间的函数以 `_until` 作为后缀**。
761761
762762
条件变量 `std::condition_variable` 的等待函数,也有两个超时的版本 [`wait_for`](https://zh.cppreference.com/w/cpp/thread/condition_variable/wait_for) 和 [`wait_until`](https://zh.cppreference.com/w/cpp/thread/condition_variable/wait_until) 。它们和我们先前讲的 `wait` 成员函数一样有两个重载,可以选择是否传递一个[*谓词*](https://zh.cppreference.com/w/cpp/named_req/Predicate)。它们相比于 `wait` 多了一个解除阻塞的可能,即:**超过指定的时长或抵达指定的时间点**。
763763
@@ -795,7 +795,7 @@ std::chrono::minutes std::chrono::duration</* int29 */, std::ratio<60>>
795795

796796
稳定时钟的主要优点在于,它可以提供相对于起始时间的稳定的递增时间,因此适用于需要保持时间顺序和不受系统时间变化影响的应用场景。相比之下,像 [`std::chrono::system_clock`](https://zh.cppreference.com/w/cpp/chrono/system_clock) 这样的系统时钟可能会受到系统时间调整或变化的影响,因此在某些情况下可能不适合对时间间隔进行精确测量。
797797

798-
不管使用哪种时钟获取时间,C++ 提供了函数,可以将时间点转换为 [**time_t**](https://zh.cppreference.com/w/cpp/chrono/c/time_t) 类型的值:
798+
不管使用哪种时钟获取时间,C++ 都提供了函数,可以将时间点转换为 [**time_t**](https://zh.cppreference.com/w/cpp/chrono/c/time_t) 类型的值:
799799

800800
```cpp
801801
auto now = std::chrono::system_clock::now();
@@ -853,16 +853,16 @@ auto one_hour = 1h;
853853
当不要求截断值的情况下(时转换为秒时没问题的,但反过来不行)时间段有隐式转换,显式转换可以由 [`std::chrono::duration_cast<>`](https://zh.cppreference.com/w/cpp/chrono/duration/duration_cast) 来完成。
854854
855855
```cpp
856-
std::chrono::milliseconds ms(3999);
856+
std::chrono::milliseconds ms{ 3999 };
857857
std::chrono::seconds s = std::chrono::duration_cast<std::chrono::seconds>(ms);
858-
std::cout << s << '\n';
858+
std::cout << s.count() << '\n';
859859
```
860860

861861
这里的结果是截断的,而不会进行所谓的四舍五入,最终的值是 `3`
862862

863863
时间库支持四则运算,可以对两个时间段进行加减乘除。时间段对象可以通过 [`count()`](https://zh.cppreference.com/w/cpp/chrono/duration/count) 成员函数获得计次数。例如 `std::chrono::milliseconds{123}.count()` 的结果就是 123。
864864

865-
基于时间段的等待都是由 `std::chrono::duration<>` 来完成。例如:等待 future 35 毫秒变为就绪状态。
865+
基于时间段的等待都是由 `std::chrono::duration<>` 来完成。例如:等待一个 future 对象在 35 毫秒内变为就绪状态:
866866

867867
```cpp
868868
std::future<int> future = std::async([] {return 6; });

md/05内存模型与原子操作.md

+5-6
Original file line numberDiff line numberDiff line change
@@ -295,13 +295,12 @@ void f(){
295295

296296
`flag` 对象的状态为设置 (`true`) 时,其线程调用 `test_and_set` 函数会返回 `true`,导致它们继续在循环中自旋,无法退出。直到先前持有锁的线程调用 `unlock()` 函数,将 `flag` 对象的状态原子地更改为清除 (`false`) 状态。此时,等待的线程中会有一个线程成功调用 `test_and_set` 返回 `false`,然后退出循环,成功获取锁。
297297

298-
> 值得注意的是,我们只是稍微的讲一下使用 `std::atomic_flag` 实现自旋锁。不过可没推荐各位在实践中使用它,具体可参见 **Linus Torvalds**[文章](https://www.realworldtech.com/forum/?threadid=189711&curpostid=189723)其中有一段话说的很直接
298+
> 值得注意的是,我们只是稍微的讲一下使用 `std::atomic_flag` 实现自旋锁。不过并不推荐各位在实践中使用它,具体可参见 [**Linus Torvalds**](https://en.wikipedia.org/wiki/Linus_Torvalds)[文章](https://www.realworldtech.com/forum/?threadid=189711&curpostid=189723)其中有一段话说得很直接
299299
>
300-
> - I repeat: **do not use spinlocks in user space, unless you actually know what you're doing**. And be aware that the likelihood that you know what you are doing is basically nil.
300+
> - **我再说一遍:不要在用户空间中使用自旋锁,除非你真的知道自己在做什么。请注意,你知道自己在做什么的可能性基本上为零。**
301+
> I repeat: **do not use spinlocks in user space, unless you actually know what you're doing**. And be aware that the likelihood that you know what you are doing is basically nil.
301302
>
302-
> > 我再说一遍:不要在用户空间中使用自旋锁,除非你真的知道自己在做什么。请注意,你知道自己在做什么的可能性基本上为零。
303-
>
304-
>然后就是推荐 `std::mutex``pthread_mutex` ,比自旋好的多。
303+
> 然后就是推荐使用 `std::mutex``pthread_mutex` ,比自旋好的多。
305304
306305
`std::atomic_flag` 的局限性太强,甚至不能当普通的 bool 标志那样使用。一般最好使用 `std::atomic<bool>`,下节,我们来使用它。
307306

@@ -344,4 +343,4 @@ print("end"); // 2
344343

345344
不禁止就是有可能,但是我们无需在乎,**就算真的 CPU 将 end 重排到 start 前面了,也得在可观测行为发生前回溯了**。所以我一直在强调,这些东西,**我们无需在意**
346345

347-
好了,到此,基本认识也就足够了以上的示例更多的是泛指,知到其表达的意思就好,这些还是简单直接且符合直觉的。
346+
好了,到此,基本认识也就足够了,以上的示例更多的是泛指,知到其表达的意思就好,这些还是简单直接且符合直觉的。

md/README.md

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# 阅读须知
2+
3+
&emsp;&emsp;本套教程侧重点并非是专注在如何更好的使用“多线程并发”。我们假设读者最低水平为:**`C++11 + STL + template`**,并没有接触过 C++ 标准并发库,假设略微了解操作系统基本知识。
4+
5+
&emsp;&emsp;我们强调了模板,因为并发支持库的很多设施其实现是较为简单的,概念与使用,再结合源码讲解会更加简单直观,然而要想阅读学习源码,模板的知识必不可少。不需要模板的水平有多高,也不需要会什么元编程,但是基本的需求得能做到,得会,这里推荐一下:[**《现代C++模板教程》**](https://github.com/Mq-b/Modern-Cpp-templates-tutorial)
6+
7+
&emsp;&emsp;本教程不保证你学习之后的成果,不过依然可以自信地说:**本教程在中文社区的同类型教程中是绝对的第一**。事实上只需要一句话就可以表达了——**伟大无需多言**
8+
9+
## 学习注意事项
10+
11+
&emsp;&emsp;我们的教程中常包含许多外部链接,这并非当前描述不足或者不够严谨,而是为了考虑读者的水平和可能的扩展学习需求。同时,也希望者能让读者避免获取二手知识与理解,我们提供的链接基本都是较为专业的文档或官方网站。
12+
13+
&emsp;&emsp;虽然教程名为《现代 C++ 并发编程教程》,但我们也扩展涉及了许多其他知识,包括但不限于:Win32、POSIX API;MSVC STL、libstdc++、libc++ 对标准库的实现;GCC 与 MSVC 的编译器扩展,以及 Clang 对它们的兼容;使用 CMake + Qt 构建带 UI 的程序,展示多线程异步的必要性;不同架构的内存模型(例如 x86 架构内存模型:Total Store Order (TSO),较为严格的内存模型)。
14+
15+
&emsp;&emsp;既然强调了“**现代**”,那自然是全方面的,具体的读者会在学习中感受到的。
16+
17+
&emsp;&emsp;另外我们的代码都会测试三大编译器 `Clang``GCC``MSVC`。通常都会是最新的,`Clang18``GCC14`。我们的教程中常常会提供 [Complier Explorer](https://godbolt.org/) 的运行测试链接以确保正确性,以及方便读者的测试与学习。如果你对此网站的使用不熟悉,可以阅读[使用文档](https://mq-b.github.io/Loser-HomeWork/src/%E5%8D%A2%E7%91%9F%E6%97%A5%E7%BB%8F/godbolt%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3)
18+
19+
## 代码风格
20+
21+
&emsp;&emsp;我们的代码风格较为简洁明了,命名全部使用下划线连接,而不是驼峰命名法。花括号通常只占一行,简短的代码可以不额外占行。一般初始化时使用 `{}`,而非 `()` 或者 `=` 。这样简单直观,避免歧义和许多问题。
22+
23+
```cpp
24+
struct move_only {
25+
move_only() { std::puts("默认构造"); }
26+
move_only(const move_only&) = delete;
27+
move_only(move_only&&)noexcept {
28+
std::puts("移动构造");
29+
}
30+
};
31+
32+
int main() {
33+
move_only m{};
34+
char buffer[1024]{} // 全部初始化为 0
35+
}
36+
```
37+
38+
如果是标量类型,可能考虑使用复制初始化,而非 `{}`,如:`int n = 0;`。
39+
40+
## 总结
41+
42+
&emsp;&emsp;本教程长期维护,接受 pr 与 issue。
43+
&emsp;&emsp;好了,稍微了解了一下,我们可以开始进入正式的学习内容了。

0 commit comments

Comments
 (0)