类对象
#include <iostream> #include <thread> #include <mutex> std::mutex cout_mutex; //定义类 class A { public: A() { std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "create A" << std::endl; } ~A() { std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "destroy A" << std::endl; } int counter = 0; int get_value() { return counter++; } }; void thread_func(const std::string& thread_name) { for (int i = 0; i < 3; ++i) { thread_local A* a = new A(); std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "thread[" << thread_name << "]: a.counter:" << a->get_value() << std::endl; } return; } int main() { std::thread t1(thread_func, "t1"); std::thread t2(thread_func, "t2"); t1.join(); t2.join(); return 0; }
上面这段代码演示了使用线程局部存储来保证多线程环境下类的独立性。
首先,定义了一个类A,该类具有一个计数器counter和一个成员函数get_value(),用于获取当前计数器的值并将计数器加1。
在主函数中,创建了两个线程t1和t2,分别调用函数thread_func()。函数thread_func()内部定义了一个指向类A对象的指针a,并将其设置为线程局部存储。接着,对a调用get_value()函数,输出当前计数器的值。
由于a是线程局部存储,因此每个线程都拥有自己的a对象。每个线程调用get_value()函数时,都是针对自己的a对象进行操作。这样可以避免多个线程同时操作同一个对象而导致的数据竞争问题。
另外,类A的构造函数和析构函数内部都加了互斥锁,这是为了确保多线程环境下构造和析构操作的安全性。加锁保证了同一时间只有一个线程能够访问类A的构造和析构函数,避免了多个线程同时进行这些操作而引发的竞争问题。
输出:
create A thread[t1]: a.counter:0 thread[t1]: a.counter:1 thread[t1]: a.counter:2 create A thread[t2]: a.counter:0 thread[t2]: a.counter:1 thread[t2]: a.counter:2
可以看出类对象的使用和创建和内部类型类似,都不会创建多个。这种情况在函数间或通过函数返回实例也是一样的:
A* creatA() { return new A(); } void loopin_func(const std::string& thread_name) { thread_local A* a = creatA(); std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "thread[" << thread_name << "]: a.counter:" << a->get_value() << std::endl; return; } void thread_func(const std::string& thread_name) { for (int i = 0; i < 3; ++i) { loopin_func(thread_name); } return; }
上面这段代码主要涉及到函数和线程的使用。
函数 A* creatA()
返回指向 A
类对象的指针。在 loopin_func
中,将 creatA()
返回的指针赋值给 thread_local A* a
,表示该指针变量的存储期是线程局部的。接着,使用 a
调用 A
类的成员函数 get_value()
,并将其返回值输出到控制台上。
在 thread_func
中,循环调用 loopin_func
函数,每次调用都会生成一个新的 A
类对象并输出其成员变量 counter
的值。
由于 a
是线程局部变量,因此每个线程都会拥有自己的 a
对象,并且每次调用 loopin_func
函数时都会生成一个新的 A
类对象。此外,使用互斥锁保证了输出的线程安全性。
输出:
create A thread[t1]: a.counter:0 thread[t1]: a.counter:1 thread[t1]: a.counter:2 create A thread[t2]: a.counter:0 thread[t2]: a.counter:1 thread[t2]: a.counter:2
虽然 createA()
看上去被调用了多次,实际上只被调用了一次,因为 thread_local 变量只会在每个线程最开始被调用的时候进行初始化,并且只会被初始化一次。
举一反三,如果不是初始化,而是赋值,则情况就不同了:
void loopin_func(const std::string& thread_name) { thread_local A* a; a = creatA(); std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "thread[" << thread_name << "]: a.counter:" << a->get_value() << std::endl; return; }
上面这段代码定义了一个函数loopin_func
,接受一个字符串参数thread_name
。该函数在每个线程的本地存储中创建了一个类型为A*
的线程局部变量a
。然后调用creatA
函数创建一个新的A
对象,并将指针存储在a
中。最后,该函数打印出对象的counter
值。
由于每个线程都拥有自己的线程局部变量a
,因此creatA
在每个线程中只会调用一次,而不是在每次调用loopin_func
时都会调用一次。这是因为线程局部变量在第一次使用时会被初始化,并在每个线程的生命周期内保持其值。
输出:
create A thread[t1]: a.counter:0 thread[t1]: a.counter:1 thread[t1]: a.counter:2 create A thread[t2]: a.counter:0 thread[t2]: a.counter:1 thread[t2]: a.counter:2
很明显,虽然只初始化一次,但却可以被多次赋值,因此 C++ 变量初始化是十分重要的(手动狗头)。
类成员变量
规定:thread_local 作为类成员变量时必须是 static 的。
class B { public: B() { std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "create B" << std::endl; } ~B() {} thread_local static int b_key; //thread_local int b_key; int b_value = 24; static int b_static; }; thread_local int B::b_key = 12; int B::b_static = 36; void thread_func(const std::string& thread_name) { B b; for (int i = 0; i < 3; ++i) { b.b_key--; b.b_value--; b.b_static--; // not thread safe std::lock_guard<std::mutex> lock(cout_mutex); std::cout << "thread[" << thread_name << "]: b_key:" << b.b_key << ", b_value:" << b.b_value << ", b_static:" << b.b_static << std::endl; std::cout << "thread[" << thread_name << "]: B::key:" << B::b_key << ", b_value:" << b.b_value << ", b_static: " << B::b_static << std::endl; return; }
上面这段代码定义了一个名为 B
的类,并在其中定义了三个属性:b_key
、b_value
和 b_static
。其中,b_key
是一个线程局部静态变量,而 b_value
和 b_static
则是普通的类成员变量。这个类的构造函数输出 create B
,析构函数为空。
thread_func
函数接收一个字符串参数,表示当前线程的名字。在函数中创建一个 B
类型的对象 b
,并循环 3 次。每次循环,将 b_key
、b_value
和 b_static
的值减一,并使用互斥锁锁定输出,将这三个属性的值打印出来,分别加上 thread_name
、B::b_key
、b_value
和 B::b_static
的前缀。
输出:
create B thread[t2]: b_key:11, b_value:23, b_static:35 thread[t2]: B::key:11, b_value:23, b_static: 35 thread[t2]: b_key:10, b_value:22, b_static:34 thread[t2]: B::key:10, b_value:22, b_static: 34 thread[t2]: b_key:9, b_value:21, b_static:33 thread[t2]: B::key:9, b_value:21, b_static: 33 create B thread[t1]: b_key:11, b_value:23, b_static:32 thread[t1]: B::key:11, b_value:23, b_static: 32 thread[t1]: b_key:10, b_value:22, b_static:31 thread[t1]: B::key:10, b_value:22, b_static: 31 thread[t1]: b_key:9, b_value:21, b_static:30 thread[t1]: B::key:9, b_value:21, b_static: 30
b_key
是 thread_local,虽然其也是 static 的,但是每个线程中有一个,每次线程中的所有调用共享这个变量。b_static
是真正的 static,全局只有一个,所有线程共享这个变量。