C++并发编程 -2.线程间共享数据

本章就以在C++中进行安全的数据共享为主题。避免上述及其他潜在问题的发生的同时,将共享数据的优势发挥到最大。

一. 锁分类和使用

按照用途分为互斥、递归、读写、自旋、条件变量。本章节着重介绍前四种,条件变量后续章节单独介绍。

由于锁无法进行拷贝和转移,通常与包装器进行连用。例如:lock_guard、unique_lock。

包装器lock_guardunique_lockshared_lock
含义作用域内自动获取互斥锁,并在作用域结束时释放锁,以确保资源的安全访问。管理互斥锁的获取和释放,更加灵活,可以指定解锁加锁的时间点允许多个线程同时共享一个互斥锁,以提高并发性能.例如多读一写的场景
属性不可移动、赋值、拷贝。必须通过构造函数初始化和析构函数销毁能拷贝、赋值、移动,也可通过构造函数初始化和析构函数销毁能拷贝、赋值、移动
灵活性    差  ,不可指定加锁解锁时间点                      灵活,可随意更改加锁解锁时间点。还可以锁超时、不锁定、条件变量灵活,可随意更改加锁解锁时间点。还可以锁超时、不锁定、条件变量
性能强       并发,强
传入参数

三者都支持adopt_lock_ttry_to_lock_t 和 defer_lock_t

adopt_lock_t:表示对象在构造的时已经被锁定,会在析构时解锁互斥量,同样用于获取锁之后将锁的所有权转移给包装器对象.

defer_lock_t:延迟锁定,在创建完毕该对象后可手动指定加锁解锁。

try_to_lock_t :对象在构造时会尝试对互斥量进行非阻塞的锁定。如果锁定成功,则 对象将持有互斥量的锁;否则,它将不持有任何锁。

1.互斥锁

        互斥锁(Mutex)是一种用于多线程编程的同步机制,用于保护共享资源的访问。它确保在任何给定时刻只有一个线程可以访问被保护的资源,从而避免数据竞争和不一致的结果。

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <algorithm>using namespace std;mutex coutMutex;void function(int i)
{thread::id threadId = this_thread::get_id();{lock_guard<mutex> lock(coutMutex);cout<<"my thread id:"<<threadId<<" i:"<<i<<endl;}//锁自动释放使用权
}int main()
{vector<thread> threads;for(int i = 0; i < 30; i++){threads.push_back(thread(function, i));}for_each(threads.begin(), threads.end(), [](thread &t){t.join();});
}

        当一个线程获得了互斥锁后,其他线程将被阻塞,直到该线程释放锁。这样可以确保在任何时刻只有一个线程可以执行临界区(对共享资源的访问代码段),从而避免了多个线程同时修改共享资源而导致的问题。

2.递归锁

        递归锁(Recursive Lock)是一种特殊类型的互斥锁,它允许同一个线程多次获取锁而不会导致死锁。在多线程环境中,递归锁可以避免同一个线程在递归调用中对同一个资源进行重复加锁而导致的死锁情况。

        递归锁内部维护了一个锁计数器,当一个线程第一次获取递归锁时,计数器加1,并且线程可以继续执行临界区代码。当同一个线程再次获取递归锁时,计数器再次加1,而不会被阻塞。只有当线程释放了与获取次数相匹配的锁时,计数器才会递减。只有当计数器为0时,其他线程才能获取该锁。

        递归锁的使用场景通常是在一个函数或方法中需要递归调用自身,并且在每次递归调用中需要对共享资源进行加锁保护。递归锁允许同一个线程在递归调用中多次获取锁,确保了对共享资源的安全访问。

#include <iostream>
#include <mutex>std::recursive_mutex myMutex;void foo(int val) {std::lock_guard<std::recursive_mutex> lock(myMutex);// 访问共享资源std::cout << "Value: " << val << std::endl;// 可以递归地再次锁定同一个 mutexif (val > 0) {foo(val - 1);}
}int main() {foo(3);return 0;
}

 

3.读写锁

        shared_mutex是一种多线程同步机制,用于实现读写锁。它允许多个线程同时访问共享资源,但在写操作期间会独占资源,以确保数据的一致性和完整性。

