目录 🌏线程互斥🌲概念🌲互斥量mutex🌲互斥量的接口🌲互斥量的原理 🌏线程安全和可重入🌲概念🌲常见的线程安全的情况🌲常见的线程不安全的情况🌲常见可重入的情况🌲常见不可重入的情况🌲区别与联系 🌏死锁🌏条件变量🌲概念🌲条件变量的接口 🌐总结⭐️ 本篇博客开始要继续给大家介绍线程同步和互斥相关的知识。多线程是如何进行同步与互斥 *** 作的,下面我来和大家一起聊一聊~
🌏线程互斥 🌲概念
线程互斥: 任何时刻,保证只有一个执行流进入临界区访问临界资源,通常对临界资源起到保护作用
相关概念
临界资源: 多线程执行流共享的资源就叫做临界资源临界区: 每个线程内部,访问临界资源的代码,就叫做临界区原子性: 不会被任何调度机制打断的 *** 作,该 *** 作只有两态(无中间态,即使被打断,也不会受影响),要么完成,要么未完成 🌲互斥量mutex概念: 多个线程对一个共享变量进行 *** 控时,会引发数据不一致的问题。此时就引入了互斥量(也叫互斥锁)的概念,来保证共享数据 *** 作的完整性。在被加锁的任一时刻,临界区的代码只能被一个线程访问。
为了让大家更好地了解不加互斥量的情况下,多个线程同时 *** 作共享变量会带来哪些的问题,我这里写了一个抢票的小程序,用全局变量ticket
代表现有票数,五个线程分别执行抢票的 *** 作,也就是对ticket
进行减减的 *** 作,直到票数为0就停止抢票
具体代码如下:
#include
#include
#include
int ticket = 100;
void* get_tickets(void* arg)
{
long id = (long)arg;
while (1){
if (ticket > 0){
// 有票
usleep(1000);
ticket--;
printf("thread %ld get a ticket, the number of remaining is %d\n", id , ticket);
}else{
// 无票,退出
break;
}
}
}
int main()
{
pthread_t t[5];
// 创建5个线程
long i = 0;
for (; i < 5; ++i)
{
pthread_create(t+i, NULL, get_tickets, (void*)i);
}
// 释放5个线程
for (i = 0; i < 5; ++i)
{
pthread_join(t[i], NULL);
}
return 0;
}
一次代码运行结果如下:
可以发现的是,票居然抢成了负数,显然不符合实际情况。
分析以上结果有以下几点原因:
--
的 *** 作有很多线程会进入if条件–ticket *** 作本身就不是一个原子 *** 作ticket有三条汇编指令(如下):
movl ticket(%rip), %eax # 把ticket的值加载到eax寄存器中
subl , %eax # 把eax寄存器中的值减1
movl %eax, ticket(%rip) # 把eax寄存器中的值赋给ticket变量
当一个线程正准备执行第三条指令时,两一个线程恰好执行了第二条指令,此时寄存器中的值又减了一次,当第一个线程执行完第三条指令时,ticket其实已经减了两次。所以这个 *** 作不是一个原子 *** 作
如何解决上述问题呢?
代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。🌲互斥量的接口
互斥量其实就是一把锁,是一个类型为pthread_mutex_t
的变量,使用前需要进行初始化 *** 作,使用完之后需要对锁资源进行释放
静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
动态分配
加锁:函数原型:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
参数:
restrict mutex:要初始化的锁restrict attr:不关心,置空返回值: 成功返回0,失败返回错误码
注意:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁再去竞争锁
解锁:函数原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数:
mutex:要加的锁返回值: 成功返回0,失败返回错误码
销毁互斥量:函数原型:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:
mutex:要解的锁返回值: 成功返回0,失败返回错误码
函数原型:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
mutex:要销毁锁返回值: 成功返回0,失败返回错误码
注意:
使用PTHREAD_ MUTEX_ INITIALIZER
初始化的互斥量不需要销毁不要销毁一个已经加锁的互斥量已经销毁的互斥量,要确保后面不会有线程再尝试加锁加锁的粒度要够小
改进上面的抢票小程序代码如下:
#include
#include
#include
pthread_mutex_t mutex;// 创建锁变量
int ticket = 100;
void* get_tickets(void* arg)
{
long id = (long)arg;
while (1){
usleep(1000);// 放这模拟效果更好一些,不然会看到一个线程抢光所所有的票的现象
// 加锁
pthread_mutex_lock(&mutex);
if (ticket > 0){
// 有票
--ticket;
printf("thread %ld get a ticket, the number of remaining is %d\n", id , ticket);
// 解锁
pthread_mutex_unlock(&mutex);
}else{
// 无票,退出
// 解锁
pthread_mutex_unlock(&mutex);
break;
}
}
}
int main()
{
pthread_t t[5];
// 初始化锁
pthread_mutex_init(&mutex, NULL);
// 创建5个线程
long i = 0;
for (; i < 5; ++i)
{
pthread_create(t+i, NULL, get_tickets, (void*)(i+1));
}
// 释放5个线程
for (i = 0; i < 5; ++i)
{
pthread_join(t[i], NULL);
}
// 销毁锁
pthread_mutex_destroy(&mutex);
return 0;
}
代码运行结果如下:
可以看到的是,这样模拟的抢票过程是一个正常的,不会把票抢成负数,这就是因为临界区得到了保护。
总结几点并回答几个问题:
锁的作用: 对临界区进行保护,所有的执行流线程都必须遵守这个规则:lock——>访问临界区——>unlock需要注意的点: 所有的线程必须看到同一把锁,锁本身就是临界资源,所以锁本身需要先保证自身安全申请锁的过程不能出现中间态,必须保证原子性任一线程持有锁之后,其它线程如果还想申请锁时申请不到的,保证互斥性 线程申请不到锁此时会做什么?加锁之后,代码执行效率一般会下降,这是为什么?进入等待队列进行等待,从运行队列转移到等待队列,状态由R变成S,持有锁的线程unlock之后,需要唤醒等待队列中的第一个线程
struct mutex { int lock;// 0 1 // ... sturct wait_queue;//锁下的等待队列 }
🌲互斥量的原理原本并发或并行的执行流变成串行了,在很多线程的情况下,OS的压力会变大
大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
下面是lock和unlock的伪代码
lock:
movb # 把0值放进寄存器a1里, %a1 # 交换a1寄存器的内容和锁的值(无线程使用锁时,metux的值为1)
xchgb %a1, mutex if
( 0%a1 > )return
0 ;# 得到锁 else
;
挂起等待;
goto lock
unlock:
movb #把1赋给锁 mutex ;
唤醒等待的线程return
0 ;#
在上述加锁的伪代码中演示了上步骤:
对寄存器的内容进行清0把mutex的值(被使用值为0,未被使用值为1)和寄存器的内容进行交换寄存器的内容为1代表得到了锁,为0代表未得到锁,要挂起等待解锁的伪代码步骤(只有有锁的线程才可以执行到这段代码):
把mutex的值改为1唤醒等待锁的线程 🌏线程安全和可重入 🌲概念线程安全: 多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行 *** 作,并且没有锁保护的情况下,会出现该问题。
重入: 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
区别:
函数是可重入的,那就是线程安全的函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的联系:
可重入函数是线程安全函数的一种线程安全不一定是可重入的(不一定发生线程安全问题),而可重入函数则一定是线程安全的。如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。 🌏死锁概念: 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁产生的四个必要条件:
互斥条件:一个资源每次只能被一个执行流使用请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系避免死锁:
破坏死锁的四个条件(上面分别对应的是:1.不使用锁 2.让一个执行流放开资源 3. 让两个执行流剥夺两一个执行流的资源 4. 调整申请资源的顺序)假设顺序要一致避免锁未释放的场景资源一次性分配避免死锁算法:
银行家算法:为了防止银行家资金无法周转而倒闭,对每一笔贷款,必须考察其是否能限期归还。在 *** 作系统中研究资源分配策略时也有类似问题,系统中有限的资源要供多个进程使用,必须保证得到的资源的进程能在有限的时间内归还资源,以供其他进程使用资源。死锁检测法实例演示: 线程1拿着锁1运行5秒后申请锁2,线程2拿着锁2运行5秒后申请锁1,观察现象
include#
include#
includepthread_mutex_t
; mutex1// 创建锁变量1pthread_mutex_t
; mutex2// 创建锁变量2int
= ticket 100 ;void
*thread_run1 (void*) argchar
{
*= name ( char*);argpthread_mutex_lock
(&)mutex1;int
= count 0 ;while
( 1)if{
( ++count== 5 )printf{
("%s is requesting a lock...\n",) name;pthread_mutex_lock
(&)mutex2;}
printf
("%s is running...\n",) name;sleep
(1);}
pthread_mutex_unlock
(&)mutex1;}
void
*thread_run2 (void*) argchar
{
*= name ( char*);argpthread_mutex_lock
(&)mutex2;int
= count 0 ;while
( 1)if{
( ++count== 5 )printf{
("%s is requesting a lock...\n",) name;pthread_mutex_lock
(&)mutex1;}
printf
("%s is running...\n",) name;sleep
(1);}
pthread_mutex_unlock
(&)mutex2;}
int
main ()pthread_t
{
, t1; t2// 初始化锁
pthread_mutex_init
(&,mutex1NULL );pthread_mutex_init
(&,mutex2NULL );// 创建2个线程
pthread_create
(&,t1NULL ,, thread_run1( void*)"thread 1");pthread_create
(&,t2NULL ,, thread_run2( void*)"thread 2");// 释放2个线程
pthread_join
(,t1NULL );pthread_join
(,t2NULL );// 销毁锁
pthread_mutex_destroy
(&)mutex1;pthread_mutex_destroy
(&)mutex2;return
0 ;}
pthread_cond_t
代码运行结果如下: 两个线程都想申请对方资源,但各自都不放手自己的资源,造成了死锁
概念: 利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使“条件成立”(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而避免饥饿问题,叫做同步
竞态条件: 因为时序问题,而导致程序异常,我们称之为竞态条件,旨在描述一个系统或者进程的输出依赖于不受控制的事件出现顺序或者出现时机。
为什么存在线程同步?
线程同步使得每个线程都能够访问临界资源,多个线程协同高效完成某些任务。
条件变量如何与互斥锁结合使用?
🌲条件变量的接口条件变量是包含一个等待队列的。多个线程可以去竞争一把锁,没有得到锁资源的线程会在锁上继续挂起等待,当拥有锁的线程条件变量满足时,会先释放锁资源,然后进入到条件变量的等待队列去等待(等待其他线程唤醒),这样其他线程就可以获得锁资源,如果此时唤醒的条件变量满足,该线程可以去唤醒等待队列中的第一个线程,自己释放锁资源,然后让第一个线程重新拥有锁资源,依次如此,多个线程就是顺序地执行工作。这样就可以实现线程同步的 *** 作
条件变量是一个类型为PTHREAD_COND_INITIALIZER
的条件变量,课通过定义变量的方式来定义一个条件变量
条件变量初始化:
静态创建:使用字段int进行初始化动态创建:函数原型:
pthread_cond_init (pthread_cond_t* ,restrict condconst pthread_condattr_t * )restrict attr;int
参数:
restrict cond:要初始化的条件变量restrict attr:不关心,置空
条件变量的销毁:
函数原型:
pthread_cond_destroy (pthread_cond_t* )cond;pthread_cond_wait
参数:
restrict cond:要销毁的条件变量
等待条件变量满足:
函数原型:
(pthread_cond_t* ,restrict condpthread_mutex_t * )restrict mutex;int
参数:
restrict cond:在这个条件条件变量下等待restrict mutex:互斥量
为什么pthread_cond_wait需要互斥量?
条件变量是实现线程同步的一种手段,如果一个线程进入等待队列还不释放锁资源,这样其他线程也不能够得到锁资源,这样唤醒线程的条件变量永远不可能满足,那么这个线程也将一直等待下去。所以一个线程进入等待队列需要释放自己手中的锁资源来实现真正地同步
唤醒条件变量满足:
函数原型:
pthread_cond_broadcast (pthread_cond_t* )cond;int pthread_cond_signal (pthread_cond_t* )cond;#
参数:
cond:第一个函数是唤醒在这个条件变量的等待队列中的所有线程;第二个条件变量是唤醒在这个条件变量的等待队列中的第一个线程
pthread_cond_broadcast和pthread_cond_signal
前者是唤醒等待队列中所以的线程,而后者只唤醒等待队列中的第一个线程。前者会带来一个很不好的效应——惊群效应。多个线程同时被唤醒,但是最终只有一个线程能够获得“控制权”,其他获得控制权失败的线程可能重新进入休眠状态。等待获得控制权的线程释放锁资源后去通知下一个线程,这样就容易引起OS和CPU的管理调度负担,所以不建议使用。
实例演示: 创建五个线程,四个线程执行run1,上来就在条件变量下等待,另一个线程执行run2,然后无脑唤醒等待队列下的线程
include#
include#
includepthread_cond_t
; cond// 条件变量pthread_mutex_t
; mutex// 锁void
*threadrun1 (void*) argchar
{
*= name ( char*);argwhile
( 1)pthread_mutex_lock{
(&)mutex;pthread_cond_wait
(&,cond& )mutex;// 挂起,释放锁,当该函数返回时,进入到临界区,重新持有锁printf
("%s is waked up...\n",) name;sleep
(1);pthread_mutex_unlock
(&)mutex;}
}
void
*threadrun2 (void*) argchar
{
*= name ( char*);argwhile
( 1)sleep{
(1);// 唤醒一个等待队列中的线程
pthread_cond_signal
(&)cond;//pthread_cond_broadcast(&cond);
printf
("%s is wakeing up a thread...\n",) name;}
}
int
main ()pthread_t
{
, pthread1, pthread2, pthread3, pthread4; pthread5// 初始化条件变量
pthread_cond_init
(&,condNULL );pthread_mutex_init
(&,mutexNULL );pthread_create
(&,pthread1NULL ,, threadrun1( void*)"pthread 1");pthread_create
(&,pthread2NULL ,, threadrun1( void*)"pthread 2");pthread_create
(&,pthread3NULL ,, threadrun1( void*)"pthread 3");pthread_create
(&,pthread4NULL ,, threadrun1( void*)"pthread 4");pthread_create
(&,pthread5NULL ,, threadrun2( void*)"pthread 5");pthread_join
(,pthread1NULL );pthread_join
(,pthread2NULL );pthread_join
(,pthread3NULL );pthread_join
(,pthread4NULL );pthread_join
(,pthread5NULL );pthread_mutex_destroy
(&)mutex;pthread_cond_destroy
(&)cond;return
0 ;}
pthread_cond_broadcast
代码运行结果如下: 可以看出的是,1-4号线程都是顺序得被唤醒,这足以说明cond是包括等待队列的
我们把唤醒的动作改成广播,下盖部分代码如下:
(&)cond;printf
("%s is wakeing up all threads...\n",) name;sleep
(2);
代码运行结果如下: 可以看出的是,等待队列的顺序被破坏了,因为是异常唤醒一群,竞争能力弱的可能一直得不到锁资源
多线程的同步与互斥的内容就先介绍到这里。下一篇博客我会给大家介绍生产者消费者模型和信号量相关内容,喜欢的话,欢迎点赞、收藏和关注~
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)