idealvin / coost

A tiny boost library in C++11.
Other
3.99k stars 562 forks source link

关于 `thread_ptr` 的疑问 #31

Closed Liam0205 closed 4 years ago

Liam0205 commented 4 years ago

@idealvin 你好。

按我的理解,当 thread_ptr 的对象,例如 foo,以 std::ref(foo) 的方式传进线程函数之后,子线程是无法获取父线程设置的指针值的。也就是说,任何一个线程要使用 foo,都必须现在线程内设置 foo 指向的对象才能使用。

既然如此,为什么不在每个线程里分别使用 std::unique_ptr 呢?使用 thread_ptr 的好处在哪里?

Liam0205 commented 4 years ago

相关代码:

https://github.com/idealvin/co/blob/30152648578b672c07f0bf51a1e973e2ce0edf92/base/unix/thread.h#L190-L271

Liam0205 commented 4 years ago

https://github.com/idealvin/co/blob/71f920481ca8d2d44bd0a44a8668c9d795c23d03/base/rpc.cc#L79

在库里的实际使用,目前只搜到了这一处。按我的理解,这里用到的正是 thread_ptr 每次在新线程里都要初始化的特点;保证每个线程用到的随机数种子不一样。即下面起到 initialization 作用的这一行:

https://github.com/idealvin/co/blob/71f920481ca8d2d44bd0a44a8668c9d795c23d03/base/rpc.cc#L243

既然在每个线程里都要 initialization 一次,那为什么不在线程函数开始的时候,声明并构造一个单独的 Random 类型对象就好了呢?当然,这样就无法把 _rand 作为数据成员封装起来了。

Liam0205 commented 4 years ago

换而言之,它最大的价值是扩充了 C++11 引入的 thread_local 关键字在数据成员上的作用。thread_local 作用在数据成员上时,要求数据成员必须是 static 的。这也就是说,同一个类在同一个线程里的对象,访问该数据成员时,就像访问全局变量一样。但 thread_ptr 能做到同一个类在同一个线程里的对象,访问该数据成员时,就像访问局部变量一样。

也就是说,如果我实现这样一个类模板 tls,然后把 Random 或者 std::unique_ptr<Random> 当做模板参数丢进去,就能实现同样的效果了,而且还无需使用 platform-specific APIs:

#pragma once

#include <unordered_map>
#include <mutex>
#include <exception>
#include <utility>

namespace yuuki {
template <typename T>
struct tls {
 private:
  static thread_local std::unordered_map<const tls<T>*, T> tls_payloads;
  mutable std::mutex mtx_;

 public:
  tls() = default;
  explicit tls(const T& value) : tls() {
    set(value);
  }
  explicit tls(T&& value) : tls() {
    set(std::move(value));
  }
  ~tls() {
    std::lock_guard<std::mutex> lock(mtx_);
    auto pair = tls_payloads.find(this);
    if (pair != tls_payloads.end()) {
      tls_payloads.erase(pair);
    }
  }

  tls(const tls& other) : tls(other.get()) {}
  tls(tls&& other)      : tls(std::move(other.get())) {}
  tls& operator=(const tls& other) {
    set(other.get());
    return *this;
  }
  tls& operator=(tls&& other) {
    set(std::move(other.get()));
    return *this;
  }

  void set(T&& value) {
    std::lock_guard<std::mutex> lock(mtx_);
    auto pair = tls_payloads.find(this);
    if (pair == tls_payloads.end()) {
      tls_payloads.emplace(this, std::move(value));
    } else if (value == pair->second) {
      return;
    } else {
      pair->second = std::move(value);
    }
  }

  void set(const T& value) {
    std::lock_guard<std::mutex> lock(mtx_);
    auto pair = tls_payloads.find(this);
    if (pair == tls_payloads.end()) {
      tls_payloads.emplace(this, value);
    } else if (value == pair->second) {
      return;
    } else {
      pair->second = value;
    }
  }

  T& get(void) const {
    std::lock_guard<std::mutex> lock(mtx_);
    auto pair = tls_payloads.find(this);
    if (pair == tls_payloads.end()) {
      throw std::logic_error("ERROR: fetching value before setting in current thread!");
    } else {
      return pair->second;
    }
  }

  T get_copy(void) const {
    return get();
  }
};

template <typename T>
thread_local std::unordered_map<const tls<T>*, T> tls<T>::tls_payloads{};
}  // namespace yuuki
idealvin commented 4 years ago

线程是在框架内部启动的,实际业务处理不需要知道是在哪个线程中。

thread_ptr<Random>,不同线程需要设置不同的Random对象

