使用带有 pthread_create() 的静态成员函数有什么含义?

What are the implications of using a static member function with pthread_create()?

我正在帮助学生做家庭作业,这是一项基本的线程练习。不幸的是,虽然他们被要求使用 C++11,但他们被禁止使用 std::thread。我看不出原理,但这不是我的功课。

这是class:

class VaccineInfo {
public:
  VaccineInfo(const std::string &t_input_filename):
    input_filename(t_input_filename)
  { }
  VaccineInfo() = delete;

  static void *count_vaccines(void *t_vi);

  int v1_count() { return vaccine_count["v1"]; }
  int v2_count() { return vaccine_count["v2"]; }
  int v3_count() { return vaccine_count["v3"]; }

private:
  std::string input_filename;

  std::map<std::string, int> vaccine_count {
    { "v1", 0 },
    { "v2", 0 },
    { "v3", 0 }
  };

};

void *VaccineInfo::count_vaccines(void *t_vi) {
  VaccineInfo *vi = reinterpret_cast<VaccineInfo*>(t_vi);
  std::ifstream input_file;
  std::string input_line;

  input_file.open(vi->input_filename);
  if (!input_file.good()) {
    std::cerr << "No such file " << vi->input_filename << std::endl;
    return nullptr;
  }

  while (std::getline(input_file, input_line)) {
    vi->vaccine_count[input_line]++;
  }

  return nullptr;
}

这就是 pthreads 的用武之地。

std::vector<std::string> filenames = find_filenames(".");
std::vector<pthread_t> thread_handles;
std::vector<VaccineInfo> vi_vector;

vi_vector.reserve(filenames.size());

for(const std::string &filename : filenames) {
pthread_t tid;
thread_handles.push_back(tid);
vi_vector.emplace_back(VaccineInfo(filename));
pthread_create(
    &thread_handles.back(), nullptr, &VaccineInfo::count_vaccines,
    static_cast<void*>(&vi_vector.back()));
}

for (const pthread_t tid : thread_handles) {
pthread_join(tid, nullptr);
}

这是一个非常基本的练习,除了你必须做多少次才能让旧的和新的玩得很好。这就是让我想知道的 - 使用静态成员方法作为 pthread_createstart_routine 参数是否有任何不良副作用?我知道静态成员变量和函数不“属于”任何对象,但我通常认为静态变量是一个 class,而不管对象的数量。如果静态成员函数也只有一个副本,那么并行化似乎是搬起石头砸自己的脚。

在这种情况下,将 vaccine_count public 和 count_vaccines() 设为全局函数会更好吗?

请用你能收集到的任何细节来打击我;我很好奇。 =) 还有,一如既往,感谢大家的时间和努力。

except for how much fluff you have to do to get the old and the new to play nice.

好吧,在 STL 中,这基本上就是 std::thread 的实际作用。如果您创建一个线程并强制它导致堆栈展开,并且如果您查看所述堆栈,您会看到 thispthread_create(或 CreateThread 在 Windows).

也就是说,使用 class 的 static 函数然后调用 class 的 private 成员在对象实例,即使使用 std::thread,它实际上只取决于您需要这些函数做什么。

does using a static member method as the start_routine argument to pthread_create have any undesirable side effects?

没有。至少从功能的角度来看不是;也就是说,在静态成员上创建线程不会导致任何 UB 或直接崩溃,因为您正在使用 static 函数。

您确实必须考虑到您在静态成员函数上进行操作这一事实,但这与必须考虑 constructors/destructors 或语言本身的任何函数没有什么不同。由于这是一项家庭作业,教授可能试图教授“事物如何工作”而不是“如何使用 C++11”。

Would it just be better, in this case, to make vaccine_count public and make count_vaccines() a global function?

是也不是。将 vaccine_count 作为私有成员意味着 count_vaccines 必须是友元函数或静态函数,并且考虑到 vaccine_count 似乎是一个“重要”数据点,您不希望“代码的用户”无意中设置,最好将其保密。

您可以添加 getter 和 setter,但这可能会使代码不必要地复杂化。

如果您相信代码的用户会保护该变量(不太可能),您也可以将其设为 public 变量,并且您也可以将 count_vaccines 设为 free/global 函数,但是你需要在 class 声明之后有这个函数。如果 class 是一个复杂的 class(可能有模板或其他一些 C++ 概念),那么它确实会使您在 class.[=38= 上操作的代码复杂化]

所以是的,它可以那样做,但是教授可能会尝试教授什么是静态函数的概念,线程如何在 class 上运行以及指针如何在此构造中工作运动等等。

If you have a static member variable, all objects access that variable.

这不是 static 在这种情况下的意思。 C++ 中的 static 关键字只是意味着您不需要对象引用来调用该代码。所以一个 static 成员变量不仅可以被任何对象访问,而且可以被任何代码访问,举个例子:

class Data {
    public:
        static int x_val;
        int y_val;
};

int Data::x_val; // have to declare it since it's static


int main(int argc, char* argv[]) {
    Data::x_val = 10; // works because it's static.
    Data::y_val = 10; // error: accessing a non-static member
    
    Data obj;
    obj.y_val = 10; // ok because it's a member variable
    obj.x_val = 20; // this works as the complier ultimately translates this to `Data::x_val = 20`
    // BUT, referencing a static member/function on an object instance is "bad form"

    return 0;
}

If you have a static member function... can it be called on more than one core simultaneously?

static 关键字对调用该函数的核心或线程没有影响,或者是否可以并行完成。

一个CPU核心每个时钟周期只能执行1条机器级指令(所以本质上,只是1条汇编指令),当一个C++程序被编译、链接和汇编时,就是这些“汇编”集基于您编写的语法的指令在 CPU 的核心(或多个核心)上执行,而不是 static 函数。

该静态函数只是内存中的一个地址,可以在任何 CPU 核心上的任意数量的线程上调用该地址,OS 在您的程序中的任何给定时间确定。

是的,您可以调用 OS API 将调用该函数的执行线程固定到特定核心,但这是一个不同的主题。

为了给您带来最后一点乐趣,在汇编级别,C++ 函数基本上被编译成类 C 函数(极端过度简化,但仅用于演示):

C++

class Data {
    public:
        void increment() {
            this->y_val += 1024;
        }
    private:
        int y_val;
};

int main() {
    Data obj;
    obj.y_val = 42;
    obj.increment(); // obj.y_val == 1066
    return 0;
}

C

struct Data {
    int y_val;
};

void Data_increment(Data* this) {
    this->y_val += 1024;
}

int main() {
    Data obj;
    obj.y_val = 42;
    increment(&obj); // obj.y_val == 1066
    return 0;
}

同样,过于简单化了,但重点是要说明它是如何构建到程序集以及程序集的作用的。