Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RP: threading ##创建线程 finish :) #36

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/img/cpp_thread_avoid_data_race.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/cpp_thread_join.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/cpp_thread_join_and_detach.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/cpp_thread_jthread_stop.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/cpp_thread_thread_state_transform.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 72 additions & 2 deletions docs/threading.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,78 @@
# C++ 多线程编程(未完工)
## 多线程中的常见概念
### 并行与并发

## 创建线程
### 进程, 线程, 协程

TODO
### 线程的状态
## jthread
### 使用C++20的 `std::jthread`
秉承学新不学旧的思路, 先介绍C++20提供的 `std::jthead`, 如果由于各种限制以至于读者无法使用jthread 也无需担心。 C++11提供的 `std::thread` 是 `std::jthead` 的阉割版。 你可以无缝的回到过去。
### 初始化`std::jthread`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不应该如此,应当先行介绍 std::thread,如果要认真聊 std::jthread 则没有单独聊 std::thread 的必要。

std::jthread 相比于 C++11 引入的 std::thread,只是多了两个功能:

  • RAII 管理:在析构时自动调用 join()。

  • 线程停止功能:线程的取消/停止。

同时,我建议约定格式,中英空格。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

错误的,用std::thread这种行为和赤膊new-delete没有任何区别,只不过是内存泄漏变成了响亮的terminate。

std::thread t([]{}); // 赤膊new
if (xxx) throw;
t.join(); // 赤膊delete

那么将会直接terminate,而jthread能自动request_stop并join。
我知道你可能担心request_stop讲不明白,那么也可以先按下不表,等后面讲到stop_token了再提起jthread的这一额外功能。

C++20封装好的线程对象jthread接受一个**可调用对象(callable)**, 换句话说就是重载了 `()` 运算符的对象, 它可以是一个函数, 重载了 `()` 的类又或者是一个lambda表达式

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不对,你对可调用对象的理解有很大问题,同时也不要求一定是 () 这种调用形式,如成员指针。

https://zh.cppreference.com/w/cpp/named_req/Callable

Copy link
Contributor

@archibate archibate Sep 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

可以说:任何可以被std::invoke调用的。例如成员函数指针&Class::memfn不支持()调用,但std::invoke(&Class::memfn, this)就支持,因而std::jthread j(&Class::memfn, this) 也是支持的。当然,支持()的肯定也都支持std::invoke(),是一个超集关系。

同时jthread的构造函数本身就是有invoke功能, 所以第一个参数是可调用对象, 后面的参数直接跟上可调用对象的参数即可。 此时需要注意一点, 如果可调用对线的参数中有引用传递, 则需要用 `std::ref` 或者 `std::cref` 包装。 因为默认是按值或移动传递。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“因为默认是按值或移动传递”,此处描述极其不准确,这里主要涉及到内部实现的问题。

void f(int, int& a) {
    std::cout << &a << '\n'; 
}

int main() {
    int n = 1;
    std::cout << &n << '\n';
    std::thread t { f, 3, std::ref(n) };
    t.join();
}

代码 void f(int, int&) 如果不使用 std::ref 并不会和 void f(int, const int&) 一样只是多了复制,而是会产生编译错误,这是因为 std::thread 内部会将保有的参数副本转换为右值表达式进行传递,这是为了那些只支持移动的类型,左值引用没办法引用右值表达式,所以产生编译错误。

将保有的参数副本”,其实说的是作为 std::thread 构造参数的传递的时候会先decay-copy

同时可调用对象还可以有返回, 但是jthread会忽略这个返回值。 如果想接住这个返回值需要借助 `std::future`。 见后。

jthread支持空初始化 `std::jthread jt;` 此时 `jt` 只是一个占位符, 并不是一个线程。如果后续需要分配任务, 使用jthread的移动语义。(jthread不能拷贝)
Copy link
Contributor

