欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 创投人物 > Linux 线程互斥

Linux 线程互斥

2024/10/23 5:22:26 来源:https://blog.csdn.net/2301_79881188/article/details/142970388  浏览:    关键词:Linux 线程互斥

1.相关背景概念

临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
#include <iostream>
#include <pthread.h>
#include <string>
#include <vector>
#include <unistd.h>
using namespace std;
struct customer
{pthread_t tid;string name;
};
int g_tickets = 1000;
void* GetTicket(void* args)
{customer* c = (customer*)args;while(true){if(g_tickets > 0){usleep(1000);cout << c->name << " is getting a ticket " << g_tickets << endl;g_tickets--;}elsebreak;}return nullptr;
}int main()
{vector<customer> custs(5);for(int i = 0; i < 5; i++){custs[i].name= "customer-" + to_string(i + 1);pthread_create(&custs[i].tid, nullptr, GetTicket, &custs[i]);}for(int i = 0; i < 5; i++){pthread_join(custs[i].tid, nullptr);}return 0;
}

例如上面代码中,我们模拟一个抢票系统,票 g_tickets 为全局变量,被所有线程共享,即为临界资源,而在所有线程进行的 GetTicket 函数中,都进行抢票,对 g_tickets 进行 -- 操作。下面访问临界资源 g_tickets 的代码区域就是临界区。

if(g_tickets > 0)
{usleep(1000);cout << c->name << " is getting a ticket " << g_tickets << endl;g_tickets--;
}

随后我们执行代码

发现其竟然抢到了0,-1,-2,-3这些不存在的票,所以我们可以得知访问临界资源时是会出现问题的,于是我们可以引出以下概念:

  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成,即一条汇编语句。

上面抢票系统为什么会出现这样的情况呢?

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程
  • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
  • --ticket 操作本身就不是一个原子操作,即它是多条汇编语句组成

例如,我们简化模型:有customer - 1 和 customer - 2 去抢一张票

该线程先判断,发现 g_ticket > 0,于是进入第一个if语句,进行g_ticket--。但是g_ticket--本质上是多条汇编语句,比如下面这样:

MOV  eax, [0x1000]  ; 读取 g_ticket 的值
DEC  eax            ; 减 1
MOV  [0x1000], eax  ; 将值写回 g_ticket

第一行MOVE把内存中 g_ticket 的数据拷贝到CPU中的eax寄存器,第二行是将 eax中的值 - 1,第三行是将eax中的值拷贝回内存中的 g_ticket。

假设我们现在执行到 g_ticket-- 汇编的第二条指令,突然线程customer-1的时间片结束了,要结束调度当前线程,去调度customer-2了。此时内存中的g_ticket还没有被修改了,于是CPU保存当前线程customer-1的上下文,切换调度customer-2

此时线程customer-2也通过if (g_ticket > 0)检测发现还有一张票,于是custome-2也去抢这张票,执行g_ticket--。

这下出问题了,customer-1已经抢了这张票,但是还没来得及把g_ticket变成0,此时customer-2又进来抢了一次票。最后就会出现一张票被两个人抢到的问题。

要解决以上问题,需要做到三点:
  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

这就需要用锁来实现线程互斥,Linux上提供的这把锁叫互斥量。

2.互斥量mutex

初始化互斥量

初始化互斥量有两种方法:
方法 1 ,静态分配 :
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
我们创建了一个名为 mutex的变量,类型是  pthread_mutex_t ,全局的互斥锁用宏PTHREAD_MUTEX_INITIALIZER 进行初始化, 并且使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁。
方法 2 ,动态分配 :
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
参数:
mutex:指向互斥量变量的指针,该变量将被初始化。
attr:指向互斥量属性的指针,如果为 NULL,则使用默认属性。

返回值:如果函数成功,返回 0;如果失败,返回相应的错误码。

方法3,C++STL:

在C++标准模板库(STL)中,可以使用std::mutex作为互斥量:

#include <mutex>int main() {std::mutex my_mutex;// 使用互斥量...// 不需要显式销毁,因为它是对象,在其作用域结束时会自动析构return 0;
}

销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
  • mutex: 这是指向已经初始化过的互斥量的指针。

返回值:如果函数成功,则返回 0;若发生错误,则返回相应的错误码。

