一起做网店类型的网站,广东东莞电子厂,wordpress首页调用文章数量,网站后台维护教程目录
一、线程互斥
1、回顾相关概念
2、抢票场景分析代码
多个线程同时操作全局变量
产生原因
如何解决
二、互斥量
1、概念
2、初始化互斥量#xff1a;
方法1#xff1a;静态分配
方法2#xff1a;动态分配
3、销毁互斥量#xff1a;
4、加锁和解锁
示例抢…目录
一、线程互斥
1、回顾相关概念
2、抢票场景分析代码
多个线程同时操作全局变量
产生原因
如何解决
二、互斥量
1、概念
2、初始化互斥量
方法1静态分配
方法2动态分配
3、销毁互斥量
4、加锁和解锁
示例抢票静态分配互斥量
示例抢票动态分配互斥量
5、加锁粒度
6、深入理解互斥锁
7、互斥量实现原理 交换的现象:内存与%a 做交换
交换的本质:共享-私有
8、互斥锁操作场景
三、可重入与线程安全
1、概念
2、常见的线程不安全的情况
3、常见的线程安全的情况
4、重入概念
5、常见不可重入的情况
6、常见可重入的情况
7、可重入与线程安全 一、线程互斥
1、回顾相关概念
临界资源是指那些在多线程或多进程环境下不允许同时被多个执行流访问的资源。这些资源的特点在于它们的状态可能因并发访问而变得不确定或引发错误因此要求在某一时刻最多只能有一个线程或进程对其进行访问和修改。 临界区则是指在各个线程或进程中针对临界资源的那一部分代码区域。当一个线程进入临界区时它正在访问或操作临界资源此时应确保其他线程不能同时进入这一相同的代码区域以防止竞态条件的发生。 互斥Mutex互斥量是一种同步机制用来确保在多线程或多进程环境中对临界资源的互斥访问。互斥量如同一把钥匙任何时刻只有一个线程能够持有这把钥匙即获得互斥量从而进入并操作临界资源。一旦线程完成了对临界资源的访问它必须释放互斥量使得其他等待的线程有机会获得这把钥匙并进入临界区。 原子性是一种更为底层的概念在并发编程中指的是一个操作要么全部完成要么完全不执行中间状态对外部是不可见的。互斥量的获取和释放通常是在硬件层面支持下实现的一种原子操作以此来保证线程间的同步行为正确无误。
2、抢票场景分析代码
下面这段代码中出现了多线程访问同一个全局变量tickets并对其进行减一操作的情景模拟了多线程环境下并发抢购门票的情况。
tickets初始值为10000表示共有10000张门票。每个线程都在循环中尝试购买一张门票即减少全局变量tickets的值。由于没有使用任何同步机制这种并发访问和修改全局变量的行为很可能导致数据不一致的问题即所谓的竞态条件。
#include iostream
#include pthread.h
#include unistd.hint tickets 10000; // 全局变量表示剩余的门票数量// 线程函数模拟抢票操作
void* getTickets(void* args)
{(void)args;//抑制编译器对未使用参数的警告while(true){// 模拟短暂的延迟usleep(1000);// 临界区操作但此处并未使用锁进行保护if(tickets 0){// 打印当前线程和剩余票数printf(%p: %d\n, pthread_self(), tickets);// 并发环境下多个线程可能同时判断tickets 0为真然后同时减一导致数据不一致tickets--;}else{break;}}return nullptr;
}int main()
{pthread_t t1, t2, t3;// 创建三个线程pthread_create(t1, nullptr, getTickets, nullptr);pthread_create(t2, nullptr, getTickets, nullptr);pthread_create(t3, nullptr, getTickets, nullptr);// 等待所有线程执行完毕pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);// 输出最后剩余的票数在实际运行中可能不准确因为存在竞态条件std::cout 最终剩余票数: tickets std::endl;return 0;
}0x7f3c2c001700: 10000
0x7f3c2c001940: 9999
0x7f3c2c001b80: 9998
0x7f3c2c001700: 9997
0x7f3c2c001940: 9996
0x7f3c2c001b80: 9995
... 假设顺利递减无竞态条件发生
0x7f3c2c001700: 2
0x7f3c2c001940: 1
0x7f3c2c001b80: 0
最终剩余票数: -1 或者其他非零数值
取决于最后一个线程何时减少票数以及是否有多个线程同时进行了最后一次减法操作。 拓展知识(void)args; 这一行的作用是用来抑制编译器对未使用参数的警告。 在这个上下文中args 是线程函数 getTickets 的参数但是在函数体内并没有使用这个参数。通常情况下编译器可能会发出未使用参数的警告为了避免这种警告将参数强制转换为 void 类型并丢弃以明确告诉编译器我们知道这个参数没有被使用而且我们故意这样做。在多线程编程中有时候线程函数并不需要从主线程那里接收任何参数但为了兼容线程创建函数如pthread_create的接口要求通常还是会保留一个形参。这里的 (void)args; 就是处理这种情况的一种方式。在本例中参数args实际并未起到传递有用信息的作用所以被忽略了。 多个线程同时操作全局变量 当多个线程同时进入if(tickets 0)判断时由于没有加锁或其他同步手段可能存在一种情况即多个线程同时判定条件为真然后同时执行tickets--操作。这样最终剩余的票数可能会少于实际应有的数目。 若要避免这种情况需要在对tickets进行读取和修改的地方添加互斥锁例如使用pthread_mutex_t确保每次只有一个线程能够进入临界区修改变量的代码段其他线程必须等待锁释放后才能继续执行。 正确的做法是在修改tickets前锁定互斥锁在修改后解锁互斥锁这样就能保证多线程环境下数据的一致性。 上述代码中的usleep(1000)是为了模拟网络延迟或其它消耗时间的操作使得多线程并发问题更容易出现实际上在高速并发环境下即便很小的时间窗口也可能导致竞态条件的发生。
产生原因 在多线程并发编程中CPU执行线程的调度是由操作系统内核进行的。每个线程有自己的上下文包括程序计数器指示下一条要执行的指令、寄存器存储临时变量和状态信息以及其他与线程执行有关的信息。当CPU从一个线程切换到另一个线程时内核会保存当前线程的上下文然后恢复下一个要执行线程的上下文。
int tickets 10000; // 全局变量表示剩余的门票数量// 线程函数模拟抢票操作
void* getTickets(void* args)
{(void)args;//抑制编译器对未使用参数的警告while(true){// 模拟短暂的延迟usleep(1000);// 临界区操作但此处并未使用锁进行保护if(tickets 0){// 打印当前线程和剩余票数printf(%p: %d\n, pthread_self(), tickets);// 并发环境下多个线程可能同时判断tickets 0为真然后同时减一导致数据不一致tickets--;}else{break;}}return nullptr;
}
在上述代码示例中tickets变量存储在内存中所有线程都能访问。当两个或更多的线程同时到达if(tickets 0)这个判断点时假设它们都观察到tickets大于0如下所示
CPU将线程A的上下文切换出去保存其现场。CPU切换到线程B线程B也判断tickets 0为真执行tickets--。线程B的上下文被暂时保存CPU又切换回到线程A。线程A接着执行tickets--尽管在CPU切换到线程B时线程A尚未完成减1操作。 这样一来尽管原本只有1张票可供销售但由于缺乏同步机制两张票都被认为已经售出这就是所谓的“竞态条件”Race Condition。这是因为CPU在同一时间窗口内允许多个线程访问并修改共享资源tickets造成了数据的不一致性。 内存访问方面由于现代计算机体系结构的缓存一致性协议在多核CPU中各个核心的缓存可能持有不同线程对tickets变量的缓存副本。当一个核心上的线程修改了tickets的值该变化可能不会立即传播到其他核心的缓存中。这也是导致多线程并发时数据不一致的另一因素称为“缓存一致性问题”。
如何解决 为解决这个问题确实需要引入互斥锁mutex或其他同步机制。当一个线程进入临界区即对tickets进行读写操作的代码段时会先获取互斥锁确保其他线程在锁未释放前无法进入该临界区。这样每次只有一个线程可以修改tickets的值从而确保了数据的一致性和完整性。 在多线程编程中为了保证共享资源的访问安全我们需要一种机制来确保在某一时刻只有一个线程能访问临界区即包含修改共享资源的代码段。上述描述的问题是在无锁机制的情况下如何原子地更新一个共享变量例如ticket计数器。 在多核CPU环境中对共享变量的操作如 load、update、store并非原子操作也就是说这些操作可能会被中断并在不同线程间交错执行导致竞态条件race condition和数据不一致的问题。 要解决这个问题确实需要引入互斥量Mutex这一同步原语。互斥量提供了一种机制使得满足以下三个关键点 互斥行为当一个线程获得了互斥量锁并进入临界区时其他线程将无法同时获取该锁并进入同一临界区。这就确保了在同一时间内只有一个线程可以执行临界区内的代码。 公平性如果有多个线程都在等待进入临界区而此时临界区内没有线程执行那么当互斥量解锁时调度器会选择其中一个等待的线程赋予锁使其进入临界区执行。虽然互斥量并不严格保证“先来后到”的顺序但在实际应用中很多互斥量实现包括POSIX互斥量都会尽量实现公平调度。 高效利用资源当一个线程不在临界区执行也就是说它已经解锁了互斥量那么其他等待的线程就可以立即获取锁并进入临界区不会受到阻碍。
因此在Linux及支持POSIX线程库的操作系统中通过使用互斥量API如pthread_mutex_lock和pthread_mutex_unlock我们可以有效地控制线程对共享资源的访问从而确保数据的一致性和正确性。
二、互斥量
1、概念
在Linux以及其他支持POSIX线程标准的操作系统中互斥量是一种基本的同步机制用于保护共享资源免受并发访问时的数据竞争。
对于线程局部存储的数据由于它们位于各自的栈空间内天然地受到线程隔离的保护不会引起数据竞争问题。然而当多个线程需要共享某些数据时这些共享变量就会成为潜在的临界资源。如果不采取适当的同步措施如使用互斥量或者其他同步原语并发访问共享变量可能导致数据不一致性和其他难以预测的行为。因此在并发编程中合理使用互斥量或其他同步工具对于保证数据的正确性和一致性至关重要。
2、初始化互斥量
方法1静态分配
通过预定义的常量 PTHREAD_MUTEX_INITIALIZER 可以静态初始化互斥量这意味着互斥量作为全局变量或静态局部变量声明时可以直接初始化
pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;
这种方式下编译器会在程序加载时自动初始化互斥量无需显式调用 pthread_mutex_init 函数。这样的互斥量通常在整个程序生命周期内有效并且无需销毁。
方法2动态分配
如果你需要在运行时动态分配和初始化互斥量例如从堆上分配你需要调用 pthread_mutex_init 函数
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
mutex指向你想要初始化的互斥量对象的指针。attr指向一个互斥量属性对象的指针若使用默认属性大多数情况下可以传递 NULL。通过非默认属性可以配置互斥量的行为比如设置它是否为递归锁、错误检查策略等。
3、销毁互斥量
当你不再需要一个动态初始化的互斥量时应当调用 pthread_mutex_destroy 函数来释放它所占用的资源
int pthread_mutex_destroy(pthread_mutex_t *mutex);
mutex需要销毁的互斥量对象的指针。
注意事项
静态初始化的互斥量 不需要调用 pthread_mutex_destroy 进行销毁因为它们的生命周期与程序相同会在程序结束时自动清理。绝对不能销毁一个已经被加锁的互斥量 否则会导致未定义行为可能引发死锁或其他同步问题。确保所有线程都不会尝试去加锁已被销毁的互斥量 。一旦互斥量被销毁其状态就不再有效后续任何对该互斥量的操作都是非法的。
正确使用互斥量的生命周期管理有助于避免潜在的并发错误和资源泄露。
4、加锁和解锁 pthread_mutex_lock 和 pthread_mutex_unlock 是 POSIX 线程库中用来实现互斥量Mutex加锁和解锁的核心函数。互斥量通过 lock 和 unlock 函数提供了线程间的一种同步机制确保同一时间只有一个线程能够访问临界区即受互斥量保护的代码区域从而防止数据竞争的发生。在多线程环境下合理地使用互斥量对于保证数据一致性至关重要。 pthread_mutex_lock(pthread_mutex_t mutex)
此函数用于锁定互斥量。当一个线程调用此函数试图锁定互斥量时可能出现以下两种情况 互斥量未被锁定如果当前互斥量处于未锁定状态调用此函数的线程会立刻获得锁并将互斥量置为锁定状态然后函数返回0表示操作成功。 互斥量已被锁定如果互斥量已经被其他线程锁定或者有其他线程在同一时刻也尝试锁定互斥量但尚未成功获取锁那么调用 pthread_mutex_lock 的线程将会进入阻塞状态即挂起不再执行后续代码直到互斥量被解锁。当互斥量解锁后该线程会被唤醒并继续执行最终成功锁定互斥量。
pthread_mutex_unlock(pthread_mutex_t mutex)
此函数用于解锁互斥量。只有当前持有互斥量锁的线程才能成功调用此函数。当调用成功后互斥量变为未锁定状态如果有其他正在等待此互斥量的线程其中一个会被唤醒并获得锁继续执行。
示例抢票静态分配互斥量
我们在这个程序中利用三个并发线程模拟抢票过程通过互斥锁确保了在多个线程间对剩余票数进行原子性操作从而避免了竞态条件和数据竞争问题。程序执行完成后将显示最终剩余票数理论上应为0。
#include iostream
#include pthread.h
#include unistd.hint tickets 10000; // 全局变量表示剩余的门票数量
pthread_mutex_t ticket_mutex PTHREAD_MUTEX_INITIALIZER; // 静态分配互斥量// 线程函数模拟抢票操作
void* getTickets(void*)
{while (true){// 加锁pthread_mutex_lock(ticket_mutex);// 模拟短暂的延迟usleep(1000);// 临界区操作现在受互斥量保护if (tickets 0){// 打印当前线程和剩余票数printf(%p: %d\n, pthread_self(), tickets);// 安全地减少票数tickets--;// 如果剩余票数为0则退出循环if (tickets 0){break;}}else{break;}// 解锁pthread_mutex_unlock(ticket_mutex);}return nullptr;
}int main()
{pthread_t t1, t2, t3;// 创建三个线程pthread_create(t1, nullptr, getTickets, nullptr);pthread_create(t2, nullptr, getTickets, nullptr);pthread_create(t3, nullptr, getTickets, nullptr);// 等待所有线程执行完毕pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);// 输出最后剩余的票数现在应该是准确的std::cout 最终剩余票数: tickets std::endl;return 0;
} 包含头文件 iostream用于标准输入输出操作。pthread.h包含了创建和管理线程所需的函数声明以及互斥锁等相关同步原语。unistd.h提供了usleep函数用于模拟线程执行过程中短暂的延迟。 全局变量和静态初始化互斥量 int tickets 10000;全局变量tickets表示剩余的门票数量初始化为10000张。pthread_mutex_t ticket_mutex PTHREAD_MUTEX_INITIALIZER;静态初始化了一个互斥锁ticket_mutex。这个互斥锁将在多个线程间共享用于保护对tickets变量的访问。 线程函数getTickets void* getTickets(void*)
{while (true){// 加锁pthread_mutex_lock(ticket_mutex);// 模拟短暂的延迟usleep(1000);// 临界区操作现在受互斥量保护if (tickets 0){// 打印当前线程和剩余票数printf(%p: %d\n, pthread_self(), tickets);// 安全地减少票数tickets--;// 如果剩余票数为0则退出循环if (tickets 0)break;}elsebreak;// 解锁pthread_mutex_unlock(ticket_mutex);}return nullptr;
}函数类型为void*符合pthread库要求的线程函数原型。参数为空指针void*此处未使用额外参数。使用无限循环while(true)不断尝试抢票直至所有票售罄。 pthread_mutex_lock(ticket_mutex);加锁操作阻止其他线程在此期间访问tickets。usleep(1000);模拟短暂的延迟比如网络请求或系统处理时间单位为微秒1毫秒1000微秒。进入临界区此时对tickets的操作受互斥锁保护。 if (tickets 0)检查还有剩余票否。 printf(%p: %d\n, pthread_self(), tickets);打印当前线程ID由pthread_self()获取和剩余票数。tickets--;安全地减少剩余票数。if (tickets 0)如果票已售罄跳出循环。pthread_mutex_unlock(ticket_mutex);解锁互斥锁允许其他线程继续抢票。函数返回nullptr这是pthread库约定的线程函数返回值类型。 主函数main int main()
{pthread_t t1, t2, t3;// 创建三个线程pthread_create(t1, nullptr, getTickets, nullptr);pthread_create(t2, nullptr, getTickets, nullptr);pthread_create(t3, nullptr, getTickets, nullptr);// 等待所有线程执行完毕pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);// 输出最后剩余的票数现在应该是准确的std::cout 最终剩余票数: tickets std::endl;return 0;
} 定义三个pthread_t类型的变量t1, t2, t3分别代表三个线程的句柄。使用pthread_create创建三个线程分别指定它们执行getTickets函数且不传递任何附加参数。使用pthread_join依次等待这三个线程执行完毕。主线程会在此处阻塞直到每个子线程都结束其工作。最后输出“最终剩余票数”理论上此时应该为0因为当票数减至0时所有线程都会停止抢票。主函数返回0表示程序正常结束。
示例抢票动态分配互斥量
我们这个程序通过两个并发线程模拟了两个用户同时在线抢购有限数量10000张票的过程。互斥锁确保了对共享资源剩余票数的访问是线程安全的而断言有助于检查互斥锁操作的正确性。最后我们计算并输出了整个抢票过程的运行时间精确到小数点后两位。
#include iostream
#include pthread.h
#include string
#include unistd.h
#include cassert
#include iomanip
using namespace std;#define THREAD_NUM 2
int tickets 10000;class ThreadData
{
public:ThreadData(const std::string n, pthread_mutex_t *pm) : tname(n), pmutex(pm){}std::string tname;pthread_mutex_t *pmutex;
};void *getTickets(void *args)
{ThreadData *td (ThreadData *)args;while (true){// 抢占逻辑int n pthread_mutex_lock(td-pmutex);assert(n 0);if (tickets 0){ // 判断的本质也是计算的一种usleep(rand() % 1500);printf(%s: %d\n, td-tname.c_str(), tickets);tickets--;n pthread_mutex_unlock(td-pmutex); // 也可能出现问题assert(n 0);}else{n pthread_mutex_unlock(td-pmutex);assert(n 0);break;}// 抢完票还需要后续的动作usleep(rand() % 2000);}delete td;return nullptr;
}int main()
{time_t start time(nullptr);pthread_mutex_t mtx;pthread_mutex_init(mtx, nullptr);srand((unsigned long)time(nullptr) ^ getpid() ^ 0x147);pthread_t t[THREAD_NUM];// 多线程抢票的逻辑for (int i 0; i THREAD_NUM; i){std::string name thread ;name std::to_string(i 1);ThreadData *td new ThreadData(name, mtx);pthread_create(t i, nullptr, getTickets, (void *)td);}for (int i 0; i THREAD_NUM; i){pthread_join(t[i], nullptr);}pthread_mutex_destroy(mtx);time_t end time(nullptr);double elapsed_seconds difftime(end, start);cout fixed setprecision(2) time: elapsed_seconds s endl;
}
这段C代码是一个多线程模拟抢票系统的实现与之前讲解的版本相同。代码的核心结构和功能保持一致只是将线程数量(THREAD_NUM)从10减少到了2。以下是代码的详细解释 包含必要的头文件 iostream提供输入/输出流操作。pthread.h包含POSIX线程库的接口。string提供字符串操作支持。unistd.h包含Unix标准函数如usleep。cassert包含断言宏assert用于在运行时检测程序内部状态。iomanip包含操纵符如setprecision用于格式化输出。 定义类ThreadData class ThreadData
{
public:ThreadData(const std::string n, pthread_mutex_t *pm) : tname(n), pmutex(pm){}std::string tname;pthread_mutex_t *pmutex;
}; 类ThreadData用于封装每个线程的相关数据包括线程名称tname和指向互斥锁的指针pmutex。在创建线程时将每个线程的名称及共享的互斥锁传递给该类实例。 定义线程执行函数getTickets void *getTickets(void *args)
{ThreadData *td (ThreadData *)args;while (true){// 抢占逻辑int n pthread_mutex_lock(td-pmutex);assert(n 0);if (tickets 0){ // 判断的本质也是计算的一种usleep(rand() % 1500);printf(%s: %d\n, td-tname.c_str(), tickets);tickets--;n pthread_mutex_unlock(td-pmutex); // 也可能出现问题assert(n 0);}else{n pthread_mutex_unlock(td-pmutex);assert(n 0);break;}// 抢完票还需要后续的动作usleep(rand() % 2000);}delete td;return nullptr;
} 函数getTickets接受一个指向ThreadData实例的指针作为参数。使用while循环持续尝试抢票直到所有票售罄tickets 0为止。 使用pthread_mutex_lock对互斥锁进行加锁确保同一时刻只有一个线程能访问共享资源tickets变量。断言判断互斥锁加锁是否成功。判断剩余票数是否大于0若大于0则继续执行以下操作 调用usleep模拟随机延迟0至1500微秒模拟购票过程中的网络延迟或处理时间。使用printf打印当前线程名称和剩余票数。将tickets减一表示已售出一张票。解锁互斥锁并断言判断解锁是否成功。若票已售罄则直接解锁互斥锁并跳出循环。在每次购票后再次调用usleep模拟随机延迟0至2000微秒模拟购票后的其他动作如支付等。删除传入的ThreadData实例避免内存泄漏。返回nullptr表明线程执行完毕。 主函数main int main()
{time_t start time(nullptr);pthread_mutex_t mtx;pthread_mutex_init(mtx, nullptr);srand((unsigned long)time(nullptr) ^ getpid() ^ 0x147);pthread_t t[THREAD_NUM];// 多线程抢票的逻辑for (int i 0; i THREAD_NUM; i){std::string name thread ;name std::to_string(i 1);ThreadData *td new ThreadData(name, mtx);pthread_create(t i, nullptr, getTickets, (void *)td);}for (int i 0; i THREAD_NUM; i){pthread_join(t[i], nullptr);}pthread_mutex_destroy(mtx);time_t end time(nullptr);double elapsed_seconds difftime(end, start);cout fixed setprecision(2) time: elapsed_seconds s endl;
} 记录当前时间start。初始化一个互斥锁mtx用于保护tickets变量。使用srand设置随机数种子结合当前时间、进程ID和固定常数确保每次运行程序时生成不同的随机数序列。定义一个数组t存储THREAD_NUM个pthread_t类型的线程句柄。遍历数组为每个线程执行如下操作 构造线程名称格式为thread X其中X为线程索引1。创建一个新的ThreadData实例包含当前线程名称和互斥锁指针。使用pthread_create创建新线程指定线程执行函数为getTickets并将ThreadData实例的地址作为参数传递。使用pthread_join等待所有线程执行完毕。这会阻塞主线程直到每个子线程都完成其任务。销毁互斥锁mtx释放相关资源。记录当前时间end计算并输出程序运行时间end - start保留小数点后两位。
5、加锁粒度
加锁粒度越小越好 减少阻塞范围较小的锁粒度意味着临界区代码更短线程持有锁的时间更短。当一个线程持有锁时其他线程必须等待。因此缩短锁的持有时间可以减少线程间的等待时间提高并发性能降低因线程阻塞导致的上下文切换开销。 避免死锁可能性细粒度锁有助于减少锁的嵌套从而降低死锁的风险。当多个锁需要按照特定顺序获取时如果锁的粒度较大可能会导致复杂的锁依赖关系增加死锁的可能性。较小的锁粒度使得锁的管理更为简单更容易避免死锁。 提高系统可伸缩性在高并发场景下细粒度锁允许更多的线程并行执行因为它们可以在不影响其他线程所需资源的情况下独立工作。大粒度锁可能导致大量线程因争夺同一锁而陷入等待限制了系统的并发能力。
实现细粒度锁的策略 局部变量加锁如果可能尽量将锁的范围限制在局部变量上而不是整个数据结构。例如如果全局变量是一个容器如列表或字典不要对整个容器加锁而是针对每次插入、删除或查找操作单独加锁。 分段锁对于大型数据结构可以使用分段锁如Java中的ConcurrentHashMap或细粒度锁数组将数据分成多个部分每个部分有自己的锁。这样即使在高并发情况下不同的线程可以同时操作数据的不同部分而不必全部等待同一把锁。 原子操作对于简单的数值型全局变量如果支持的话可以使用原子操作如AtomicInteger、std::atomic等代替锁。原子操作在硬件级别保证了操作的完整性无需显式加锁提供了极细粒度的同步。 无锁数据结构和算法在某些场景下可以使用专门设计的无锁数据结构和算法它们通过CASCompare-and-Swap等非阻塞同步原语来实现线程安全进一步降低锁的使用和开销。
综上所述为了防止多线程访问全局变量时互相影响应使用加锁机制确保访问的原子性和一致性。同时遵循“加锁粒度越小越好”的原则通过减少阻塞范围、避免死锁以及提高系统可伸缩性来优化多线程程序的并发性能和稳定性
6、深入理解互斥锁 加锁是否意味着串行执行 是的在使用互斥锁保护的临界区内线程执行是串行的。具体来说当一个线程成功获取到互斥锁并进入临界区后其他试图获取该锁的线程将被阻塞直到持有锁的线程执行完毕临界区代码并释放锁。这种机制确保了在同一时刻只有一个线程能够访问和修改受保护的共享资源在这里是 tickets 变量。尽管线程间的调度仍然是不确定的但在互斥锁的约束下对临界区的访问是有序的、不可重叠的从而实现了对共享资源的串行化访问。 加锁后线程在临界区中是否会切换会有问题吗 线程在临界区中仍有可能被操作系统调度器切换出去这是正常的线程调度行为。然而即使发生切换由于该线程持有锁其他线程无法进入临界区执行相同的代码。这意味着 数据一致性得到保障即使持有锁的线程在执行临界区代码时被切换出去由于锁未释放其他线程无法干扰正在进行的操作。当持有锁的线程恢复执行时它将继续从上次中断的地方完成对 tickets 的操作不会出现数据竞争或竞态条件。 原子性体现互斥锁提供的保护确保了临界区内的操作如判断剩余票数、输出信息、递减票数作为一个整体对于其他线程而言是不可分割的即具有原子性。即使线程在执行这些操作的过程中被切换其他线程也无法看到中间状态只能看到操作完全完成后的结果。 关于锁作为共享资源的理解 正确锁本身确实是一种共享资源因为所有试图访问受保护资源的线程都需要与之交互。每个线程都必须先尝试获取这把锁只有成功获取锁的线程才能进入临界区。其他线程在锁被释放之前只能等待。这种共享性是线程间协作的基础通过统一的锁机制协调对共享资源的访问顺序。 谁来保证锁的安全 锁的安全由底层操作系统和/或硬件提供支持。具体实现细节取决于使用的编程语言和平台但通常包括以下方面 原子性操作对锁状态锁定/解锁的更改通常是通过底层硬件提供的原子指令如 Compare-and-Swap, CAS来实现的确保即使在多处理器环境下对锁状态的修改也不会被中断或产生竞态条件。 内核同步原语在用户态线程库如 Pthreads中对锁的操作通常封装成系统调用交由操作系统内核来处理。内核负责在更高层次上管理和调度锁的分配确保其正确性和安全性。
综上所述加锁的确意味着对临界区代码的串行执行即使线程在临界区内被切换互斥锁的存在确保了数据的一致性和操作的原子性。锁作为共享资源其安全性由操作系统和硬件的底层支持来保证确保线程在申请和释放锁时的原子性操作从而有效地协调多线程对共享资源的访问。 7、互斥量实现原理 swap或exchange指令 以一条汇编的方式将内存和CPU内寄存区数据进行交换 如果我们在汇编的角度只有一条汇编语句我们就认为该汇语句的执行是原子的! 在执行流视角是如何看待CPU上面的奇存器的?CPU内部的寄存器 本质叫做当前执行流的上下文!!寄存器们空间是被所有的执行流 共亭的但是寄存器的内容是被每一个执行流私有的!上下文! 在CPU架构中寄存器是位于CPU内部的小型存储单元用于临时存储运算数据或指令地址等信息。每个独立的执行线程或上下文都有自己的寄存器状态这些状态是私有的也就是说不同的执行流如多线程环境中的不同线程或进程在执行时它们各自的寄存器内容互不影响。 在单核CPU中同一时刻只有一个执行流线程在运行CPU通过上下文切换在不同的执行流之间切换。在切换时CPU会保存当前执行流的寄存器状态至内存通常是内核堆栈或任务控制块Task Control BlockTCB然后加载下一个即将执行的线程的寄存器状态。这样一来尽管寄存器空间物理上是共享的但每个执行流都有自己独立的一套寄存器值这就是所谓的上下文。
lock:movb $0%alxchgb almutexif(al寄存器的内容0){return 0;}else挂起等待;goto lock;
unlock:movb $l,mutex唤醒等待Mutex的线程;return 0; 交换的现象:内存与%a 做交换 lock这部分是获取互斥锁的过程。首先将寄存器al设置为0movb $0, %al然后使用xchg指令交换al的内容与mutex变量的值。如果mutex的初始值大于0说明已经有其他线程持有该锁那么当前线程就返回0并挂起等待否则它会继续执行并获得锁。 unlock这部分是释放互斥锁的过程。同样地将寄存器al设置为1movb $1, mutex然后唤醒等待Mutex的线程并返回0。 对于swap或exchange这样的指令它们通常用于在内存和寄存器之间交换数据而且在一些体系结构中这类指令是可以保证原子性的即在多线程环境下不会有其他线程能在指令执行过程中中断并改变被交换的数据。例如在x86架构中可以用xchg指令实现寄存器和内存位置的数据原子交换。 在执行流视角来看CPU寄存器就是当前执行流状态的重要组成部分它们反映了当前执行点的关键信息如算术逻辑运算的中间结果、函数调用的返回地址、栈指针等。每条执行流有自己的寄存器上下文保证了各自执行的独立性和连续性。 在汇编层面swap或exchange指令常用于实现内存与CPU内部寄存器间的数据原子性交换。一旦这条汇编指令仅由单条语句构成我们通常认为该指令在执行期间是不可分割的即原子操作不会受到其他执行流的干扰。
交换的本质:共享-私有 从执行流的角度审视CPU内部的寄存器它们本质上构成了当前执行流的运行环境或上下文。尽管所有执行流共享CPU内部的寄存器空间资源但每个独立的执行流如线程或进程对其使用的寄存器内容享有专属权也就是说寄存器的具体值是各个执行流私有的状态信息。 换言之在并发或多线程环境中尽管物理上的寄存器空间是公共资源但CPU通过巧妙的上下文切换机制确保每个执行流在运行时都有自己独特且独立的寄存器内容。因此可以说寄存器的内容反映了执行流在某一特定时间点的执行状态和局部数据这种状态是与其他执行流隔离的。
8、互斥锁操作场景 在下方的图示中可以看到一个CPU和内存之间的交互过程。当CPU尝试获取锁时它需要检查内存中的mutex变量的状态。如果状态为0则可以成功获取锁反之如果状态非零则表示另一个线程已经持有了锁此时CPU需要等待。 这个表格描述了一个典型的多线程环境下的互斥锁操作场景。在这个场景中有两个线程A和B试图同时访问同一段共享资源例如一段内存区域或一个变量。为了防止多个线程同时修改这段共享资源导致的数据不一致或其他问题我们需要一种机制来保证每次只有一个线程能够访问这段资源。
互斥锁就是这样的一个机制。它提供了一种方式来控制对共享资源的访问使得在同一时刻只有一个线程能够拥有锁并访问资源。当一个线程想要访问共享资源时它必须先尝试获取锁。如果锁已经被其他线程持有那么这个线程就会被阻塞并进入等待状态直到锁被释放为止。 线程A尝试获取锁 A线程首先尝试获取锁由于此时锁还没有被任何线程持有所以A线程能够成功获取锁。在获取锁之后A线程就可以安全地访问共享资源而不用担心与其他线程发生冲突。 线程B尝试获取锁 B线程也尝试获取锁但由于锁已经被A线程持有所以B线程无法立即获取锁。此时B线程会被阻塞并进入等待状态直到A线程完成对共享资源的操作并释放锁。 线程A释放锁 当A线程完成对共享资源的操作后它会释放锁这使得其他正在等待锁的线程有机会重新尝试获取锁。 线程B获取锁 在A线程释放锁之后B线程可以从等待状态恢复过来并尝试再次获取锁。这次由于没有其他线程持有锁所以B线程能够成功获取锁并开始访问共享资源。
通过这种方式我们可以在多线程环境中实现对共享资源的安全访问避免了数据竞争和其他并发问题。每个线程都必须遵循相同的规则来获取和释放锁以确保所有线程都能正确地协调它们对共享资源的访问。
三、可重入与线程安全
1、概念
线程安全Thread Safety是指在多线程环境下即使多个线程同时访问同一段代码或数据也能确保程序行为的正确性和一致性不会出现因竞态条件而导致的数据不一致、死锁或其他不确定行为。
2、常见的线程不安全的情况
线程不安全的情况常常出现在对全局变量、静态变量进行无同步机制保护的修改操作时。例如
没有采取互斥措施直接操作共享变量的函数。函数内部的状态会在调用过程中发生改变并且这种改变会影响到后续调用的结果。函数返回的是指向静态存储区变量的指针这意味着多个线程可能同时读写此变量造成数据竞争。调用了其他非线程安全函数的函数除非整个调用链都进行了适当的同步控制。
3、常见的线程安全的情况
线程安全的情形通常包括
所有线程仅对全局变量或静态变量拥有读取权限而不进行写入操作这样的设计使得多个线程可以并发读取而不会相互影响。设计良好的类或接口其内部方法保证了在多线程环境下的原子性即从开始到结束的过程不会被其他线程中断从而避免了执行结果的不确定性。多个线程在执行特定接口或函数时即使发生上下文切换也不会因此导致执行结果的歧义或错误这是因为这类函数内部实现了必要的同步控制如使用锁或其他并发控制机制。
4、重入概念
重入Reentrancy则是特指一个函数能够被多个执行流如线程同时调用而不会引起任何错误或意外行为的能力。
一个可重入函数在其执行过程中不会依赖于任何全局或静态状态并且不会阻止其他调用者获取相同的资源。即使在前一次调用尚未完成时又有新的调用介入只要各个调用之间使用的资源是独立的如函数只使用局部变量或对共享资源的访问采用的是线程安全方式该函数就能保证每次调用都能获得一致且正确的结果。
5、常见不可重入的情况 调用了malloc和free函数由于这两个函数依赖于全局链表来维护和管理堆内存因此在多线程环境下若同时有两个线程试图分配或释放内存可能会引发竞态条件导致程序行为未定义。 调用了标准I/O库函数许多标准I/O库的实现中其内部会以非线程安全的方式使用全局数据结构。当多个线程同时访问这些全局数据时可能导致数据混乱或程序崩溃。
6、常见可重入的情况 函数不依赖全局变量或静态变量在多线程环境中如果一个函数不直接读写任何全局或静态存储区的数据那么该函数就可以被视为可重入的。 不调用非可重入函数函数在其执行过程中不调用任何具有上述不可重入特征如使用全局数据结构、调用malloc/free等的函数才能确保自身的可重入性。 不返回静态或全局数据函数的返回值及输出参数均不涉及静态或全局存储区的数据所有结果由函数的调用者提供的输入参数计算得出。 通过创建全局数据的本地副本保护全局数据若函数确实需要访问全局数据可以通过在函数内部复制全局数据到局部作用域内避免对原始全局数据的直接操作从而实现可重入。
总之一个函数要具备可重入性关键在于它不能对共享资源进行竞争性访问必须独立于其运行环境并且在任何时候都能被多个并发执行的实体安全地调用。
7、可重入与线程安全
可重入性和线程安全是多线程编程中相关的两个重要概念它们之间的联系与区别体现在以下几个方面
联系 可重入函数与线程安全的正相关性 如果一个函数是可重入的意味着它在执行过程中不会依赖任何外部上下文如全局变量或静态变量并且可以安全地在同一进程中被多个线程同时调用而不互相干扰从而保证了数据一致性所以可重入函数必定是线程安全的。 共同关注点 无论是可重入函数还是线程安全函数它们的核心都是确保在并发环境下的正确执行即避免因资源共享带来的竞争条件和数据破坏问题。
区别 可重入函数的特性更加严格 可重入函数不仅在多线程环境下安全而且能够在递归调用或中断/恢复执行时仍保持正确性无需担心内部状态冲突。相比之下线程安全函数只要求在多线程环境下不会因为共享资源的竞争而导致错误结果。 线程安全函数范围较宽 线程安全函数可能依赖于同步机制如互斥锁来保护共享资源防止数据竞争但并非所有这样的函数都具有可重入性。例如一个函数在获取锁后进入临界区如果在其执行过程中再次尝试获取同一把锁即重入且之前获取的锁尚未释放就可能导致死锁这样的函数就不具备可重入性。
简而言之可重入函数是线程安全函数的一个子集它不仅在线程间切换时能保证安全性还能在函数自身递归调用时维持安全无误的状态。而线程安全函数则可能通过锁定或其他同步手段来避免并发问题但如果不满足可重入的要求在特定条件下也可能导致问题如死锁。