Linux一学就会——线程互斥
Linux线程的互斥
进程线程间的互斥相关背景概念
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界自娱的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
互斥量mutex
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量(全局变量、静态变量等),可以通过数据的共享,完成线程之间的交互。(放在已初始化数据段内)
多个线程并发的操作共享变量,会带来一些问题。
测试函数的书写
#include<iostream>
#include<unistd.h>
#include<thread>
#include<pthread.h>
using namespace std;
int tickets=10000;
void* threadrout(void* arg)
{
char* tname=(char*)arg;
while(1)
{
if(tickets>0)
{
usleep(1000);
cout<<tname<<" : 抢到一张,还剩 : "<<--tickets<<endl;
}
else
{
break;
}
}
}
int main()
{
pthread_t t1,t2,t3,t4;
pthread_create(&t1,NULL,threadrout,(void*)"t1");
pthread_create(&t2,NULL,threadrout,(void*)"t2");
pthread_create(&t3,NULL,threadrout,(void*)"t3");
pthread_create(&t4,NULL,threadrout,(void*)"t4");
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_join(t3,NULL);
pthread_join(t4,NULL);
return 0;
}
makefile文件的书写
testThread:testThread.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY clean:
clean:
rm -f testThread
居然减到负数了呢!这要是用在生活当中,一家电影院多卖了三个人的票,总不能让这三个人集体站票吧?
如果你做过单片机的项目也会出现这种情况,但单片机用消抖来消除按键抖动的,可是一个程序一定需要效率,不可能消抖呀,所以要用别的方法。
为什么可能无法获得争取结果?
- if 语句判断条件为真以后,代码可以并发的切换到其他线程
- usleep这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
- –ticket操作本身就不是一个原子操作(不是一条汇编指令完成的)
减减 操作并不是原子操作,而是对应三条汇编指令: - load:将共享变量ticket从内存加载到寄存器中
- update: 更新寄存器里面的值,执行-1操作
- store:将新值,从寄存器写回共享变量ticket的内存地址
要解决以上问题,需要做到三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
互斥量的接口
初始化互斥量
初始化互斥量有两种方法:
- 方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
- 方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL
采用动态加锁代码
#include<iostream>
#include<unistd.h>
#include<thread>
#include<pthread.h>
using namespace std;
int tickets=10000;
pthread_mutex_t mx;
void* threadrout(void* arg)
{
char* tname=(char*)arg;
while(1)
{
pthread_mutex_lock(&mx);
//while中包裹的都是临界区
if(tickets>0)
{
usleep(1000);
cout<<tname<<" : 抢到一张,还剩 : "<<--tickets<<endl;
pthread_mutex_unlock(&mx);
}
else
{
pthread_mutex_unlock(&mx);
break;
}
}
}
int main()
{
pthread_mutex_init(&mx,NULL);
pthread_t t1,t2,t3,t4;
pthread_create(&t1,NULL,threadrout,(void*)"t1");
pthread_create(&t2,NULL,threadrout,(void*)"t2");
pthread_create(&t3,NULL,threadrout,(void*)"t3");
pthread_create(&t4,NULL,threadrout,(void*)"t4");
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_join(t3,NULL);
pthread_join(t4,NULL);
pthread_mutex_destroy(&mx);
return 0;
}
速度一下就慢了好多。
但是有一个问题,就是一个线程会枪好久,一直不轮转。
如图:
稍微修改一下:
void* threadrout(void* arg)
{
char* tname=(char*)arg;
while(1)
{
pthread_mutex_lock(&mx);
//while中包裹的都是临界区
if(tickets>0)
{
usleep(1000);
cout<<tname<<" : 抢到一张,还剩 : "<<--tickets<<endl;
pthread_mutex_unlock(&mx);
}
else
{
pthread_mutex_unlock(&mx);
break;
}
usleep(1000);//增加一个休息轮转的时间
}
}
但是速度就又慢了一些。
互斥量实现原理探究
- 经过上面的例子,大家已经意识到单纯的i++或者++i都不是原子的,有可能会有数据一致性问题
我们的加锁是只有一条汇编执行的,就是exchange指令 - 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下
可重入VS线程安全
概念
- 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
- 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
常见的线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
常见可重入的情况
不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
死锁问题我们下节课讲。