首页 > 系统相关 >Linux线程互斥

Linux线程互斥

时间:2022-12-31 21:34:11浏览次数:39  
标签:cout void Linux nullptr 互斥 线程 pthread mutex


写在前面

这个博客的内容很少,但是很关键,这是我们线程安全相关的内容,里面会涉及到线程互斥和加锁的相关观念,总体而言还是很难的.

线程互斥

先看一下下面的代码,这里是一切的开端.我们模拟一下多线程抢票过程,让多个线程去抢这张表.

int tickets = 100;

void *getTickets(void *args)
{
const char *name = static_cast<const char *>(args);

while (true)
{

if (tickets > 0)
{

cout << name << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;
sleep(1);
}
else
{
// 票抢到几张,就算没有了呢?0
cout << name << "] 已经放弃抢票了,因为没有了..." << endl;
break;
}
}

return nullptr;
}

int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;
pthread_t tid4;
pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");
pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");
pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");
pthread_create(&tid4, nullptr, getTickets, (void *)"thread 4");

int n = pthread_join(tid1, nullptr);
cout << n << ":" << strerror(n) << endl;
n = pthread_join(tid2, nullptr);
cout << n << ":" << strerror(n) << endl;
n = pthread_join(tid3, nullptr);
cout << n << ":" << strerror(n) << endl;
n = pthread_join(tid4, nullptr);
cout << n << ":" << strerror(n) << endl;

return 0;
}

现在我们要谈三个概念,都是我们之前谈过的,这里要给他汇总起来.

临界资源

我们多个线程去去抢票的时候,是不是多个线程先看到票,那么这个票不就是一个临界资源吗!访问临界资源的代码 我们称之为临界区.

那么请问那些是临界区?这是if判断和–两个临界资源.–我们可以理解,那么if判断那里不就是票数和零判断了一下吗?为何他也是临界区.请问条件判断是不是逻辑运算?是的,逻辑运算在底层会被变化成算数运算,最终变成票数减零和某一个值进行比较,实际上在底层所有的运算大多会被转化为移位和相加的操作.

原子性

这里我们以–为例子,请问–是原子性的吗?不是的,这一条语句在底层最起码会被翻译成三条语句,也就是加载到CPU,修改,放回内存.它的操作不是原子的,就代表会出现下面的事情.

Linux线程互斥_加锁

Linux线程互斥_d3_02

线程切换

我们前面的博客已经谈到过线程切换了,线程在调用系统接口和时间片跑完了进行进程切换,也就是进程切换会发生在任意时刻.我们也知道进程切换时,OS会把进程切换的上下文保存在寄存器中,切回时又会恢复上下文.寄存器是所有线程共享的,但是寄存器中的数据确是线程独有的,由于–不是原子的,这就有下面情况的概率.

Linux线程互斥_linux_03

互斥锁

我们上谜案理论已经谈完了,是会存在这样的可能性,要知道我们计算机运行的时间是很长的,它的速度很快i,哪怕有一点毛病也会被无限的放大,但是上面我们抢票的时候是没有看到错误的,这里我们需要先看到现象.

int tickets = 1000;

void *getTickets(void *args)
{
const char *name = static_cast<const char *>(args);

while (true)
{

if (tickets > 0)
{
usleep(100000);

cout << name << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;
}
else
{
// 票抢到几张,就算没有了呢?0
cout << name << "] 已经放弃抢票了,因为没有了..." << endl;
break;
}
}

return nullptr;
}

int main()
{
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;
pthread_t tid4;
pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");
pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");
pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");
pthread_create(&tid4, nullptr, getTickets, (void *)"thread 4");

int n = pthread_join(tid1, nullptr);
cout << n << ":" << strerror(n) << endl;
n = pthread_join(tid2, nullptr);
cout << n << ":" << strerror(n) << endl;
n = pthread_join(tid3, nullptr);
cout << n << ":" << strerror(n) << endl;
n = pthread_join(tid4, nullptr);
cout << n << ":" << strerror(n) << endl;

return 0;
}

既然我们已经看到了这个现象,那么我们应该如何解决呢?这个时候就要谈到锁了,我们临界区代码经过加锁后,有竞争临界资源变化成先竞争这个锁,此时一对线程来争抢一把锁,拿到锁的线程才能访问临界资源.这就是加锁的原理.我们谈互斥锁.互斥锁是我们常用的一吧锁,互斥锁说没有锁的线程会被阻塞住,等待被人把锁放回来.Linux为我们提供了接口.