shared_mutex提供了两种操作:共享访问和独占访问。

        在共享访问模式下,多个线程可以同时读取共享资源,而不会互相干扰。这对于读取频繁、写入较少的场景非常有用,可以提高并发性能。

        而在独占访问模式下,只有一个线程可以获得对共享资源的写权限,其他线程必须等待写操作完成后才能继续执行。这样可以保证在写操作期间,不会有其他线程读取或写入资源,从而确保数据的一致性。

#include <iostream>
#include <thread>
#include <shared_mutex>
#include <mutex>std::shared_mutex mutex;
int sharedData = 0;void readerThread() {while (true) {std::shared_lock<std::shared_mutex> lock(mutex);  // 共享锁定std::cout <<"this thread id:"<<std::this_thread::get_id()<< " Reader Thread: " << sharedData << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(500));}
}void writerThread() {while (true) {std::unique_lock<std::shared_mutex> lock(mutex);  // 独占锁定sharedData++;std::cout << "Writer Thread: " << sharedData << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(1000));}
}int main() {std::thread reader1(readerThread);std::thread reader2(readerThread);std::thread writer(writerThread);reader1.join();reader2.join();writer.join();while(1)std::this_thread::sleep_for(std::chrono::milliseconds(2000));return 0;
}

4.自旋锁

        自旋锁是一种基本的同步机制,用于保护共享资源,它使用忙等待的方式来实现线程的同步。当一个线程尝试获取自旋锁时,如果锁已经被其他线程占用,那么该线程会一直在一个循环中自旋等待,直到锁被释放。

        优点:1)线程占用锁的时间非常短,短到不值得进行线程上下文切换的开销。

                   2)不会导致线程阻塞,避免了线程切换的开销。

        缺点:自旋等待期间,线程会一直占用CPU资源,如果自旋等待时间过长,会浪费CPU资源

        适用场景:

                    1)线程占用锁的时间非常短,短到不值得进行线程上下文切换的开销。

                    2)锁的占用时间较短,大部分情况下能够立即获取到锁。

class spinlock_mutex
{spinlock_mutex():spinMutex(ATOMIC_FLAG_INIT){}void lock(){while(spinMutex.test_and_set(std::memory_order_acquire)); //读之前保证变量最新}void unlock(){spinMutex.clear(std::memory_order_release); //写之前保证变量最新}private:std::atomic_flag spinMutex;
};

在后续第七章节会结合内存序列进行详细介绍。

二. 死锁的原因

              线程1先锁定mutex1,线程2先锁定mutex2.都需要等待对方释放锁权限,造成死锁.如下:

#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <chrono>std::mutex mutex1;
std::mutex mutex2;void threadFunction1() {std::unique_lock<std::mutex> lock1(mutex1);std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 延迟一下,以确保线程2也能获取锁std::cout << "Thread 1: Trying to lock mutex2..." << std::endl;std::unique_lock<std::mutex> lock2(mutex2); // 尝试获取mutex2std::cout << "Thread 1: Got both locks!" << std::endl;
}void threadFunction2() {std::unique_lock<std::mutex> lock2(mutex2);std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 延迟一下,以确保线程1也能获取锁std::cout << "Thread 2: Trying to lock mutex1..." << std::endl;std::unique_lock<std::mutex> lock1(mutex1); // 尝试获取mutex1std::cout << "Thread 2: Got both locks!" << std::endl;
}int main() {std::thread t1(threadFunction1);std::thread t2(threadFunction2);t1.join();t2.join();return 0;
}

三. 如何避免死锁

1.使用固定顺序加锁解锁