@archibate archibate Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
jthread支持空初始化 `std::jthread jt;` 此时 `jt` 只是一个占位符, 并不是一个线程。如果后续需要分配任务, 使用jthread的移动语义。(jthread不能拷贝)
jthread可以捕获参数,就像bind一样,也存在不能捕获引用的痛点。通常情况下使用的是 jthread 基于单个可调用对象的功能,而不会使用捕获参数的功能,如需捕获任何参数,可以使用更可读性的lambda表达式的 `[=]`,例如:
```cpp
void func(int i, int j);
int i = 1, j = 2;
jthread t(func, i, j);
// 等价于:
jthread t([=] { func(i, j); });
\`\`\`
`jthread` 就和 `unique_ptr` 一样,只支持移动,不支持拷贝,且具有一个空状态(类比于unique_ptr的nullptr)。
通过默认构造函数创建的 `jthread` 就处于这种空状态,只是一个占位符,并不持有线程资源,等待你的后续构造和存入。
好处是,如需延迟初始化,可以先把jthread构造为空状态,之后构造出jthread时,再使用jthread的移动赋值函数,往里面赋值。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

不应该使用所谓的“占位符”这种用词,此处不对。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这样呢

Suggested change
jthread支持空初始化 `std::jthread jt;` 此时 `jt` 只是一个占位符, 并不是一个线程。如果后续需要分配任务, 使用jthread的移动语义。(jthread不能拷贝)
jthread支持空初始化 `std::jthread jt;` 此时 `jt` 只是一个未绑定线程的空对象, 并不是一个线程。如果后续需要分配任务, 使用jthread的移动语义。(jthread不能拷贝)

```cpp
std::jthread jt;
jt = std::jthread([] {});
// jt = std::move(other_jthread); 不能拷贝
```


## 线程的结束方式
线程的结束方式有两种:`join()` 和 `detach()`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

此处用词错误,joindetach 不是“线程的结束方式”,不符合自然语言,改为“执行策略”会更加合适。

其具体的关系如下:
![[img/cpp_thread_join_detach.png]]
正如图中所示, 当 fork1_thread 调用其join()时, 其父线程必须等待其结束能继续进行后续操作. 而 fork2_thread 调用 detach() 时, 父线程则不需要等待他的结束. 当线程调用 detach() 必须要保证在 主线程main 结束之前结束, 不然main结束会释放资源, 如果此时子线程还没有结束则会导致使用一个已经被释放的资源。

但是同时也要知道等待线程join可能是一件非常耗时的时候, 所以一般会在最后join。 但是detach()可以在一开始就进行, 因为反正也不需要等他返回。

如果既不调用 `join()` 也不调用 `detach()`. 当线程对象的析构函数被调用时(通常在离开作用域或显式销毁时),由于线程对象仍然和一个活动的线程相关联,这会导致调用 `std::terminate()`,终止整个程序。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

应当强调是 std::thread 析构函数会调用 joinable() 判断当前线程对象是否有活跃线程,如果有,则直接调用 std::terminate() 终止程序,因为这不符合逻辑,线程还活着,线程对象还要析构。


![[img/cpp_thread_join.png]]

实际上对于C++20的jthread而言, 会在其销毁的时候自动调用 `join()`. 但如果你使用的旧版本 `std::thread` 则需要手动的调用 `join()` 或者 `detach()`, 此时你应该通过thread_guard类保证在作用域结束之后自动调用的`join()`

> #### `joinable()`
> 初始化子线程 `t` 后, 该子线程自动就是 joinable 的, 也就是 `t.joinable()` 的值是 `true`. 换句话说 `t.joinable()` 等于 `true` 的条件就是该线程有一个与之相关联的线程(父线程)。 当其detach之后也就独立于父线程运行, 此时的 `t.joinable()` 就返回 `false`. 在官方的描述中, `t.joinable()` 返回 `true` 则意味着可以通过 `get_id()` 得到这个线程的唯一标识. 但是当detach之后这个标识会返回0。换句话说, 一旦将一个线程detach之后就再也无法直接控制这个线程, 只能按照其原本的逻辑运行直至结束。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

其实joinable()实际上就是is_not_null(),这个名字是误导性的,detach后jthread变为空状态,刚好就是joinable()==false,detach后getid返回0就是因为这个,detach后的jthread和默认初始化的jthread是一样的。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“detach后jthread变为空状态”,准确来说是 detach 是让线程对象放弃了线程的所有权,线程对象不再和线程资源关联,线程对象自然为空。我们此时可以继续操作空的线程对象。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

补充:detach和join返回后,jthread对象都会进入这种空状态。


在Unix语境下, detach的线程一般称作守护线程(daemon thread). 这类线程一般会在后台长时间运行. 虽然也可以将detach认为是即用即扔的线程, 但是要**保证其必须在main结束之前完成, 且不要持有资源**。

此时, 我们会发现:jthread是更加符合RAII思想的, 所以应该**优先使用jthread**。 同时如果你没有1000%的把握, 同时也为了维持你的san值处于正常水平, 赛博SCP基金会建议您**不要使用detach**。

如果说用于初始化线程的可调用对象抛出异常但是没有处理时, 异常不会跨线程传播而是使用 `std::terminate()`。 如果内部处理了异常自然无事发生。
```cpp
int main() {
try {
/*
根本捕获不到,而是直接 std::terminate()
异常不会传播到主线程
*/
std::jthread thread([] {
throw std::runtime_error("Error occurred");
});
}
catch (const std::exception& e) {
std::cout << "Caught exception: " << e.what() << std::endl;
}
std::jthread thread([] {
try {
// 在内部处理
throw std::runtime_error("Error occurred");
} catch (const std::exception& e) {
std::cout << "Caught exception in thread: " << e.what() << std::endl;
}
});
return 0;
}
```
### jthread 的停止功能

### 无奈的妥协回到std::thread

## 为什么数据竞争

Expand Down