初识化锁,其中如果你的锁是全局的或者静态的,你也可以通过宏来初始化,用宏初始化就不用销毁了

int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

lock – 阻塞式加锁,unlock解锁.

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

多的不说,先来用起来.

pthread_mutex_t mutex;

int tickets = 1000;

void *getTickets(void *args)
{
const char *name = static_cast<const char *>(args);

while (true)
{
pthread_mutex_lock(&mutex);
if (tickets > 0)
{
usleep(10000);
cout << name << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;
pthread_mutex_unlock(&mutex);
}
else
{
// 票抢到几张,就算没有了呢?0
cout << name << "] 已经放弃抢票了,因为没有了..." << endl;
pthread_mutex_unlock(&mutex);

break;
}
}

return nullptr;
}

int main()
{
pthread_mutex_init(&mutex, nullptr);
pthread_t tid1;
pthread_t tid2;
pthread_t tid3;
pthread_t tid4;
pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");
pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");
pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");
pthread_create(&tid4, nullptr, getTickets, (void *)"thread 4");

int n = pthread_join(tid1, nullptr);
cout << n << ":" << strerror(n) << endl;
n = pthread_join(tid2, nullptr);
cout << n << ":" << strerror(n) << endl;
n = pthread_join(tid3, nullptr);
cout << n << ":" << strerror(n) << endl;
n = pthread_join(tid4, nullptr);
cout << n << ":" << strerror(n) << endl;
pthread_mutex_destroy(&mutex);
return 0;
}

先来解决一个问题,我们如何加锁,或者说加在临界区哪一个位置?我们要求是加的越细越好.

如果我们要是按照下面的方式加锁,那么只能有一个线程来抢票,他会抢走所有的票.

void *getTickets(void *args)
{
const char *name = static_cast<const char *>(args);
pthread_mutex_lock(&mutex);
while (true)
{
if (tickets > 0)
{
usleep(10000);
cout << name << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;
}
else
{
cout << name << "] 已经放弃抢票了,因为没有了..." << endl;
pthread_mutex_unlock(&mutex);
break;
}
}

下面的方式会造成死锁的情况,因为抢到锁的线程在退出的时候没有把锁放回来,其他的线程都在阻塞的等待锁,但是主线程又在join,这就构成一个死结.

void *getTickets(void *args)
{
const char *name = static_cast<const char *>(args);
pthread_mutex_lock(&mutex);
while (true)
{
if (tickets > 0)
{
usleep(10000);
cout << name << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;
}
else
{
// 票抢到几张,就算没有了呢?0
cout << name << "] 已经放弃抢票了,因为没有了..." << endl;
break;
}
}

return nullptr;
}

现在再说一下锁的初始化方式,我们这里直接来个综合的,静态用宏来初始化.那么请问我们如何把这个锁给线程,这个很简单,我们创建线程的时候第四个参数不是指针吗?上面我们可以说你是线程的名字,此时我们就把他看出指针.

void *getTickets(void *args)
{
pthread_mutex_t *mutex_p = (pthread_mutex_t *)args;
while (true)
{
pthread_mutex_lock(mutex_p);
if (tickets > 0)
{
usleep(100000);
cout << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;

pthread_mutex_unlock(mutex_p);
}
else
{
// 票抢到几张,就算没有了呢?0
cout << " 已经放弃抢票了,因为没有了..." << endl;
pthread_mutex_unlock(mutex_p);

break;
}
}

return nullptr;
}

int main()
{
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_t tid1;

pthread_create(&tid1, nullptr, getTickets, (void *)&mutex);

int n = pthread_join(tid1, nullptr);
cout << n << ":" << strerror(n) << endl;

return 0;
}

Linux线程互斥_临界资源_04

这个指针就表示是如果你想传入其他的东西,我们也是可以使用结构体来的,这里顺带说一下寻常的初始化方式.

int tickets = 1000;
struct mythread
{
char name[100];
pthread_mutex_t *mutexp;
};

void *getTickets(void *args)
{
struct mythread *p = (struct mythread *)args;
while (true)
{
pthread_mutex_lock(p->mutexp);
if (tickets > 0)
{
usleep(100000);
cout << p->name << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;

pthread_mutex_unlock(p->mutexp);
}
else
{
// 票抢到几张,就算没有了呢?0
cout << p->name << " 已经放弃抢票了,因为没有了..." << endl;
pthread_mutex_unlock(p->mutexp);

break;
}
}

return nullptr;
}

int main()
{
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, nullptr);

struct mythread *m = new mythread;
m->mutexp = &mutex;
strcpy(m->name, "测试");

pthread_t tid1;

pthread_create(&tid1, nullptr, getTickets, (void *)m);

int n = pthread_join(tid1, nullptr);
pthread_mutex_destroy(m->mutexp);
delete m;

return 0;
}

