信息型企业网站有哪些,安徽网站建设优化推广,哪个网站做分享赚佣金,rss导入wordpress本章就以在C中进行安全的数据共享为主题。避免上述及其他潜在问题的发生的同时#xff0c;将共享数据的优势发挥到最大。 一. 锁分类和使用
按照用途分为互斥、递归、读写、自旋、条件变量。本章节着重介绍前四种#xff0c;条件变量后续章节单独介绍。
由于锁无法进行拷贝… 本章就以在C中进行安全的数据共享为主题。避免上述及其他潜在问题的发生的同时将共享数据的优势发挥到最大。 一. 锁分类和使用
按照用途分为互斥、递归、读写、自旋、条件变量。本章节着重介绍前四种条件变量后续章节单独介绍。
由于锁无法进行拷贝和转移通常与包装器进行连用。例如lock_guard、unique_lock。
包装器lock_guardunique_lockshared_lock含义作用域内自动获取互斥锁并在作用域结束时释放锁以确保资源的安全访问。管理互斥锁的获取和释放更加灵活可以指定解锁加锁的时间点允许多个线程同时共享一个互斥锁以提高并发性能.例如多读一写的场景属性不可移动、赋值、拷贝。必须通过构造函数初始化和析构函数销毁能拷贝、赋值、移动也可通过构造函数初始化和析构函数销毁能拷贝、赋值、移动灵活性 差 不可指定加锁解锁时间点 灵活可随意更改加锁解锁时间点。还可以锁超时、不锁定、条件变量灵活可随意更改加锁解锁时间点。还可以锁超时、不锁定、条件变量性能强 差并发强传入参数 三者都支持adopt_lock_t、try_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 algorithmusing namespace std;mutex coutMutex;void function(int i)
{thread::id threadId this_thread::get_id();{lock_guardmutex lock(coutMutex);coutmy thread id:threadId i:iendl;}//锁自动释放使用权
}int main()
{vectorthread 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 mutexstd::recursive_mutex myMutex;void foo(int val) {std::lock_guardstd::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 mutexstd::shared_mutex mutex;
int sharedData 0;void readerThread() {while (true) {std::shared_lockstd::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_lockstd::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 chronostd::mutex mutex1;
std::mutex mutex2;void threadFunction1() {std::unique_lockstd::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_lockstd::mutex lock2(mutex2); // 尝试获取mutex2std::cout Thread 1: Got both locks! std::endl;
}void threadFunction2() {std::unique_lockstd::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_lockstd::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_lockstd::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_lockstd::mutex lock2(mutex2); // 尝试获取mutex2std::cout Thread 1: Got both locks! std::endl;}
}void threadFunction2() {std::unique_lockstd::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_lockstd::mutex lock2(mutex2); // 尝试获取mutex2std::cout Thread 2: Got both locks! std::endl;}
} 适用场景适用于需要对多个互斥锁进行加锁且对性能要求不高的场景并且需要确保以相同的顺序对这些锁进行加锁。 优点实现相对简单不需要复杂的算法和数据结构。只需要定义好资源的顺序即可。 缺点固定顺序加锁解锁可能会降低系统的灵活性由于多个线程都按照顺序等待锁的占用释放可能会对系统的性能产生一定的影响。 2.使用超时锁 超时锁Timeout Lock是一种在多线程编程中常用的技术用于在一段时间内尝试获取锁如果超过指定的时间仍未成功获取锁则放弃获取锁并执行相应的处理逻辑。
#include iostream
#include mutex
#include condition_variable
#include chronostd::mutex mtx;
std::condition_variable cv;
bool isLocked false;bool tryLockFor(int milliseconds) {std::unique_lockstd::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_guardstd::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 chronoclass 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_valuethis_thread_hierarchy_value;this_thread_hierarchy_valuehierarchy_value;std::coutupdate_hierarchy_value previous_hierarchy_value previous_hierarchy_valuestd::endl;std::coutupdate_hierarchy_value this_thread_hierarchy_value this_thread_hierarchy_valuestd::endl;std::coutvalue:valuestd::endl;}
public:explicit hierarchical_mutex(unsigned long value):hierarchy_value(value),previous_hierarchy_value(0){}void lock(int value){std::coutlock:valuestd::endl;check_for_hierarchy_violation();internal_mutex.lock();update_hierarchy_value(value);}void unlock(int value){std::coutunlock:valuestd::endl;this_thread_hierarchy_valueprevious_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运行 m1.lock(1);进行低层次枷锁同时线程2运行 m2.lock(2);进行高层次加锁。此时线程1运行到m2.lock(1);准备进行低层次加锁发现之前已经被加过锁了锁是高层次锁。因此抛出异常。 通过这种方式避免嵌套锁导致死锁的发生
4.转移锁的所有权 转移锁的所有权是指将一个互斥锁mutex从一个线程转移到另一个线程使得新的线程可以继续使用该锁。
#include iostream
#include string
#include thread
#include mutex
#include stack
#include chrono
#include condition_variable
#include mutexstd::mutex some_mutex;void prepare_data(){std::coutprepare_datastd::endl;}
void do_something(){std::coutdo_somethingstd::endl;}std::unique_lockstd::mutex get_lock()
{extern std::mutex some_mutex;std::unique_lockstd::mutex lk(some_mutex);prepare_data();return lk; // 1
}void process_data()
{std::unique_lockstd::mutex lk(get_lock()); // 2do_something();
}int main()
{std::unique_lockstd::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 mutexstd::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_caststd::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_caststd::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.异常发生在接口处 有时候我们可以将对共享数据的访问和修改聚合到一个函数在函数内加锁保证数据的安全性。但是对于读取类型的操作即使读取函数是线程安全的但是返回值抛给外边使用存在不安全性。比如一个栈对象我们要保证其在多线程访问的时候是安全的可以在判断栈是否为空判断操作内部我们可以加锁但是判断结束后返回值就不在加锁了就会存在线程安全问题。 比如我定义了如下栈, 对于多线程访问时判断栈是否为空此后两个线程同时出栈可能会造成崩溃,因为两个线程运行顺序可能如下 templatetypename T
class threadsafe_stack1
{
private:std::stackT data;mutable std::mutex m;
public:threadsafe_stack1() {}threadsafe_stack1(const threadsafe_stack1 other){std::lock_guardstd::mutex lock(other.m);data other.data;}threadsafe_stack1 operator(const threadsafe_stack1) delete;void push(T new_value){std::lock_guardstd::mutex lock(m);data.push(std::move(new_value));}T pop(){std::lock_guardstd::mutex lock(m);auto element data.top();data.pop();return element;}bool empty() const{std::lock_guardstd::mutex lock(m);return data.empty();}
}; 线程1和线程2先后判断栈都不为空之后执行出栈操作可能会造成崩溃。
void test_threadsafe_stack1() {threadsafe_stack1int 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_guardstd::mutex lock(m);if (data.empty()) throw empty_stack();auto element data.top();data.pop();return element;
} 2.异常发生在栈分配 但是现在仍然还有可能存在问题假设有一个stackvectorintvector是一个动态容器当你拷贝一个vetcor标准库会从堆上分配很多内存来完成这次拷贝。当这个系统处在重度负荷或有严重的资源限制的情况下这种内存分配就会失败所以vector的拷贝构造函数可能会抛出一个std::bad_alloc异常。当vector中存有大量元素时这种情况发生的可能性更大。当pop()函数返回“弹出值”时(也就是从栈中将这个值移除)会有一个潜在的问题这个值被返回到调用函数的时候栈才被改变但当拷贝数据的时候调用函数抛出一个异常会怎么样 如果事情真的发生了要弹出的数据将会丢失它的确从栈上移出了但是拷贝失败了 3.避免竞争方法 3.1 传入引用使得操作原子化 void pop(T value){std::lock_guardstd::mutex lock(m);if(data.empty()) throw empty_stack();valuedata.top();data.pop();} 既然要传入引用 大部分情况下需要临时构造出一个堆中类型的实例用于接收目标值。从时间和资源的角度上来看都不划算 3.2 返回弹出元素的指针
std::shared_ptrT pop()
{std::lock_guardstd::mutex lock(m);if(data.empty()) throw empty_stack();std::shared_ptrT const res(std::make_sharedT(data.top()));data.pop();return res;
} 直接pop出智能指针类型这样在pop函数内部减少了数据的拷贝防止内存溢出其实这做法确实是相比之前直接pop固定类型的值更节省内存运行效率也好很多。
六. 保护共享数据的初始化过程
1.早期局部静态的问题 当一个函数中定义一个局部静态变量那么这个局部静态变量只会初始化一次就是在这个函数第一次调用的时候以后无论调用几次这个函数函数内的局部静态变量都不再初始化。 那我们可以利用局部静态变量这一特点实现单例. 在C11 以前存在多线程不安全的情况编译器可能会初始化多个静态变量。但是C11推出以后各厂商优化编译器能保证线程安全。所以为了保证运行安全请确保使用C11以上的标准。
2.延迟初始化 下述示例中延时系统资源初始化过程存在这样一个问题在多线程情况下可能同时满足if(!resource_ptr)条件导致数据多次重复初始化为了解决这一问题使用 双重检查锁定模式。
std::shared_ptrsome_resource resource_ptr;
void foo()
{if(!resource_ptr){resource_ptr.reset(new some_resource); // 1}resource_ptr-do_something();
}3.双重检查锁定模式 在下述锁中先判断资源是否被初始化如果为被初始化则通过2步骤加锁使得别的线程无法进行资源初始化然后再次进行判断防止等锁期间资源进行被初始化。
std::shared_ptrsome_resource resource_ptr;
std::mutex resource_mutex;void foo()
{if(!resource_ptr){std::lock_guardstd::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_flagcall_once确保初数据竞争
std::shared_ptrsome_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 threadclass Singleton {
public:static Singleton getInstance() {if (instance nullptr) {std::lock_guardstd::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运行时系统管理。在多线程环境中如果多个线程同时访问静态全局变量可能会导致竞争条件从而导致多次初始化。