           基于第二节造成死锁的原因,可以在需要加锁的期间按照相同顺序加锁解锁,如下改动:

void threadFunction1() {std::unique_lock<std::mutex> lock1(mutex1);std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 延迟一下,以确保线程2也能获取锁std::cout << "Thread 1: Locked mutex1, trying to lock mutex2..." << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 增加延迟,增加死锁的可能性{std::unique_lock<std::mutex> lock2(mutex2); // 尝试获取mutex2std::cout << "Thread 1: Got both locks!" << std::endl;}
}void threadFunction2() {std::unique_lock<std::mutex> lock1(mutex1);std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 延迟一下,以确保线程1也能获取锁std::cout << "Thread 2: Locked mutex1, trying to lock mutex2..." << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 增加延迟,增加死锁的可能性{std::unique_lock<std::mutex> lock2(mutex2); // 尝试获取mutex2std::cout << "Thread 2: Got both locks!" << std::endl;}
}

        适用场景:适用于需要对多个互斥锁进行加锁且对性能要求不高的场景,并且需要确保以相同的顺序对这些锁进行加锁。

        优点:实现相对简单,不需要复杂的算法和数据结构。只需要定义好资源的顺序即可。

        缺点:固定顺序加锁解锁可能会降低系统的灵活性,由于多个线程都按照顺序等待锁的占用释放,可能会对系统的性能产生一定的影响。


2.使用超时锁

        超时锁(Timeout Lock)是一种在多线程编程中常用的技术,用于在一段时间内尝试获取锁,如果超过指定的时间仍未成功获取锁,则放弃获取锁并执行相应的处理逻辑。

#include <iostream>
#include <mutex>
#include <condition_variable>
#include <chrono>std::mutex mtx;
std::condition_variable cv;
bool isLocked = false;bool tryLockFor(int milliseconds) {std::unique_lock<std::mutex> lock(mtx);auto timeout = std::chrono::system_clock::now() + std::chrono::milliseconds(milliseconds);while (isLocked) {if (cv.wait_until(lock, timeout) == std::cv_status::timeout) {return false; // 超时未获取到锁}}isLocked = true;return true; // 成功获取到锁
}void unlock() {std::lock_guard<std::mutex> lock(mtx);isLocked = false;cv.notify_one();
}int main() {std::cout << "Trying to acquire lock..." << std::endl;if (tryLockFor(2000)) {std::cout << "Have already locked, write data" << std::endl;unlock();} else {std::cout << "Failed to acquire lock within the timeout." << std::endl;}return 0;
}

        适用并发访问共享数据结构:多个线程同时访问共享的数据结构(如链表、队列等)时,超时锁可以避免数据结构的破坏和不一致。

3.使用锁的层级结构      

        在一个复杂的多线程程序中,可能存在多个共享资源,每个资源都需要使用一个独立的锁进行保护。当多个线程需要同时访问多个资源时,为了避免死锁,需要按照一定的顺序获取这些锁。锁的层级结构可以通过定义锁的获取和释放顺序来实现。

#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <stack>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <stdexcept>
#include <climits>
#include <chrono>class hierarchical_mutex
{std::mutex internal_mutex;unsigned long const hierarchy_value;unsigned long previous_hierarchy_value;static thread_local unsigned long this_thread_hierarchy_value;void check_for_hierarchy_violation(){if(this_thread_hierarchy_value <= hierarchy_value){throw std::logic_error("mutex hierarchy violated");}}void update_hierarchy_value(int value){previous_hierarchy_value=this_thread_hierarchy_value;this_thread_hierarchy_value=hierarchy_value;std::cout<<"update_hierarchy_value "<<"previous_hierarchy_value "<<previous_hierarchy_value<<std::endl;std::cout<<"update_hierarchy_value "<<"this_thread_hierarchy_value "<<this_thread_hierarchy_value<<std::endl;std::cout<<"value:"<<value<<std::endl;}
public:explicit hierarchical_mutex(unsigned long value):hierarchy_value(value),previous_hierarchy_value(0){}void lock(int value){std::cout<<"lock:"<<value<<std::endl;check_for_hierarchy_violation();internal_mutex.lock();update_hierarchy_value(value);}void unlock(int value){std::cout<<"unlock:"<<value<<std::endl;this_thread_hierarchy_value=previous_hierarchy_value;internal_mutex.unlock();}bool try_lock(int value){check_for_hierarchy_violation();if(!internal_mutex.try_lock())   //锁被占用,返回falsereturn false;update_hierarchy_value(value);return true;}
};/*函数声明为thread_local,使得每个线程都有其拷贝的副本,线程中的该变量独立互不影响*/
thread_local unsigned longhierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX);      //最大值hierarchical_mutex m1(42);
hierarchical_mutex m2(2000);void thread_function1() {// 线程1尝试获取 m1(层级值为42)锁m1.lock(1);std::cout << "Thread 1 acquired m1 lock." << std::endl;// 在持有 m1 锁的情况下,线程1尝试获取 m2(层级值为2000)锁m2.lock(1);std::cout << "Thread 1 acquired m2 lock." << std::endl;// 这里可以执行一些线程1的工作// 释放锁时必须按相反的顺序释放m2.unlock(1);m1.unlock(1);
}void thread_function2() {// 线程2尝试获取 m2(层级值为2000)锁m2.lock(2);std::cout << "Thread 2 acquired m2 lock." << std::endl;// 在持有 m2 锁的情况下,线程2尝试获取 m1(层级值为42)锁,这将导致层级违规m1.lock(2); // 这里将引发 std::logic_error 异常std::cout << "Thread 2 acquired m1 lock." << std::endl;// 这里可以执行一些线程2的工作// 释放锁时必须按相反的顺序释放m1.unlock(2);m2.unlock(2);
}int main() {std::thread t1(thread_function1);std::thread t2(thread_function2);t1.join();t2.join();return 0;
}

 运行结果:   

分析:

  1. 线程1运行 m1.lock(1);进行低层次枷锁,同时线程2运行 m2.lock(2);进行高层次加锁。
  2. 此时线程1运行到m2.lock(1);,准备进行低层次加锁
  3. 发现之前已经被加过锁了,锁是高层次锁。因此抛出异常。 通过这种方式避免嵌套锁导致死锁的发生

4.转移锁的所有权

        转移锁的所有权是指将一个互斥锁(mutex)从一个线程转移到另一个线程,使得新的线程可以继续使用该锁。

#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <stack>
#include <chrono>
#include <condition_variable>
#include <mutex>std::mutex some_mutex;void prepare_data(){std::cout<<"prepare_data"<<std::endl;}
void do_something(){std::cout<<"do_something"<<std::endl;}std::unique_lock<std::mutex> get_lock()
{extern std::mutex some_mutex;std::unique_lock<std::mutex> lk(some_mutex);prepare_data();return lk;  // 1
}void process_data()
{std::unique_lock<std::mutex> lk(get_lock());  // 2do_something();
}int main()
{std::unique_lock<std::mutex> main_lock(get_lock()); // 获取互斥量所有权std::cout << "Main thread is doing something..." << std::endl;// 在进入 process_data 函数之前释放互斥量的所有权main_lock.unlock();// 在 process_data 函数中获取互斥量的所有权process_data();// 在 process_data 函数返回后重新获取互斥量的所有权main_lock.lock();return 0;
}

优点:

        减少锁的竞争:当一个线程完成了对共享资源的操作后,可以将锁的所有权转移到下一个需要访问共享资源的线程,避免了其他线程之间的锁竞争,提高了并发性能。

        减少上下文切换:转移锁的所有权可以避免线程在释放锁后重新竞争锁的过程,从而减少了不必要的上下文切换,提高了系统的响应性能。

  缺点:转移锁的所有权需要额外的代码来管理锁的状态和转移过程,增加了代码的复杂性和维护成本。

四. 粗锁和细锁适用差别

        锁的粒度是指在并发编程中,锁定共享资源的范围大小,锁的粒度对程序性能有着重要的影响。

        较粗粒度的锁会锁定较大范围的共享资源,这意味着在并发访问时,只有一个线程能够访问该资源,其他线程需要等待。这种情况下,锁的争用会增加,可能导致线程间的竞争和等待时间增加,从而降低程序的性能。相反,较细粒度的锁会锁定较小范围的共享资源,这意味着并发访问时,多个线程可以同时访问不同的资源,减少了锁的争用。这种情况下,线程间的竞争和等待时间减少,从而提高了程序的性能。

        通常情况下,应该尽量使用细粒度的锁,以最大程度地减少锁的争用,提高并发性能。但是,过细的锁粒度也可能导致锁的开销增加,因此需要在实际应用中进行权衡和测试,找到合适的锁粒度

        以下展示适用一种粗锁提升性能的场景:

#include <iostream>
#include <thread>
#include <chrono>
#include <mutex>std::mutex mtx; // 全局互斥量
int counter = 0;void increment() {auto start = std::chrono::high_resolution_clock::now(); // 记录开始时间for (int i = 0; i < 1000000; ++i) {mtx.lock(); // 细粒度锁counter++;mtx.unlock(); // 细粒度锁}auto end = std::chrono::high_resolution_clock::now(); // 记录结束时间auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); // 计算运行时间std::cout << "Increment thread duration: " << duration.count() << " milliseconds" << std::endl;
}void decrement() {auto start = std::chrono::high_resolution_clock::now(); // 记录开始时间mtx.lock(); // 粗粒度锁for (int i = 0; i < 1000000; ++i) {counter--;}mtx.unlock(); // 粗粒度锁auto end = std::chrono::high_resolution_clock::now(); // 记录结束时间auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); // 计算运行时间std::cout << "Decrement thread duration: " << duration.count() << " milliseconds" << std::endl;
}int main() {std::thread t1(increment);std::thread t2(decrement);t1.join();t2.join();std::cout << "Counter: " << counter << std::endl;return 0;
}

        测量多次结果:

        

五. 构建线程安全的栈

        1.异常发生在接口处

        有时候我们可以将对共享数据的访问和修改聚合到一个函数,在函数内加锁保证数据的安全性。但是对于读取类型的操作,即使读取函数是线程安全的,但是返回值抛给外边使用,存在不安全性。比如一个栈对象,我们要保证其在多线程访问的时候是安全的,可以在判断栈是否为空,判断操作内部我们可以加锁,但是判断结束后返回值就不在加锁了,就会存在线程安全问题。

        比如我定义了如下栈, 对于多线程访问时判断栈是否为空,此后两个线程同时出栈,可能会造成崩溃,因为两个线程运行顺序可能如下:

template<typename T>
class threadsafe_stack1
{
private:std::stack<T> data;mutable std::mutex m;
public:threadsafe_stack1() {}threadsafe_stack1(const threadsafe_stack1& other){std::lock_guard<std::mutex> lock(other.m);data = other.data;}threadsafe_stack1& operator=(const threadsafe_stack1&) = delete;void push(T new_value){std::lock_guard<std::mutex> lock(m);data.push(std::move(new_value));}T pop(){std::lock_guard<std::mutex> lock(m);auto element = data.top();data.pop();return element;}bool empty() const{std::lock_guard<std::mutex> lock(m);return data.empty();}
};

        线程1和线程2先后判断栈都不为空,之后执行出栈操作,可能会造成崩溃。 

void test_threadsafe_stack1() {threadsafe_stack1<int> safe_stack;safe_stack.push(1);std::thread t1([&safe_stack]() {if (!safe_stack.empty()) {std::this_thread::sleep_for(std::chrono::seconds(1));safe_stack.pop();}});std::thread t2([&safe_stack]() {if (!safe_stack.empty()) {std::this_thread::sleep_for(std::chrono::seconds(1));safe_stack.pop();}});t1.join();t2.join();
}

         针对可能造成栈区异常,可以适当抛出异常来提醒。例如定义一个空栈函数,代码优化如下:

struct empty_stack : std::exception
{const char* what() const throw();
};
T pop()
{std::lock_guard<std::mutex> lock(m);if (data.empty()) throw empty_stack();auto element = data.top();data.pop();return element;
}

        2.异常发生在栈分配

       但是现在仍然还有可能存在问题,假设有一个stack<vector<int>>,vector是一个动态容器,当你拷贝一个vetcor,标准库会从堆上分配很多内存来完成这次拷贝。当这个系统处在重度负荷,或有严重的资源限制的情况下,这种内存分配就会失败,所以vector的拷贝构造函数可能会抛出一个std::bad_alloc异常。当vector中存有大量元素时,这种情况发生的可能性更大。当pop()函数返回“弹出值”时(也就是从栈中将这个值移除),会有一个潜在的问题:这个值被返回到调用函数的时候,栈才被改变;但当拷贝数据的时候,调用函数抛出一个异常会怎么样? 如果事情真的发生了,要弹出的数据将会丢失;它的确从栈上移出了,但是拷贝失败了!

        3.尽可能减少拷贝

        在上一小节提到由于函数出栈涉及数据拷贝,若此时异常内存暴增就会导致拷贝失败数据丢失。为避免上述问题,可采用一下方法

         3.1 传入引用
    void pop(T& value){std::lock_guard<std::mutex> lock(m);if(data.empty()) throw empty_stack();value=data.top();data.pop();}

        缺点:既然要传入引用, 大部分情况下需要临时构造出一个堆中类型的实例,用于接收目标值。从时间和资源的角度上来看都不划算。

         3.2 返回弹出元素的指针
std::shared_ptr<T> pop()
{std::lock_guard<std::mutex> lock(m);if(data.empty()) throw empty_stack();std::shared_ptr<T> const res(std::make_shared<T>(data.top()));data.pop();return res;
}

            直接pop出智能指针类型,这样在pop函数内部减少了数据的拷贝,防止内存溢出,其实这做法确实是相比之前直接pop固定类型的值更节省内存,运行效率也好很多。

六. 保护共享数据的初始化过程

1.早期局部静态的问题

        当一个函数中定义一个局部静态变量,那么这个局部静态变量只会初始化一次,就是在这个函数第一次调用的时候,以后无论调用几次这个函数,函数内的局部静态变量都不再初始化。
那我们可以利用局部静态变量这一特点实现单例. 在C++11 以前存在多线程不安全的情况,编译器可能会初始化多个静态变量。但是C++11推出以后,各厂商优化编译器,能保证线程安全。所以为了保证运行安全请确保使用C++11以上的标准。

2.延迟初始化   

        下述示例中,延时系统资源初始化过程,存在这样一个问题:在多线程情况下,可能同时满足if(!resource_ptr)条件导致数据多次重复初始化,为了解决这一问题,使用 双重检查锁定模式。

std::shared_ptr<some_resource> resource_ptr;
void foo()
{if(!resource_ptr){resource_ptr.reset(new some_resource);  // 1}resource_ptr->do_something();
}

3.双重检查锁定模式

        在下述锁中,先判断资源是否被初始化,如果为被初始化,则通过2步骤加锁,使得别的线程无法进行资源初始化,然后再次进行判断,防止等锁期间资源进行被初始化。

std::shared_ptr<some_resource> resource_ptr;
std::mutex resource_mutex;void foo()
{if(!resource_ptr){std::lock_guard<std::mutex> lock(resource_mutex);  // 2 if(!resource_ptr){resource_ptr.reset(new some_resource);  // 3}}resource_ptr->do_something();
}

但是双重锁定也存在潜在问题:

    在双重锁定模式中,一个线程可能在第一个条件判断中检查到 resource_ptr 为null,并认为资源尚未初始化。然后,在另一个线程中,资源被成功初始化(操作③),但这个修改可能仅存在于初始化线程的缓存中,尚未同步到主内存。   因此,第一个线程可能无法看到这个修改,仍然认为资源没有初始化, 调用 do_something() 的问题:如果第一个线程继续调用 do_something()(操作④),它可能会使用一个尚未初始化的资源,这会导致不正确的结果。  这是因为第一个线程无法正确看到第二个线程对 some_resource 实例的修改。

4.使用once_flag+call_once确保初数据竞争

std::shared_ptr<some_resource> resource_ptr;
std::once_flag resource_flag;void initialize_resource()
{resource_ptr.reset(new some_resource);
}void foo()
{std::call_once(resource_flag, initialize_resource);resource_ptr->do_something();
}

        引入了一个std::once_flag对象resource_flag,用于标记资源是否已经被初始化。initialize_resource函数用于实际初始化资源。在foo函数中,使用std::call_once来保证initialize_resource函数只会被调用一次。这样就可以避免多个线程同时初始化资源的问题,从而实现线程安全。

5.静态局部实例

        相对于call_once,使用静态局部变量来保证第一次调用初始化,保证线程安全。

        优点:直观+开销小+不依赖标准库+移植性强

#include <iostream>
#include <mutex>
#include <thread>class Singleton {
public:static Singleton& getInstance() {if (instance == nullptr) {std::lock_guard<std::mutex> lock(mutex);  // 加锁if (instance == nullptr) {instance = new Singleton();}}return *instance;}private:Singleton() { std::cout << "Singleton" << std::endl; }  // 构造函数私有化,确保不能直接实例化static Singleton* instance;static std::mutex mutex;
};Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mutex;int main() {std::thread t1([](){Singleton& s1 = Singleton::getInstance();std::cout << "Thread 1: " << &s1 << std::endl;});std::thread t2([](){Singleton& s2 = Singleton::getInstance();std::cout << "Thread 2: " << &s2 << std::endl;});t1.join();t2.join();return 0;
}

静态局部实例和静态全局实例区别:

        

静态局部实例:静态局部变量是在函数内部声明的静态变量,它们的初始化只会在第一次进入包含它们的函数时进行,然后在程序的生命周期内保持其状态。这是因为编译器会生成代码来确保只在第一次进入函数时初始化它们,而后续调用不会再次初始化。

静态全局实例:静态全局变量是在全局作用域内声明的静态变量,它们的初始化在程序启动时进行,由C++运行时系统管理。在多线程环境中,如果多个线程同时访问静态全局变量,可能会导致竞争条件,从而导致多次初始化。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/251842.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

三层交换组网实验(华为)

思科设备参考&#xff1a;三层交换组网实验&#xff08;思科&#xff09; 一&#xff0c;技术简介 三层交换技术的出现&#xff0c;解决子网必须依赖路由器进行管理的问题&#xff0c;解决传统路由器低速、复杂所造成的网络瓶颈问题。一个具有三层交换功能的设备可简单理解为…

蓝桥杯每日一题-----数位dp练习

题目 链接 参考代码 写了两个&#xff0c;一个是很久以前写的&#xff0c;一个是最近刚写的&#xff0c;很久以前写的时候还不会数位dp所以写了比较详细的注释&#xff0c;这两个代码主要是设置了不同的记忆数组&#xff0c;通过这两个代码可以理解记忆数组设置的灵活性。 im…

npm ERR! code CERT_HAS_EXPIRED

执行npm i报错&#xff1a; npm ERR! code ETIMEDOUT npm ERR! syscall connect npm ERR! errno ETIMEDOUT npm ERR! network request to https://registry.npmjs.org/react-redux failed, reason: connect ETIMEDOUT 104.16.2.35:443 npm ERR! network This is a problem rel…

python常用pandas函数nlargest / nsmallest及其手动实现

目录 pandas库 Series和DataFrame nlargest和nsmallest 用法示例 代替方法 手动实现 模拟代码 pandas库 是Python中一个非常强大的数据处理库&#xff0c;提供了高效的数据分析方法和数据结构。它特别适用于处理具有关系型数据或带标签数据的情况&#xff0c;同时在时间…

十分钟学会用springboot制作微信小程序富文本编辑器

1.1 富文本模型设计 在构建富文本编辑器系统时&#xff0c;首先需要设计一个合适的富文本模型。 CREATE TABLE IF NOT EXISTS rich_texts (id INT PRIMARY KEY AUTO_INCREMENT,title VARCHAR(255),content TEXT,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );这个表包括…

【算法与数据结构】647、516、LeetCode回文子串+最长回文子序列

文章目录 一、647、回文子串二、516、最长回文子序列三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、647、回文子串 思路分析&#xff1a;判断一个字符串是否为回文串那么必须确定回文串的所在区间&#xff0c;而一维…

(2)(2.13) Rockblock Satellite Modem

文章目录 前言 1 支持的MAVLink命令信息 2 设置 3 使用方法 4 数据成本 5 参数 前言 &#xff01;Note 该功能仅适用于 ArduPilot 4.4 或更高版本&#xff0c;并且要求飞行控制器支持 LUA 脚本(LUA Scripts)。 RockBLOCK 卫星调制解调器可实现与 ArduPilot 飞行器的全球…

如何将pdf转换成ppt?掌握这个方法就简单多了

有时候&#xff0c;PDF文件的布局和设计可能需要进行微调或重新排版&#xff0c;以适应PPT的特定格式和风格。那么怎么pdf怎么转ppt呢&#xff1f;为了更方便地对布局、字体、图像和其他元素进行编辑和调整&#xff0c;以符合PPT的需求&#xff0c;我们可以直接通过pdf在线转pp…

SQL Server之DML触发器

一、如何创建一个触发器呢 触发器的定义语言如下&#xff1a; CREATE [ OR ALTER ] TRIGGER trigger_nameon {table_name | view_name}{for | After | Instead of }[ insert, update,delete ]assql_statement从这个定义语言我们可以知道如下信息&#xff1a; trigger_name&…

Linux项目自动化构建工具之make/Makefile演示gcc编译

文章目录 一、背景二、如何使用&#xff1f;三、原理四、关于make的问题五、再次理解/编写makefile依赖关系依赖方法 六、原理讲解项目清理makefile是支持变量的取消执行make后显示命令依赖方法可以多行 一、背景 会不会写makefile&#xff0c;从一个侧面说明了一个人是否具备…

nop-entropy可逆计算入门(1)

第1步&#xff1a;从大佬的gitee&#xff1a;https://gitee.com/canonical-entropy/nop-entropy下载源码&#xff0c;进行本地编译&#xff0c;具体编译看项目下的readme,想偷懒的可以下载我编译后的jar&#xff0c;放到自己的maven仓库 https://pan.baidu.com/s/1p9MOh40MJ2m…

mac如何实现升级node版本、切换node版本

一、 查看node所有版本&#xff08;前提:安装了nodejs&#xff09; npm view node versions二、安装指定node版本 sudo n 版本号三、检查目前安装了哪些版本的node&#xff0c;会出现已安装的node版本 n四、切换已安装的node版本 sudo n 版本号其他命令 1、sudo npm cache…

Centos 内存和硬盘占用情况以及top作用

目录 只查看内存使用情况&#xff1a; 内存使用排序取前5个&#xff1a; 硬盘占用情况 定位占用空间最大目录 top查看cpu及内存使用信息 前言-与正文无关 生活远不止眼前的苦劳与奔波&#xff0c;它还充满了无数值得我们去体验和珍惜的美好事物。在这个快节奏的世界中&…

Matlab数字图像处理——图像复原与滤波算法应用方法

图像处理领域一直以来都是计算机科学和工程学的一个重要方向&#xff0c;图像复原则是其中一个重要的研究方向之一。图像复原旨在通过运用各种滤波算法&#xff0c;对图像进行去噪、恢复和改善&#xff0c;以提高图像的质量和可视化效果。在本文中&#xff0c;我们将介绍如下内…

python Flask 写一个简易的 web 端程序(附demo)

python Flask 写一个简易的 web 端程序 &#xff08;附demo&#xff09; 介绍简单介绍装饰器 app.route("/") 进阶增加接口设置端口 静态网页核心代码完整代码 介绍 Flask 是一个用于构建 Web 应用程序的轻量级 Python Web 框架。它设计简单、易于学习和使用&#x…

ReactNative实现宽度变化实现的动画效果

效果如上图所示&#xff0c;通过修改设备宽度实现动画效果 import React, {useRef, useEffect, useState} from react; import {Animated, Text, View, Image} from react-native;const FadeInView props > {const fadeAnim useRef(new Animated.Value(0)).current;React…

list基本使用

list基本使用 构造迭代器容量访问修改 list容器底层是带头双向链表结构&#xff0c;可以在常数范围内在任意位置进行输入和删除&#xff0c;但不支持任意位置的随机访问&#xff08;如不支持[ ]下标访问&#xff09;&#xff0c;下面介绍list容器的基本使用接口。 template <…

k8s学习-Kubernetes的网络

Kubernetes作为编排引擎管理着分布在不同节点上的容器和Pod。Pod、Service、外部组件之间需要⼀种可靠的方找到彼此并进行通信&#xff0c;Kubernetes网络则负责提供这个保障。 1.1 Kubernetes网络模型 Container-to-Container的网络 当Pod被调度到某个节点&#xff0c;Pod中…

ARM PAC指针认证的侧信道攻击——PACMAN安全漏洞

目录 Q1. PACMAN论文的内容是什么&#xff1f; Q2. Arm处理器是否存在漏洞&#xff1f; Q3. 受Arm合作伙伴架构许可设计的处理器实现是否受到影响&#xff1f; Q4. Cortex-M85受到影响吗&#xff1f; Q5. Cortex-R82受到影响吗&#xff1f; Q6. 指针认证如何保护软件&…

Python 实现 五子棋小游戏【附源码】

引言 五子棋是一种古老而深受欢迎的策略游戏&#xff0c;它具有简单的规则和无穷的变化。作为一种传统的中国棋类游戏&#xff0c;五子棋已经在世界范围内流行起来&#xff0c;并成为智力挑战和休闲娱乐的优秀选择。 规则和玩法&#xff1a; 五子棋使用一个15x15的棋盘&#x…