Linux线程互斥_临界资源_05

这里有一个问题,我们访问临界资源是先去竞争锁,也就是锁要被多个线程看到,那么锁不久成为临界资源了吗?是的,那么是谁保证我们竞争锁是原子性的呢?这里是程序员,程序员在设计这些函数的时候已经考虑到这个情况,所以我们可以大胆放心的用.

我想问的是对于已经拿到锁的线程,是不是代表这个线程不能被切换了?不是的,任何一个线程在任何时间任何地点都有可能被切换,但是如果拿到锁的线程被切走了,此时他会带着锁一起走,这样其他的线程是竞争不到锁的,他们是无法访问临界资源的.也就是一旦一个线程具有了锁,该线程是不用担心线程切换的,线程访问临界区只有没有进入和使用完比两个状态,我们在临界区最好不要写太耗时的代码.

加锁原理

这里我和大家说一下加锁的原理,锁的实现方式是有很多种的,我们今天说最常见的一种.**为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期. 现在我们把lock和unlock的伪代码改一下.

Linux线程互斥_加锁_06

其中%al是一个寄存器,所谓的return 0 就是拿到锁成功.下面我用动图给大家演示一下.

Linux线程互斥_临界资源_07

我这里用文字解释一下,在内存种我们有一个mutex,它的值是1并且是唯一的.这个1就代表这把锁,我们线程都在抢这把锁,也就是把寄存器中的值和mutex中的值进行交换,由于这条语句是一条语句,故我们可以称呼为这个交换操作是原子性的,算有所有线程无论是挂起还是被切走,此时都会带走它的上下文,多个线程都会看到%al这个寄存器,也就是寄存器是共享的,但是里面的数据确是线程私有的,所以我们不用担心线程写0的时候覆盖掉上一次寄存器中的内容.这一点很重要,鉴于1只有一个,故是哪个线程先抢到1就是哪个线程抢到了锁.

下面我想把锁给大家封装一下,这样的话后面我们学习C++的线程库可能会更加容易一点,这里主要是借助类的构造函数和析构函数的特性,算是一种RAII的实现吧.

class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_mutex, nullptr);
}
~Mutex()
{
pthread_mutex_destroy(&_mutex);
}

void lock()
{
pthread_mutex_lock(&_mutex);
}
void unlock()
{
pthread_mutex_unlock(&_mutex);
}

private:
pthread_mutex_t _mutex;
};

class Lock_GUARD
{
private:
Mutex *_pMutex;

public:
Lock_GUARD(Mutex *p)
: _pMutex(p)
{
std:: cout << "加锁成功" << std::endl;
_pMutex->lock();
}

~Lock_GUARD()
{
std::cout << "解锁成功" << std::endl;

_pMutex->unlock();
}
};

这里我们用代码验证一下,很简单的.

int tickets = 1000;
Mutex mutex;

bool getTickets()
{
bool ret = false;
Lock_GUARD l(&mutex); // 完成加锁 函数退出后自动析构解锁

if (tickets > 0)
{
usleep(100000);
cout << "thread " << pthread_self() << " 抢到了票, 票的编号: " << tickets << endl;
tickets--;
ret = true;
}
return ret;
}

void *startRoutine(void *args)
{
char *name = (char *)args;
while (true)
{
if (!getTickets())
{
break;
}
cout << name << "获得票成功" << endl;
sleep(1);
}
}

int main()
{

pthread_t tid1;
pthread_t tid2;
pthread_t tid3;
pthread_t tid4;

pthread_create(&tid1, nullptr, startRoutine, (void *)"thread1");
pthread_create(&tid2, nullptr, startRoutine, (void *)"thread2");
pthread_create(&tid3, nullptr, startRoutine, (void *)"thread3");
pthread_create(&tid4, nullptr, startRoutine, (void *)"thread4");

pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
pthread_join(tid3, nullptr);
pthread_join(tid4, nullptr);

return 0;
}

