线程(二) linux 互斥
目录
概念补充
互斥量mutex
为什么有互斥
例:多线程抢票
--操作
互斥量(锁)
操作
例:多线程抢票
补充
互斥量的封装
小知识
概念补充
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,叫做临界区
互斥:任何时刻,互斥保证只有一个执行流进入临界区,访问临界资源,对临界资源起保护作用
原子性:不会被任何调度机制打断的操作,该操作只有两种状态,完成和未完成
互斥量mutex
为什么有互斥
多线程如果并发(同一时间段切换着访问同一资源)访问了共享的资源,共享资源的安全性无法保证.
比如
1.多个线程同时访问显示器文件,向屏幕中打印数据,会因为线程调度切换导致打印错乱的问题;
2.如果有全局变量,多个线程都访问会出问题
例:多线程抢票
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <pthread.h>int ticket = 100;void *route(void *arg) {char *id = (char*)arg;while ( 1 ) {if ( ticket > 0 ) {usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;} else {break;}} } int main( ) {pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, "thread 1");pthread_create(&t2, NULL, route, "thread 2");pthread_create(&t3, NULL, route, "thread 3");pthread_create(&t4, NULL, route, "thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL); }
执行到最后,票数会变为负数,为什么呢?
因为在汇编层面,代码会变成多条汇编语句,而单条汇编语句的执行是不会被打断的,执行了就一定会执行完,是原子的;
当执行到ticket为1时,某线程可能刚执行完ticket>0的汇编语句,线程就因为时间片耗尽等原因被切换了,其他线程这时执行ticket>0的汇编语句取到的也是1,然后再次切换,所有线程都在ticket>0这里判断成功了,之后向下执行,导致ticket最后减到了-3.
这就是多线程访问公共资源导致的问题之一.
--操作
--被汇编为多条指令,可能一个线程刚从内存取数据,时间片到了,调度另一个线程,另一个线程取数据,分析数据,执行--,然后结束,调度回之前的线程,结果两个线程执行完,结果只--了一次.
所以--和++操作多线程下也是不安全的.
互斥量(锁)
要解决上面的问题,需要做到三点
1.代码要有互斥行为:当代码进入临界区时,不允许其他线程进入临界区
2.如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许⼀个线程进入该临界区。
3.如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区
linux提供了一把锁,叫互斥量
操作
全局互斥量
直接使用宏来初始化一个全局mutex锁
局部互斥量
局部互斥量需要初始化和销毁
申请锁和释放锁
int pthread_mutex_lock(pthread_mutex_t *mutex);//申请
int pthread_mutex_unlock(pthread_mutex_t *mutex);//释放
使用时,将临界区代码放在申请和释放锁的内部.
例:多线程抢票
#include <iostream>
#include <unistd.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int tickets = 100;
void *func(void *args)
{
(void*)args;
while(true)
{
{
pthread_mutex_lock(&lock);
if(tickets>0)
{
sleep(0.1);
printf("剩余票数: %d\n",--tickets);
}
else
{
break;
}
pthread_mutex_unlock(&lock);
}//加一层花括号表示锁住的区域
}
return (void*)0;
}
int main()
{
pthread_t t1,t2,t3;
pthread_create(&t1,nullptr,func,nullptr);
pthread_create(&t2,nullptr,func,nullptr);
pthread_create(&t3,nullptr,func,nullptr);
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
return 0;
}
补充
1.互斥量会被多个线程并发访问,所以它也是临界资源,怎么保证自身的安全?
解: 锁是原子的
实现原子性的方案:
(1)硬件上:关闭所有中断
(2)软件上:因为每一条汇编语句都是原子的,所以只通过一条汇编语句实现互斥量(锁)即可
linux中使用交换的机制,大体流程:
在内存中会有mutex变量的数据区域,存放1,cpu中有寄存器存放0
程序执行到锁这里时,直接交换内存和cpu寄存器中的值,然后判断寄存器中的值,是1,继续执行,是0,该线程不向下执行,而是或者阻塞等待,或者不断重复申请,这样只有申请到1的线程可以执行临界区代码,其他线程只能等.
解锁就是将1设置到内存中的mutex区域,以便其他线程申请
2.申请成功,线程继续运行,申请失败,线程会被阻塞
3.如果进入了临界区,线程还会被切换导致并发问题吗?
会被切换,但即使被切换,此进程未解锁时,其他线程进行申请锁也会失败,阻塞住
互斥量的封装
<Lock.hpp>
#pragma once
#include <iostream>
#include <unistd.h>
#include <pthread.h>
class Mutex
{
pthread_mutex_t _mutex;
public:
Mutex(const Mutex&) = delete;
const Mutex& operator = (const Mutex&) = delete;
Mutex()
{
int n = pthread_mutex_init(&_mutex,nullptr);
(void)n;
}
void Lock()
{
int n = pthread_mutex_lock(&_mutex);
(void)n;
}
void Unlock()
{
int n = pthread_mutex_unlock(&_mutex);
(void)n;
}
pthread_mutex_t *Get()
{
return &_mutex;
}
~Mutex()
{
int n = pthread_mutex_destroy(&_mutex);
(void)n;
}
};
class LockGuard
{
Mutex &_mutex;
public:
LockGuard(Mutex& mutex):_mutex(mutex)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
};
LockGarud用来管理锁
所以多线程抢票的代码可以改为
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "Lock.hpp"int ticket = 1000;Mutex mutex;
void *route(void *arg)
{char *id = (char *)arg;while (1){LockGuard lockguard(mutex); // 使⽤RAII⻛格的锁 if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;}else{break;}}return nullptr;
}
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void*)"thread 1");pthread_create(&t2, NULL, route, (void*)"thread 2");pthread_create(&t3, NULL, route, (void*)"thread 3");pthread_create(&t4, NULL, route, (void*)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}
小知识
1.锁的使用的最佳实践: 加锁和解锁时,囊括的区域尽量是最小集
2.互斥的负面影响:降低程序的运行效率