销毁互斥量需要注意:
  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • pthread_mutex_lock:用于申请锁,如果申请失败,就阻塞等待,直到申请到锁;如果申请成功,就执行临界区代码。
  • pthread_mutex_trylock:用于非阻塞等待申请锁,如果申请失败,立即返回错误码EBUSY,表示互斥量当前不可用;如果申请成功,就执行临界区代码。
int pthread_mutex_unlock(pthread_mutex_t *mutex);

pthread_mutex_unlock:释放(解锁)互斥量mutex,使其他线程可以获取该互斥量,调用这个函数的线程应该是当前持有互斥量的线程。

接下来我们修改一下抢票系统的代码,给它加锁,保证抢票g_ticket--的原子性:

int g_tickets = 1000;
//定义一个全局变量的锁,保证所有线程都用同一个锁
pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
void *GetTicket(void *args)
{customer *c = (customer *)args;while (true){pthread_mutex_lock(&g_mutex); //加锁if (g_tickets > 0){usleep(1000);cout << c->name << " is getting a ticket " << g_tickets << endl;g_tickets--;pthread_mutex_unlock(&g_mutex); //解锁}else{pthread_mutex_unlock(&g_mutex); //解锁break;}}return nullptr;
}

当第一个线程被调度进行抢票,先申请锁g_mutex,然后再去if中访问g_ticket。假如在访问临界资源的过程中,CPU调度了第二个线程,第二个线程也想访问g_ticket,于是也申请锁g_mutex,但是由于锁已经被第一个线程申请走了,此时第二个线程pthread_mutex_lock就会失败,然后阻塞等待。等到第一个线程再次被调度,访问完临界区后,对g_mutex解锁,此时锁又可以被申请了。于是线程二申请到锁,再去访问g_ticket。加锁可以保证任何时候都只有一个线程访问临界区,这就保证了临界区的原子性,从而维护线程的安全!

3. 互斥量的实现原理

互斥量的汇编伪代码如下:

加锁lock

moveb $0, %al
xchgb %al, mutex
if (al寄存器的内容 > 0){return 0;
}else挂起等待;
goto lock

假设有这样的情形,有两个线程thread-1thread-2,它们共用内存中的锁mutex。在CPU中有一个寄存器%al,用于存储锁的值。

假设thread-1进行调度执行pthread_mutex_lock

首先执行指令moveb $0, %al,把寄存器%al内部的值变成0

随后执行xchgb %al, mutex,让内存中的mutex与寄存器%al的值进行交换:

此时寄存器%al的值变成1mutex的值变成0。随后执行:

if (al寄存器的内容 > 0){return 0;
}else挂起等待;

判断当前%al内部的值是否大于0,如果大于0那么说明争夺到了锁,此时函数pthread_mutex_lock返回0,表示加锁成功,否则执行else进行挂起等待。

现在假设thread-1执行到第一条汇编语句后,%al的值还是0,但是CPU切换去调度thread-2了:

现在thread-1保存自己的硬件上下文,包括%al = 0在内,随后therad-2进入:

现在thread-2执行了两行汇编语句,成功把内存中的mutex与自己的%al交换,申请到了锁,此时thread-1再次调度,thread-2拷贝走自己的硬件上下文:

恢复硬件上下文后,thread-1的%al等于0,执行第二条语句后,%al和mutex依然是0,这表明锁已经别的线程拿走了,此时在执行if内部的内容,thread-1挂起等待。

其实锁的本质,就是保证mutex变量中以及所有访问锁的线程的%al寄存器中,只会有一个非零值。只有拿到非零值的线程才有资格去访问临界资源。其它线程如果要再次申请锁,由于自己的%al和mutex都是0,就算交换后还是0,也申请不到锁。并不是谁先调用ptherad_mutex_lock,谁就先抢到锁,而是谁先执行该函数内部的xchgb %al, mutex语句,把非零值放到自己的%al中,谁才抢到锁。

再简单看看解锁unlock

moveb $1, mutex
唤醒等待mutex的线程;
return 0;

moveb $1, mutex就是把%al中的1还给mutex,然后唤醒所有等待该锁的线程,让它们再次争夺这把锁。最后return 0,也就是pthread_mutex_unlock函数返回0

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com