Linux线程互斥_linux_08

可重入函数

在之前的博客中我们是喝大家分析过了函数是否可以被重入,上面的抢票getTickets函数就是被多个线程重复进入了,由于他没有出现安全问题,所以我们说他是可重入函数.大家记得,是否可重入是函数的一个特性,不是函数的安全问题,我们在学习C++的时候,STL中接口大多都是不可重入的,当你不能说他是错误的.

常见锁

上面谈到过互斥锁,我们这个把锁的一些边边角角的知识和大家说一下,倒是挺简单的.我们知道有些函数是不可重入的,但是有可能经过我们加锁之后,他变得可重入了,这里我们要提一下,加锁是一个规范,如果我们后面的项目必须要加锁,那么我们我们就加,还要加的越细越好.但是如果我们可以不加锁,我们尽量就不要加了,毕竟加锁回答来一定的性能损失,关键的是你可能会遇到死锁问题.

死锁

什么是死锁?你可以理解为由于我们加锁的不合理倒是程序卡住了,这就是死锁.我们先来见识一下现象.

pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;
void *callback1(void *args)
{
pthread_mutex_lock(&mutexA);
pthread_mutex_lock(&mutexB);
while (1)
{
cout << "我是线程1" << std::endl;
sleep(1);
}

pthread_mutex_unlock(&mutexA);
pthread_mutex_unlock(&mutexB);
}
void *callback2(void *args)
{
pthread_mutex_lock(&mutexA);
pthread_mutex_lock(&mutexB);
while (1)
{
cout << "我是线程2" << std::endl;
sleep(1);
}

pthread_mutex_unlock(&mutexA);
pthread_mutex_unlock(&mutexB);
}
int main()
{
pthread_t t1, t2;
pthread_create(&t1, nullptr, callback1, (void *)"thread1");
pthread_create(&t1, nullptr, callback2, (void *)"thread1");

pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
return 0;
}

Linux线程互斥_临界资源_09

这个时候竞争锁还是挺不错的,但是如果是下面的加锁方式呢?

pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;
void *callback1(void *args)
{
pthread_mutex_lock(&mutexA);
sleep(1);

pthread_mutex_lock(&mutexB);
while (1)
{
cout << "我是线程1" << std::endl;
sleep(1);
}

pthread_mutex_unlock(&mutexA);
pthread_mutex_unlock(&mutexB);
}
void *callback2(void *args)
{
pthread_mutex_lock(&mutexB);
sleep(1);

pthread_mutex_lock(&mutexA);
while (1)
{
cout << "我是线程2" << std::endl;
sleep(1);
}

pthread_mutex_unlock(&mutexA);
pthread_mutex_unlock(&mutexB);
}
int main()
{
pthread_t t1, t2;
pthread_create(&t1, nullptr, callback1, (void *)"thread1");
pthread_create(&t1, nullptr, callback2, (void *)"thread1");

pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
return 0;
}

Linux线程互斥_d3_10

这个时候我们可以理解为线程A拿着一把锁,他要竞争另外一把锁,此时线程B也是拿着一吧锁,要竞争A手中的锁,他们两个互相争夺对方的锁,谁都不让谁,此时就构成了死锁问题.

下面我们说下产生死锁的几个必要条件.

  • 互斥条件:一个资源每次只能被一个执行流使用,用锁就会构成互斥
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放. 就像线程A拿了一吧锁,还要申请另外一吧锁
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺 .线程A不会放手自己的锁
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系 .线程A要执行,必要拿到B的锁,反之也是如此

那么我想问问,只有一吧锁也会构成死锁吗?是的,加锁是一种规范,不好好加构成死锁的概率还是很大的.解决死锁方式就是少用锁,能不用就不用.

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int cnt = 100;
void *callback1(void *args)
{

while (1)
{
pthread_mutex_lock(&mutex);
pthread_mutex_lock(&mutex);// 脑子抽了
cout << "count " << cnt-- << std::endl;
pthread_mutex_unlock(&mutex);

sleep(1);
}

}

int main()
{
pthread_t t1;
pthread_create(&t1, nullptr, callback1, (void *)"thread1");
pthread_join(t1, nullptr);
return 0;
}

Linux线程互斥_linux_11


标签:cout,void,Linux,nullptr,互斥,线程,pthread,mutex
From: https://blog.51cto.com/byte/5982287

相关文章