---原始邮件--- 发件人: "Liam Huang"<notifications@github.com> 发送时间: 2019年12月13日(星期五) 晚上7:32 收件人: "idealvin/co"<co@noreply.github.com>; 抄送: "Mention"<mention@noreply.github.com>;"Alvin"<idealvin@qq.com>; 主题: Re: [idealvin/co] 关于 thread_ptr 的疑问 (#31)

https://github.com/idealvin/co/blob/71f920481ca8d2d44bd0a44a8668c9d795c23d03/base/rpc.cc#L79

在库里的实际使用,目前只搜到了这一处。按我的理解,这里用到的正是 thread_ptr 每次在新线程里都要初始化的特点;保证每个线程用到的随机数种子不一样。即下面起到 initialization 作用的这一行:

https://github.com/idealvin/co/blob/71f920481ca8d2d44bd0a44a8668c9d795c23d03/base/rpc.cc#L243

既然在每个线程里都要 initialization 一次,那为什么不在线程函数开始的时候,声明并构造一个单独的 Random 类型对象就好了呢?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or unsubscribe.

Liam0205 commented 4 years ago

那就是了。thread_ptr 起到的并不是指针的作用,而是 non-static thread_local data member 的作用。

idealvin commented 4 years ago

也有指针的作用,thread_ptr用起来跟std::unique_ptr差不多,只是各线程都需要自行设置其值

---原始邮件--- 发件人: "Liam Huang"<notifications@github.com> 发送时间: 2019年12月13日(星期五) 晚上8:30 收件人: "idealvin/co"<co@noreply.github.com>; 抄送: "Mention"<mention@noreply.github.com>;"Alvin"<idealvin@qq.com>; 主题: Re: [idealvin/co] 关于 thread_ptr 的疑问 (#31)

那就是了。thread_ptr 起到的并不是指针的作用,而是 non-static thread_local data member 的作用。

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or unsubscribe.

idealvin commented 4 years ago

对的,原则上是这样,但是 map 加锁的实现效率不高

---原始邮件--- 发件人: "Liam Huang"<notifications@github.com> 发送时间: 2019年12月13日(星期五) 晚上7:45 收件人: "idealvin/co"<co@noreply.github.com>; 抄送: "Mention"<mention@noreply.github.com>;"Alvin"<idealvin@qq.com>; 主题: Re: [idealvin/co] 关于 thread_ptr 的疑问 (#31)

换而言之,它最大的价值是扩充了 C++11 引入的 thread_local 关键字在数据成员上的作用。thread_local 作用在数据成员上时,要求数据成员必须是 static 的。这也就是说,同一个类在同一个线程里的对象,访问该数据成员时,就像访问全局变量一样。但 thread_ptr 能做到同一个类在同一个线程里的对象,访问该数据成员时,就像访问局部变量一样。

也就是说,如果我实现这样一个类 tls,然后把 Random 当做模板参数丢进去,就能实现同样的效果了:

include <unordered_map> template <typename T> struct tls { private: static thread_local std::unordered_map<tls<T>, T> tlspayloads; mutable std::mutex mtx; public: tls() = default; explicit tls(const T& value) : tls() { set(value); } ~tls() { std::lockguard<std::mutex> lock(mtx); auto pair = tls_payloads.find(this); if (pair != tls_payloads.end()) { tls_payloads.erase(pair); } } tls(const tls& other) { set(other.get()); } tls(tls&& other) { set(other.get()); } tls& operator=(const tls& other) { set(other.get()); } tls& operator=(tls&& other) { set(other.get()); } void set(const T& value) { std::lockguard<std::mutex> lock(mtx); auto pair = tls_payloads.find(this); if (pair == tls_payloads.end()) { tls_payloads.insert({this, value}); return; } else if (value == pair->second) { return; } else { pair->second = value; } } T& get(void) { std::lockguard<std::mutex> lock(mtx); auto pair = tls_payloads.find(this); if (pair == tls_payloads.end()) { std::terminate(); } else { return pair->second; } } T const_get(void) { return get(); } }; template <typename T> thread_local std::unordered_map<tls<T>, T> tls<T>::tls_payloads{};

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or unsubscribe.

Liam0205 commented 4 years ago

额……其实我怀疑,pthread_key_t 内部实现也是带了 map 和锁的……

idealvin commented 4 years ago

额……其实我怀疑,pthread_key_t 内部实现也是带了 map 和锁的……

理论上不需要锁,可能是个数组,不是map.

可以参考 co::Mutex,co::Pool 的实现。

Liam0205 commented 4 years ago

嗯,我看一下 co::Mutexco::Pool 的实现。

还是没想明白为啥 pthread_key_t 内部不需要 map。它相当于要根据 pid 拿到不同指针,然后根据指针去找到真正存在 key 里的数据。如果是数组的话,那这个数组岂不是要很长……?要不然链表?那还不如用 map 呢?

Liam0205 commented 4 years ago

看了一下 darwin_libpthread 的代码,pthread_key_t 内部是用一个 array 实现的 map 的功能。这个结构是 pthread_t 里的 void* tsd[],代码如下:

https://github.com/apple/darwin-libpthread/blob/03c4628c8940cca6fd6a82957f683af804f62e7f/src/internal.h#L232-L241

我之前一直疑惑的是,thread 的数量是不确定的,怎么用一个固定大小的 array 解决这个问题呢?看到注释我就笑了……人家根本就没解决哈哈哈~

Liam0205 commented 4 years ago

然后在 pthread_key_create 的时候,把所有可能用上的 thread,都 create 了一遍。

https://github.com/apple/darwin-libpthread/blob/03c4628c8940cca6fd6a82957f683af804f62e7f/src/pthread_tsd.c#L133-L150