目录
一、锁的概念
一些需要了解的概念
什么是锁?为什么需要锁?什么时候使用锁?怎么定义锁?
二、锁的接口
1.初始化锁
2.加锁
3.申请锁
4.解锁
5.销毁锁
三、死锁
什么是死锁
什么时候会形成死锁
怎么避免死锁
四、实践(写代码):黄牛抢票
一、锁的概念
一些需要了解的概念
临界资源:任一时刻只允许一个线程访问的共享资源临界区:访问临界资源的代码原子性:不会被任何调度机制打断的操作,该操作只有两态:要么完成,要么未完成互斥:任何时刻,互斥可以保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用,而锁就是实现互斥的。同步:同步是一种机制,用于协调不同进程、线程或设备之间的操作,确保它们按照预期的顺序和方式进行。同步的目的是保持数据的一致性和系统的稳定性。
什么是锁?为什么需要锁?什么时候使用锁?怎么定义锁?
什么是锁?
锁是一种同步机制,用于控制多个线程对共享资源的访问。通过锁,可以确保一次只有一个线程能够访问特定的代码段或数据,从而防止数据竞争和不一致。锁的主要目的是确保数据的一致性和线程安全性。
为什么需要锁?
需要锁的主要原因在于确保多线程或多用户环境中共享资源访问的原子性和数据一致性。在多线程应用中,若多个线程同时访问并修改同一资源,可能导致数据冲突、不一致甚至损坏。
故事说明:把线程比作一个学生,小明,锁是自习室的门口上的锁头,当小明要进自习室时,他就从墙上拿钥匙(钥匙只有一把)解锁进入教室,而此时的自习室就是临界区,小明在自习室的书本,本子,笔就是临界资源,当小明突然要上厕所离开自习室时,因为自习室里有小明的书本呀,笔呀,等等东西,所以小明离开时就把门锁上了。小明自习了一天了,到晚上了吃饭了,小明不想自习了,小明带上他的东西离开,然后 把门锁上,把钥匙挂回墙上,此时其他同学就可以使用自习室了。在这例子中锁的作用就是只允许有钥匙的学生进入自习室,不允许其他没有钥匙的同学进入(其他线程),换言之锁的作用就是实现在一个临界区中的任一时刻只允许一个线程进入,访问。在这个故事中如果自习室门上没有锁,当小明要上厕所时,别的同学可以可以进入自习室破坏,拿走小明的东西呢?答案是有可能的,所以为了保证自习室里的小明的资源的安全,所以需要锁,把门上锁。
什么时候使用锁?
使用锁主要在多线程或多用户环境中,当多个线程或用户需要并发访问和修改共享资源时。锁能确保同一时间只有一个线程或用户访问资源,避免数据冲突和不一致。在需要保证数据完整性、原子性和安全性的场景下,应使用锁来同步和控制对共享资源的访问。
在Linux中锁长什么样呢?我们怎么定义锁呢?
pthread_mutex_t是一个用于线程同步的互斥锁类型。例如:pthread_mutex_t mutex:定义互斥锁变量mutex,mutex就是一个锁(锁变量)。
二、锁的接口
1.初始化锁
功能:用于初始化锁变量 原型
#include
锁为局部变量时使用如下函数初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
锁为全局变量时使用如下方法初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
参数
pthread_mutex_t *restrict mutex:指向要初始化的互斥锁变量的指针。这个指针指向的互斥锁变量在调用 pthread_mutex_init 之前应该是未初始化的。
const pthread_mutexattr_t *restrict attr:指向互斥锁属性的指针。这个参数是可选的,通常可以传递 NULL (nullptr)来使用默认的互斥锁属性。如果你需要设置特定的属性(例如互斥锁的类型),你需要先使用 pthread_mutexattr_init 初始化一个 pthread_mutexattr_t 变量,然后设置所需的属性,最后将其传递给 pthread_mutex_init。
返回值:成功返回0;失败返回错误码
使用例子:
#include
#include
using namespace std;
//全局锁初始化方式
pthread_mutex_t global_mutex = PTHREAD_MUTEX_INITIALIZER;//定义并初始化锁
int main()
{
//局部初始化方式
pthread_mutex_t local_mutex;//定义锁
pthread_mutex_init(&local_mutex, nullptr);//初始化锁
//加锁
//...
//解锁
//销毁锁
return 0;
}
2.加锁
功能:获取(锁定)互斥锁,如果锁当前未被其他线程占用(即锁是“空闲”的),那么调用此函数的线程将成功获取锁,并可以继续执行其临界区代码。如果锁已被其他线程占用,则调用线程将被阻塞,直到锁被释放(即被当前持有锁的线程调用 pthread_mutex_unlock) 原型
#include
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数
pthread_mutex_t *mutex:是一个指向互斥锁变量的指针。
返回值
pthread_mutex_lock 函数的返回值用于指示加锁操作是否成功。
如果成功获取锁,函数返回 0。如果在尝试获取锁时发生错误(例如,由于无效的互斥锁指针或系统资源不足),函数将返回一个错误码(非零值)。具体的错误码可以根据不同的系统和库实现而有所不同,但通常会遵循 POSIX 线程标准中定义的错误码。
注意事项
使用 pthread_mutex_lock 时,必须确保在不再需要锁时调用 pthread_mutex_unlock 来释放锁,以避免死锁。如果在调用 pthread_mutex_lock 后线程被中断或取消,锁可能仍然处于锁定状态,需要特别小心处理。如果互斥锁的类型是递归锁(recursive mutex),则同一个线程可以多次获取同一个锁,但每次获取锁后都必须对应地释放锁。
3.申请锁
功能:尝试获取一个互斥锁(mutex),而不会阻塞调用线程。如果互斥锁已经被其他线程持有,则 pthread_mutex_trylock 不会使调用线程进入睡眠状态等待锁释放,而是立即返回表示失败的错误码。 原型
#include
int pthread_mutex_trylock(pthread_mutex_t *mutex); 参数
pthread_mutex_t *mutex:是一个指向互斥锁变量的指针。 返回值
如果成功获取锁,函数返回 0。如果锁已被其他线程占用,或者发生其他错误(如传递了无效的互斥锁指针),函数将返回一个错误码。常见的错误码包括 EBUSY(表示锁当前被其他线程占用)和 EINVAL(表示传递给函数的互斥锁是无效的)。
4.解锁
功能:用于释放互斥锁(mutex),在多线程编程中用于确保线程同步的正确性。当一个线程完成对共享资源的访问后,它应该调用 pthread_mutex_unlock 来释放锁,以便其他线程能够获取该锁并访问相同的资源。 原型
#include
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数
pthread_mutex_t *mutex:是一个指向互斥锁变量的指针。 返回值
如果成功释放锁,函数返回 0。如果在尝试解锁时发生错误(例如,传递给函数的互斥锁是无效的,或者当前线程没有持有该锁),函数将返回一个错误码。常见的错误码包括 EINVAL(表示传递给函数的互斥锁是无效的)和 EPERM(表示当前线程没有持有该锁)。
5.销毁锁
功能:用于销毁(释放)互斥锁(mutex)的函数。在多线程编程中,当不再需要某个互斥锁时,应该调用 pthread_mutex_destroy 来销毁它,以释放相关资源。 原型
#include
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数
pthread_mutex_t *mutex:是一个指向互斥锁变量的指针。 返回值
如果成功销毁锁,函数返回 0。如果在尝试销毁锁时发生错误(例如,传递给函数的互斥锁是无效的,或者锁仍被持有),函数将返回一个错误码。常见的错误码包括 EBUSY(表示锁当前被其他线程占用)和 EINVAL(表示传递给函数的互斥锁是无效的)。
三、死锁
什么是死锁
死锁是一种因为线程/进程之间进行资源竞争,造成相互等待资源,“永久”阻塞的一种现象。
什么时候会形成死锁
形成死锁要同时满足4个条件:
互斥条件:一个资源只能被一个线程拥有。请求与保持条件:线程因为申请资源而造成阻塞时,其所拥有的资源不会释放。不剥夺条件:线程所拥有的资源在其未使用完之前,不能被强行剥夺。循环等待条件:线程与线程之间形成一种首尾相接相互等待对方资源的情况。
怎么避免死锁
只要不同时满四个条件就行:
破坏互斥条件:互斥是不能避免的,因为锁就是想要互斥性呀,锁没有了互斥性就没了意义。破坏请求与保持条件:只要我们一次性申请了所需要的资源,就不会造成阻塞,导致占用资源不释放的情况。也可以在申请资源阻塞时主动释放自己所拥有的资源。这两种做法都能破坏请求与保持条件。破坏不剥夺条件:线程在申请资源而阻塞时,主动释放自己的资源。破坏循环等待条件:定义资源访问的顺序,并要求所有线程按照固定的顺序请求资源。
四、实践(写代码):黄牛抢票
说明:用多线程模拟黄牛,常数模拟票
Makefile
test:test.cc
g++ -o $@ $^ -std=c++11 -lpthread
PHONY:clean
clean:
rm -f test;
test.cc
#include
#include
#include
#include
#include
using namespace std;
int ticket = 1000; // 一千张票
int threadnum = 5; // 黄牛数
// 定义全局锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *threadtask(void *args)
{
while (ticket > 0)
{
string name = static_cast
// 加锁
pthread_mutex_lock(&mutex);
cout << "我是: " << name << "我抢到了" << ticket << "号票" << endl;
ticket--; // 票数--
sleep(1);
// 解锁
pthread_mutex_unlock(&mutex);
}
return nullptr;
}
int main()
{
// 五个黄牛
vector
for (int i = 0; i < threadnum; i++)
{
pthread_t tid;
char threadname[64];
sprintf(threadname, "thraed-%d", i + 1);
pthread_create(&tid, nullptr, threadtask, threadname);
threads.push_back(tid);
}
void *ret;
for (int i = 0; i < threads.size(); i++)
{
pthread_join(threads[i], &ret);//等待线程,回收资源
}
// 销毁锁
pthread_mutex_destroy(&mutex);
return 0;
}
结果
全是5号线程抢到票,原因是Linux内核中的线程调度器根据线程的优先级、状态和其他因素来决定哪个线程应该被调度执行。如果某个线程的优先级高于其他线程,或者其状态更适合执行(例如,它已准备好运行并且没有受到阻塞),那么它就更有可能被调度器选中。你可以在你的linux中试试或许会有不同的结果。
完结!!!