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

Conversation

lzorn-lzorn
Copy link

添加了 threaing ## 创建线程 的内容,下次希望继续上传 ## 线程间通信<mutex> 的内容
thread 的笔记我已经基本都写好了,会慢慢往上添加,请小彭老师多指教

lzorn-lzorn and others added 5 commits September 3, 2024 14:15
 Changes to be committed:
	new file:   docs/cpp_thread.md
	new file:   docs/img/cpp_thread/avoid_data_race.png
	new file:   docs/img/cpp_thread/join.png
	new file:   docs/img/cpp_thread/join_and_detach.png
	new file:   docs/img/cpp_thread/jthread_stop.png
	new file:   docs/img/cpp_thread/mem_order.png
	new file:   docs/img/cpp_thread/mem_order_const.png
	new file:   docs/img/cpp_thread/thread_state_transform.png
 Your branch is ahead of 'origin/main' by 1 commit.
   (use "git push" to publish your local commits)

 Changes to be committed:
	deleted:    docs/cpp_thread.md
	deleted:    docs/img/cpp_thread/mem_order.png
	deleted:    docs/img/cpp_thread/mem_order_const.png
	deleted:    "docs/img/cpp_thread/\350\216\267\345\276\227\351\207\212\346\224\276\344\270\200\350\207\264\346\200\247.png"
	renamed:    docs/img/cpp_thread/avoid_data_race.png -> docs/img/cpp_thread_avoid_data_race.png
	renamed:    docs/img/cpp_thread/join.png -> docs/img/cpp_thread_join.png
	renamed:    docs/img/cpp_thread/join_and_detach.png -> docs/img/cpp_thread_join_and_detach.png
	renamed:    docs/img/cpp_thread/jthread_stop.png -> docs/img/cpp_thread_jthread_stop.png
	renamed:    docs/img/cpp_thread/thread_state_transform.png -> docs/img/cpp_thread_thread_state_transform.png
	modified:   docs/threading.md
 Your branch is up to date with 'origin/main'.
 Changes to be committed:
	modified:   docs/threading.md
@lzorn-lzorn
Copy link
Author

threading ## 创建线程

@archibate
Copy link
Contributor

archibate commented Sep 3, 2024 via email

@archibate
Copy link
Contributor

archibate commented Sep 3, 2024 via email

@lzorn-lzorn
Copy link
Author

确实是我以前写好的笔记,所以只放了放了一小点先看看。我以后再改一下吧,把顺序和内容的方向再修正一下。感谢:)

@lzorn-lzorn lzorn-lzorn closed this Sep 4, 2024
@lzorn-lzorn lzorn-lzorn reopened this Sep 4, 2024
 Your branch is up to date with 'origin/main'.
 Changes to be committed:
	modified:   docs/threading.md
 Your branch is ahead of 'origin/main' by 1 commit.
   (use "git push" to publish your local commits)
 Changes to be committed:
	modified:   docs/threading.md

同时可调用对象还可以有返回, 但是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的移动赋值函数,往里面赋值。

@lzorn-lzorn
Copy link
Author

threading jthread 1. 初始化 2. 结束线程

实际上对于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对象都会进入这种空状态。

## 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提供的 `std::jthead`, 如果由于各种限制以至于读者无法使用jthread 也无需担心。 C++11提供的 `std::thread` 是 `std::jthead` 的阉割版。 你可以无缝的回到过去。
### 初始化`std::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(),是一个超集关系。

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

同时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

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不能拷贝)



## 线程的结束方式
线程的结束方式有两种:`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 不是“线程的结束方式”,不符合自然语言,改为“执行策略”会更加合适。


但是同时也要知道等待线程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() 终止程序,因为这不符合逻辑,线程还活着,线程对象还要析构。

@Mq-b
Copy link
Contributor

Mq-b commented Sep 12, 2024

剩下还有很多用词问题,自己看。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants