线程互斥
线程互斥及相关概念
线程互斥(Mutual Exclusion)是指在多线程环境下,同一时刻只能有一个线程访问共享资源,以避免对该资源的不正确访问,造成数据不一致等问题。
例如,如果有多个线程都要同时对同一个全局变量进行修改,那么就需要使用线程互斥来保证对该变量的访问是互斥的,也就是说,在任意时刻只能有一个线程对该变量进行修改。
临界资源(Critical Resource)是指在多线程环境下需要被多个线程共享访问的资源,对该资源的访问需要进行同步(如使用互斥进行互斥)以避免出现不正确的访问。
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。是对临界资源保护的一种手段。
原子性:不会被任何调度机制影响的操作,该操作只有两态,要么完成,要么未完成。
多线程抢票
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
using namespace std;
// 共享资源,多线程同时访问(未来的临界资源)
int tickets = 10000;
void* getTickets(void* args)
{
string* ps = (string*)args;
while(true)
{
if(tickets > 0) // 未来的临界区
{
usleep(1000);
printf("%s : %d\n", ps->c_str(), tickets); // 未来的临界区
// cout << *ps << " get ticket " << tickets << endl;
tickets--; // 未来的临界区
}
else
{
break;
}
}
delete ps;
return nullptr;
}
// 多线程抢票程序
int main()
{
pthread_t tid[3];
for(int i = 0; i < 3; ++i)
{
string* ps = new string("thread");
ps->operator+=(to_string(i+1));
pthread_create(tid + i, nullptr, getTickets, (void*)ps);
}
for(int i = 0; i < 3; ++i)
{
pthread_join(tid[i], nullptr);
// printf("主线程等待回收新线程%d成功\n", i + 1);
// cout << "主线程等待回收新线程" << i+1 << "成功" << endl;
}
return 0;
}

分析
上方程序为多线程抢票程序,全局数据tickets为共享资源(未来的临界资源,此时还没有进行互斥保护),多个线程对getTickets函数进行了重入,getTickets方法中对全局tickets变量访问和修改的代码都是未来的临界区,如if判断,printf打印及tickets--代码都是未来的临界区代码。
因为多线程并发执行,访问共享资源,因时序及线程切换等原因造成的数据不一致等问题。我们则需要对访问共享资源的代码进行加锁保护,进行线程互斥。
为什么多个线程并发访问共享资源时,因为线程切换就会造成数据不一致呢?下面举两点解释说明:
if(tickets > 0):tickets > 0判断的本质也是计算,则该代码执行时需要CPU进行逻辑运算,tickets全局数据存储在内存中,则需要将tickets数据load到CPU寄存器中,本质就是将数据load到当前线程的上下文中。执行if判断的后面代码块时,因为多线程并发执行,此时的执行线程随时可能被切换(此时寄存器中的线程上下文数据也会被线程保存起来),则就可能造成多个执行线程同时进入if判断内部。若此时tickets为1,则就会因为线程切换造成tickets减到0甚至-1。(上方程序中的usleep更加提高了这种情况发生的可能性)
tickets--:这条C语句在不进行优化的情况下翻译为汇编时,最少会变为三条:1. load到CPU寄存器中 2. 对寄存器内容进行-- 3. 将寄存器数据load回内存中。因此这个--操作并不是原子的,执行到哪一步时都有可能进行线程切换。则存在以下场景:两个线程,线程1和线程2接下来要进行tickets--操作,此时 tickets为10,线程1执行完load到寄存器之后,被切换了,此时线程1会保存它的上下文数据,比如此时保存tickets的寄存器值,其他临时数据,程序计数器eip,栈顶栈底指针的值等。线程2执行tickets--的过程中没有被切换,此时内存中的tickets的值成功被--到了9。再切换为线程1,线程1执行第二步和第三步。此时内存中的tickets又被重复写入到了9。
造成上方多线程抢票程序问题的主要原因其实是第一点,第二点也会存在,只是概率相对更低。实际的执行的情况会比上方所述复杂的多,总之这样的多线程并发访问共享资源的程序是有问题的。
因此我们需要进行线程互斥,常见的保护措施就是互斥锁。使得加锁和解锁之间的代码区域同一时刻只能有一个线程执行,这样的代码区域称为临界区,tickets数据称为临界资源。
pthread线程库mutex互斥锁
// 初始化互斥锁
// 1. 静态分配,适用于全局或静态的互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 2. 动态分配,适用于局部的互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr); // 参数二设为nullptr即可
// 销毁互斥锁
// 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥锁不需要销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 加锁,解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// int pthread_mutex_trylock(pthread_mutex_t *mutex);
调用pthread_mutex_lock时,可能出现以下情况。
互斥锁处于未锁状态,该函数会将互斥锁锁定,同时返回成功(0)
调用时,互斥锁已经被其他线程锁定,或者有其他线程同时申请互斥锁且此线程没有竞争到互斥锁。则pthread_mutex_lock会将调用线程进行阻塞等待,等待