精华内容
下载资源
问答
  • Linux多线程的使用一:互斥锁
    2020-12-28 19:53:36

    多线程经常会在Linux的开发中用到,我想把平时的使用和思考记录下来,一是给自己做个备忘,二是分享给可能会用到的人。

    POSIX标准下互斥锁是pthread_mutex_t,与之相关的函数有:

    1 int pthread_mutex_init(pthread_mutex_t * mutex , pthread_mutexattr_t *attr);2 int pthread_mutex_destroy (pthread_mutex_t *mutex);3 int pthread_mutex_lock (pthread_mutex_t *mutex );4 int pthread_mutex_unlock (pthread_mutex_t *mutex );5 int pthread_mutex_trylock (pthread_mutex_t * mutex );

    初始化锁用pthread_mutex_init,也可以用pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER(普通锁,最常见)来初始化;销毁用pthread_mutex_destroy,Linux中互斥锁并不占用资源,所以不去销毁也可以。一旦互斥锁被锁住了(pthread_mutex_lock),另一个地方再调用pthread_mutex_lock,就会被阻塞住,直到有pthread_mutex_unlock来解锁这个互斥锁,以此来保证多线程执行的有序性。pthread_mutex_trylock不会被阻塞住,如果当前互斥锁被锁住了,pthread_mutex_trylock会返回一个异常值;如果没被锁住,就去锁定之,和pthread_mutex_lock效果一样。我感觉trylock在平时并不常用,最常用的还是初始化,lock,unlock,因为Linux下锁不去销毁也可以,所以destory用的也不多。

    在C++的使用环境中,通常为了方便使用,会去封装一下:

    1 classCMutex2 {3 public:4 CMutex()5 {6 mutex =PTHREAD_MUTEX_INITIALIZER;7 }8 ~CMutex(){}9 voidLock()10 {11 pthread_mutex_lock(&mutex);12 }13 voidUnlock()14 {15 pthread_mutex_unlock(&mutex);16 }17 private:18 pthread_mutex_t mutex;19 };

    还有较常用的方式是封装成自动锁,当这个对象创建的时候上锁;当执行到这个对象的作用域外,对象销毁,自动解锁。

    1 classCAutoMutex2 {3 public:4 CAutoMutex()5 {6 mutex =PTHREAD_MUTEX_INITIALIZER;7 pthread_mutex_lock(&mutex);8 }9 ~CAutoMutex()10 {11 pthread_mutex_unlock(&mutex);12 }13 private:14 pthread_mutex_t mutex;15 };

    更多相关内容
  • 互斥锁、自旋锁、读写锁和文件锁互斥锁自旋锁自旋锁与互斥锁之间的区别读写锁文件锁乐观锁与悲观锁举个例子服务端是如何解决这种冲突的 互斥锁 互斥锁(mutex)又叫互斥量,从本质上说是一把锁,在访问共享资源之前...

    互斥锁

    互斥锁(mutex)又叫互斥量,从本质上说是一把锁,在访问共享资源之前对互斥锁进行上锁,在访问完成后释放互斥锁(解锁);对互斥锁进行上锁之后,任何其它试图再次对互斥锁进行加锁的线程都会被阻塞,直到当前线程释放互斥锁。如果释放互斥锁时有一个以上的线程阻塞,那么这些阻塞的线程会被唤醒,它们都会尝试对互斥锁进行加锁,当有一个线程成功对互斥锁上锁之后,其它线程就不能再次上锁了,只能再次陷入阻塞,等待下一次解锁。

    在我们的程序设计当中,只有将所有线程访问共享资源都设计成相同的数据访问规则,互斥锁才能正常工作。如果允许其中的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其它的线程在使用共享资源前都申请锁,也还是会出现数据不一致的问题。互斥锁使用 pthread_mutex_t 数据类型表示,在使用互斥锁之前,必须首先对它进行初始化操作。

    自旋锁

    自旋锁与互斥锁很相似,从本质上说也是一把锁,在访问共享资源之前对自旋锁进行上锁,在访问完成后释放自旋锁(解锁);事实上,从实现方式上来说,互斥锁是基于自旋锁来实现的,所以自旋锁相较于互斥锁更加底层。

    如果在获取自旋锁时,自旋锁处于未锁定状态,那么将立即获得锁(对自旋锁上锁);如果在获取自旋锁时,自旋锁已经处于锁定状态了,那么获取锁操作将会在原地“自旋”,直到该自旋锁的持有者释放了锁。由此介绍可知,自旋锁与互斥锁相似,但是互斥锁在无法获取到锁时会让线程陷入阻塞等待状态;而自旋锁在无法获取到锁时,将会在原地“自旋”等待。“自旋”其实就是调用者一直在循环查看该自旋锁的持有者是否已经释放了锁,“自旋”一词因此得名。

    自旋锁的不足之处在于:自旋锁一直占用的 CPU,它在未获得锁的情况下,一直处于运行状态(自旋),所以占着 CPU,如果不能在很短的时间内获取锁,这无疑会使 CPU 效率降低。

    试图对同一自旋锁加锁两次必然会导致死锁,而试图对同一互斥锁加锁两次不一定会导致死锁,原因在于互斥锁有不同的类型,当设置为PTHREAD_MUTEX_ERRORCHECK 类型时,会进行错误检查,第二次加锁会返回错误,所以不会进入死锁状态。

    因此我们要谨慎使用自旋锁,自旋锁通常用于以下情况:需要保护的代码段执行时间很短,这样就会使得持有锁的线程会很快释放锁,而“自旋”等待的线程也只需等待很短的时间;在这种情况下就比较适合使用自旋锁,效率高!

    自旋锁与互斥锁之间的区别

    • 实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;
    • 开销上的区别:获取不到互斥锁会陷入阻塞状态(休眠),直到获取到锁时被唤醒;而获取不到自旋锁会在原地“自旋”,直到获取到锁;休眠与唤醒开销是很大的,所以互斥锁的开销要远高于自旋锁、自旋锁的效率远高于互斥锁;但如果长时间的“自旋”等待,会使得 CPU 使用效率降低,故自旋锁不适用于等待时间比较长的情况。
    • 使用场景的区别:自旋锁在用户态应用程序中使用的比较少,通常在内核代码中使用比较多;因为自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能被抢占(内核中使用自旋锁会自动禁止抢占),一旦休眠意味着执行中断服务函数时主动交出了CPU使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁!
    • 最底层的两种就是「互斥锁和自旋锁」,有很多高级的锁都是基于它们实现,可以认为它们是各种锁的地基,所以得清楚它俩之间的区别和应用。加锁的目的是保证共享资源在任意时间里,只有一个线程访问,这样就可以避免多线程导致共享数据错乱的问题。当已经有一个线程加锁后,其他线程加锁则会失败,互斥锁和自旋锁对于加锁失败后的处理方式是不一样的。

    读写锁

    互斥锁或自旋锁要么是加锁状态、要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁有3 种状态:读模式下的加锁状态(以下简称读加锁状态)、写模式下的加锁状态(以下简称写加锁状态)和不加锁状态(见),一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。因此可知,读写锁比互斥锁具有更高的并行性!
    在这里插入图片描述
    读写锁有如下两个规则:

    • 当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞。
    • 当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。

    虽然各操作系统对读写锁的实现各不相同,但当读写锁处于读模式加锁状态,而这时有一个线程试图以写模式获取锁时,该线程会被阻塞;而如果另一线程以读模式获取锁,则会成功获取到锁,对共享资源进行读操作。

    所以,读写锁非常适合于对共享数据读的次数远大于写的次数的情况。当读写锁处于写模式加锁状态时,它所保护的数据可以被安全的修改,因为一次只有一个线程可以在写模式下拥有这个锁;当读写锁处于读模式加锁状态时,它所保护的数据就可以被多个获取读模式锁的线程读取。所以在应用程序当中,使用读写锁实现线程同步,当线程需要对共享数据进行读操作时,需要先获取读模式锁(对读模式锁进行加锁),当读取操作完成之后再释放读模式锁(对读模式锁进行解锁);当线程需要对共享数据进行写操作时,需要先获取到写模式锁,当写操作完成之后再释放写模式锁。

    读写锁也叫做共享互斥锁。当读写锁是读模式锁住时,就可以说成是共享模式锁住。当它是写模式锁住时,就可以说成是互斥模式锁住。

    文件锁

    现象一下,当两个人同时编辑磁盘中同一份文件时,其后果将会如何呢?在 Linux 系统中,该文件的最后状态通常取决于写该文件的最后一个进程。多个进程同时操作同一文件,很容易导致文件中的数据发生混乱,因为多个进程对文件进行 I/O 操作时,容易产生竞争状态、导致文件中的内容与预想的不一致!

    对于有些应用程序,进程有时需要确保只有它自己能够对某一文件进行 I/O 操作,在这段时间内不允许其它进程对该文件进行 I/O 操作。为了向进程提供这种功能,Linux 系统提供了文件锁机制。

    前面学习过互斥锁、自旋锁以及读写锁,文件锁与这些锁一样,都是内核提供的锁机制,锁机制实现用于对共享资源的访问进行保护;只不过互斥锁、自旋锁、读写锁与文件锁的应用场景不一样,互斥锁、自旋锁、读写锁主要用在多线程环境下,对共享资源的访问进行保护,做到线程同步。

    而文件锁,顾名思义是一种应用于文件的锁机制,当多个进程同时操作同一文件时,我们怎么保证文件数据的正确性,linux 通常采用的方法是对文件上锁,来避免多个进程同时操作同一文件时产生竞争状态。譬如进程对文件进行 I/O 操作时,首先对文件进行上锁,将其锁住,然后再进行读写操作;只要进程没有对文件进行解锁,那么其它的进程将无法对其进行操作;这样就可以保证,文件被锁住期间,只有它(该进程)可以对其进行读写操作。

    一个文件既然可以被多个进程同时操作,那说明文件必然是一种共享资源,所以由此可知,归根结底,文件锁也是一种用于对共享资源的访问进行保护的机制,通过对文件上锁,来避免访问共享资源产生竞争状态

    乐观锁与悲观锁

    前面提到的互斥锁、自旋锁、读写锁,都是属于悲观锁。悲观锁做事比较悲观,它认为多线程同时修改共享资源的概率比较高,于是很容易出现冲突,所以访问共享资源前,先要上锁。

    相反的,如果多线程同时修改共享资源的概率比较低,就可以采用乐观锁。乐观锁做事比较乐观,它假定冲突的概率很低,它的工作方式是:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。

    放弃后如何重试,这跟业务场景息息相关,虽然重试的成本很高,但是冲突的概率足够低的话,还是可以接受的。可见,乐观锁的心态是,不管三七二十一,先改了资源再说。另外,你会发现乐观锁全程并没有加锁,所以它也叫无锁编程。

    举个例子

    在线文档是可以同时多人编辑,如果使用了悲观锁,只要有一个用户正在编辑文档,此时其他用户就无法打开相同的文档,用户体验当然不好。

    实现多人同时编辑,实际上是用了乐观锁,它允许多个用户打开同一个文档进行编辑,编辑完提交之后才验证修改的内容是否有冲突。

    怎么样才算发生冲突?这里举个例子,比如用户 A 先在浏览器编辑文档,之后用户 B 在浏览器也打开了相同的文档进行编辑,但是用户 B 比用户 A 提交改动,这一过程用户 A 是不知道的,当 A 提交修改完的内容时,那么 A 和 B 之间并行修改的地方就会发生冲突。

    服务端是如何解决这种冲突的

    由于发生冲突的概率比较低,所以先让用户编辑文档,但是浏览器在下载文档时会记录下服务端返回的文档版本号;当用户提交修改时,发给服务端的请求会带上原始文档版本号,服务器收到后将它与当前版本号进行比较,如果版本号一致则修改成功,否则提交失败。

    实际上,常见的 SVN 和 Git 也是用了乐观锁的思想,先让用户编辑代码,然后提交的时候,通过版本号来判断是否产生了冲突,发生了冲突的地方,需要我们自己修改后,再重新提交。

    乐观锁虽然去除了加锁解锁的操作,但一旦发生冲突,重试的成本非常高,所以只有在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。

    总结

    互斥锁、自旋锁和读写锁用于解决多线程同步的问题

    文件锁适用于多个进程同时操作同一文件,这时很容易导致文件中的数据发生混乱

    展开全文
  • 互斥锁(Mutex) 互斥锁是一个互斥的同步对象,意味着同一时间有且仅有一个线程可以获取它...它处于等到状态直至所调用互斥锁可以被获取,因此该方法将组织主调线程直到指定的互斥锁可用,如果不需要拥有互斥锁,用R
  • 互斥锁与死锁(linux多线程)

    千次阅读 2020-04-10 19:39:47
    之前我们了解到一些线程的基本知识,线程等待,线程分离啊什么的。现在我们用这些知识简单实现一个火车站抢票的功能。 假设一共有100张票,我们开放4个窗口(线程),让每个窗口进行卖票的功能,每个窗口之间是独立的...

    之前我们了解到一些线程的基本知识,线程等待,线程分离啊什么的。现在我们用这些知识简单实现一个火车站抢票的功能。
    假设一共有100张票,我们开放4个窗口(线程),让每个窗口进行卖票的功能,每个窗口之间是独立的,他们的任务就是卖完这100张票,每卖一张票,就让总票数-1。

    void* ThreadStart(void* arg)
    {
       (void*)arg;
     
       while(1)
       {
         if(g_tickets > 0)
         {
           g_tickets--; //总票数-1
           //usleep(100000); //模拟一个窗口的堵塞                                                                                                                                                  
           printf("i am thread [%p],I'll sell ticket number [%d]\n",\
                     pthread_self(),g_tickets + 1);
         }                                  
         else                               
         {                                  
           break;//没有票了,直接返回该线程 
         }                                  
       }                                    
       return NULL;                         
    } 
    

    这样写每个线程的任务,看上去好像是没有什么问题,先看看运行结果
    在这里插入图片描述
    好像真的没有什么问题,但是这是建立在每个线程执行一个任务都是很快的情况下,我们现实中每一个买票的过程所花费的时间都不短,这可以理解成一种阻塞。我们在程序中模拟一下这个阻塞的过程,看看会出现什么结果。
    在这里插入图片描述
    不得了了,我们发现好像有几张票没卖出去,又好像1号票被卖了四次,不同的窗口出售了同样的一张票,结果出现了二义性,这个问题很严重,怎么解决呢?这就得用线程安全的知识了

    线程安全

    通过上面的代码,我们发现多个线程同时运行的时候,在访问临界资源后,使得程序出现了二义性的结果。
    线程安全就是为了解决多个线程在同时运行时,在访问临界资源的同时不能让程序出现二义性的结果。

    • 临界资源:在同一时刻,一个资源只能被一个线程(执行流)所访问
    • 访问:在临界区中对临界资源进行非原子操作
    • 原子操作:原子操作使得当前操作是一步完成的,也就是每一个操作只能有两个结果,一个是完成,一个是未完成

    再次了解原子性

    在这里插入图片描述

    1. 执行流A先从CPU中获得数据 g_tickets = 100,在获取完数据之后发生了阻塞,开始处理这个数据,此时还没有执行 g_tickets-- 的操作。
    2. 在执行流A处理第100张票的同时,执行流B开始从CPU中获取数据,此时因为执行流A还没有进行 g_tickets-- 的操作,CPU中 g_tickets = 100,执行流B开始处理这个数据,进行 g_tickets–,然后返回给CPU处理后的结果,g_tickets = 99。
    3. 之后执行流A在执行 g_tickets-- 之后,返回给CPU的结果是 g_tickets = 99。

    经过这样一个模拟阻塞的过程,发现原本应该是 g_tickets = 98 的结果,却因为二义性导致结果是 g_tickets = 99。这就是由于执行流A执行的 g_tickets-- 操作是非原子的操作,也就是执行流A在执行的时候,可能会遇到时间片耗尽,从而导致执行流A被调度。相当于执行流A在执行时的任何一个地方都可能会被打断。

    如何保证线程安全

    • 互斥:保证在同一时刻只能有一个执行流访问临界资源,如果多个执行流想要同时问临界资源,并且临界资源没有执行流在执行,那么只能允许一个执行流进入该临界区。
      也就是如果执行流A,B,C…想要同时访问临界资源,这时就只能有一个执行流先访问临界资源,假设此时访问的是执行流A,其他执行流B,C…不能打断执行流A的执行。
    • 同步:保证程序对临界资源的合理访问。也就是执行流A执行完自己的任务时,必须让出CPU资源,不能一直占着CPU,应该及时让出CPU,使其他执行流也可以执行他们自己的任务。

    在这里插入图片描述
    看着图片“形象”的再来理解一次,假设有一个厕所,互斥就是同一时间只能有一个滑稽去上厕所,其他滑稽只能在外面排队;同步就是滑稽A上完厕所后不能占着茅坑不拉* ,应该赶紧出去让出坑位给其他滑稽。

    想要做到这几点,本质上就是需要一把锁,也就是互斥量 (mutex)
    在这里插入图片描述

    互斥锁

    互斥锁是用来保证互斥属性的一种操作
    互斥锁的底层是互斥量,而互斥量**(mutex)**的本质是一个计数器,这个计数器只有两个状态,即 0 和 1 。

    • 0:表示无法获取互斥锁,也就是需要访问的临界资源不可以被访问
    • 1:表示可以获取互斥锁,也就是需要访问的临界资源可以被访问

    在这里插入图片描述

    加锁与解锁

    加锁的过程可以使用互斥锁提供的接口,以此来获取互斥锁资源

    • 互斥量计数器的值为1,表示可以获取互斥锁,获取到互斥锁的接口就正常返回,然后访问临界资源。
    • 互斥量计数器的值为0,表示不可以获取互斥锁,当前获取互斥锁的接口进行阻塞等待。

    加锁操作:对互斥锁当中的互斥量保存的计数器进行减1操作
    解锁操作:对互斥锁当中的互斥量保存的计数器进行加1操作

    看到这里,就可以简单的理解为加锁和解锁就是这样的一个过程在这里插入图片描述
    那么问题来了,我们在购票代码中改变票数的操作就是 g_tickets–,这个的本质就是减一。互斥量计数器本身也是一个变量,这个变量的取值是0/1,对于这样一个变量进行加一减一操作的时候,这就是原子性操作吗?

    这时候不禁想起老爹的那句话
    在这里插入图片描述
    所以说,想要解决原子性的问题,还得要用原子性的操作,怎么一步就判断有没有加锁呢?
    在这里插入图片描述
    汇编中有一个指令xchgb,可以用来交换寄存器和内存中的内容,这个操作就是原子性的,一步到位。
    我们再来分析一下,如果是需要加锁的情况,互斥量计数器最后就会从1变成0;如果是不能加锁的情况,互斥量计数器中的值还是0,也就是判断之后,互斥量计数器的值都会变成0。所以我们可以在寄存器中存一个数字0,然后用这个数字0和互斥量计数器中的内容进行交换,一步到位,然后我们再根据这个寄存器交换后的值来判断加锁的情况。

    当交换完毕之后,判断寄存器中的值的两种情况

    • 如果寄存器中的值为0,则表示不能进行加锁,意味着当前加锁操作就会被阻塞,也就是不能访问这个临界资源
    • 如果寄存器中的值为1,则表示可以进行加锁,互斥量计数器与内存的值进行xchgb交换操作,就相当于给互斥量计数器进行了一个减一的操作,然后该执行流就可以获得被锁着的资源,并且加锁操作返回,从而去访问临界资源

    再次总结一下

    • 当互斥量计数器的值为1的时候,表示可以进行加锁。然后把寄存器和内存的值进行xchgb交换之后,就相当于给计数器进行了减一操作
    • 当互斥量计数器的值为0的时候,表示不可以加锁。这个时候把寄存器和内存的值进行xchgb交换之后,并没有影响到原互斥量计数器数据的真实性,当前的执行流就被挂起等待了。

    互斥锁的使用流程

    1.定义互斥锁

    • pthread_mutex_t:互斥锁变量的类型
    pthread_mutex_t lock;
    

    2.初始化互斥锁

    方法一:
    #include <pthread.h>
    int pthread_mutex_init(pthread_mutex_t *restrict mutex,
                  const pthread_mutexattr_t *restrict attr);
    
    • mutex:互斥锁变量,传参的时候传入互斥锁变量的地址
    • attr:互斥锁的属性,一般情况下采用默认属性,传入一个参数NULL,让操作系统去处理细节的操作
    方法二:
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //pthread_mutex_initializer
    

    pthread_mutexattr_t 本身是一个结构体的类型,我们可以用 PTHREAD_MUTEX_INITIALIZER 宏定义一个结构体的值,使用这种初始化的方法可以直接填充 pthread_mutexattr_t 这个结构体
    在这里插入图片描述
    3.加锁

    方法一:
    #include <pthread.h>
    int pthread_mutex_lock(pthread_mutex_t *mutex);
    
    
    • mutex:传入互斥锁变量的地址,来进行加锁操作
      使用该接口进行加锁操作的时候:
      如果计数器当中的值为1,意味着可以加锁,加锁操作之后,计数器当中的1变成0
      如果计数器当中的值为0,意味着不可以加锁,该接口进行阻塞等待,执行流就不会向下继续执行了
    方法二:
    int pthread_mutex_trylock(pthread_mutex_t *mutex);
    
    • mutex:传入互斥锁变量的地址,来进行加锁操作
      使用该接口进行加锁操作的时候:
      如果计数器当中的值为1,意味着可以加锁,加锁操作之后,计数器当中的1变成0
      如果计数器当中的值为0,意味着不可以加锁,该接口直接返回,不进行阻塞等待,返回值为EBUSY(表示拿不到互斥锁)

    所以说,一般在采用 pthread_mutex_trylock 加锁的方式时,做一个循环加锁的操作,防止因为拿不到临界资源而直接返回,进而在代码总直接访问临界资源,从而导致程序产生二义性的结果。

    方法三:带有超时时间的加锁接口
    #include <pthread.h>
    #include <time.h>
    int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
       const struct timespec *restrict abs_timeout);
    
    • mutex:传入互斥锁变量的地址,来进行加锁操作
    • abs_timeout:加锁超时时间。当加锁的时候,如果超过设置的超时时间还没有获取到互斥锁的时候,则进行报错返回,并不会进行阻塞等待,返回值为ETIMEOUT
      struct timespace 有两个变量,一个代表秒,一个代表纳秒
      在这里插入图片描述

    4.解锁

    int pthread_mutex_unlock(pthread_mutex_t *mutex);
    

    不管是对 pthread_mutex_lock ,pthread_mutex_trylock,还是pthread_mutex_timedlock进行加锁操作,使用该函数都可以进行一个解锁,“万能钥匙”。

    5.销毁互斥锁

    int pthread_mutex_destroy(pthread_mutex_t *mutex);
    

    互斥锁销毁接口,如果使用互斥锁完成之后,如果不调用销毁接口,就会造成内存泄漏的问题。

    我们再来完善一下那个买票的程序

    1. 互斥锁的定义:因为他通常需要在不同的地方使用,所以定义为全局变量会比较方便一点;如果是C++程序的话,可以定义在类的成员变量中。
    2. 初始化:在创建线程之前就进行初始化
    3. 销毁:在所有线程退出之后进行销毁。如果还有线程没有退出就销毁了互斥锁,其他线程就会出现卡顿,死锁。
    4. 加锁的地方:是他访问临界资源的地方之前
    5. 解锁的地方:必须在所有有可能退出程序的地方都要解一个锁。如果一个进程加锁之后,该执行流直接退出掉,就会使得其他想要获取该互斥锁的进程卡死。
    6. 如果一个执行流加锁了,但是没有进行相应的解锁操作,就会使其他想获取该互斥锁的执行流陷入进程阻塞。
      在这里插入图片描述
    #include <stdio.h>
    #include <unistd.h>
    #include <pthread.h>
    
    #define THREADNUM 4   //4个线程来当做购票的窗口
    int g_tickets = 100;  //100张票
    pthread_mutex_t lock; 定义一个互斥锁变量
    
    void* ThreadStart(void* arg)
    {
      (void*)arg;
    
      while(1)
      {
        pthread_mutex_lock(&lock);
        if(g_tickets > 0)
        {
          	g_tickets--; //总票数-1
        	usleep(10000); //模拟一个
    
          	printf("i am thread [%p],I'll sell ticket number [%d]\n",\
                    pthread_self(),g_tickets + 1);
        }
        else 
        {
         	//假设有一个执行流判断了g_tickets之后发现,g_tickets的值是小于等于0的            
           //则会执行else逻辑,直接就被break跳出while循环
           //跳出while循环的执行流还加着互斥锁
           //所以在所有有可能退出线程的地方都需要进行解锁操作
           pthread_mutex_unlock(&lock);
           break;//没有票了,直接返回该线程
    
        }
        pthread_mutex_unlock(&lock);
      }
      return NULL;
    }
    
    int main()
    {
       pthread_mutex_init(&lock,NULL);//创建线程之前进行初始化
       pthread_t tid[THREADNUM];//保存线程的标识符
      
      int  i = 0;
      for(i = 0; i < THREADNUM; i++)
      {
        int ret = pthread_create(&tid[i],NULL,ThreadStart,NULL);
        if(ret < 0)
        {
          perror("pthread_create error\n");
          return 0;
        }
      }
      sleep(1);
      for(i = 0; i < THREADNUM; i++)
      {
        //线程等待 
        pthread_join(tid[i],NULL);
      }
      //锁销毁 
      pthread_mutex_destroy(&lock);
      return 0;
    }
    

    死锁

    产生死锁的两种方式

    1. 假如程序当中有一个执行流因为结束了当前线程而没有进行解锁操作,由于他没有进行解锁操作,就会使其他想要获取互斥锁的线程进行阻塞,从而产生死锁
    2. 当程序中有多个互斥锁存在的时候,两个或者多个已经上锁的线程之间互相申请对方的互斥锁资源,就会使双方都陷入永久等待的状态,从而产生死锁

    在这里插入图片描述
    对第二种产生死锁方式的解释
    假设有两个执行流(执行流A,执行流B),两个互斥锁(互斥锁1,互斥锁2)。
    两个线程任务的第一步就是上锁,执行流A先获取互斥锁1,执行流B获取互斥锁2。
    第二步,执行流A在已经上锁了互斥锁1的条件下,想要想要获取互斥锁2;与此同时,执行流B又在上锁了互斥锁2的条件下想要获取互斥锁1。两个执行流第二步想要获取的互斥锁都处于上锁的状态,同时两个执行流都处于无法停止的任务中,也就是阻塞状态。

    #include <stdio.h>
    #include <unistd.h>
    #include <pthread.h>
    
    pthread_mutex_t lock1;
    pthread_mutex_t lock2;
    
    void* ThreadA(void* arg)
    {
      (void)arg;
      //设置进程属性为结束后自动释放进程空间 
      pthread_detach(pthread_self());
      //获取互斥锁1 
      pthread_mutex_lock(&lock1);
      sleep(3);
    
      //获取互斥锁2 
      pthread_mutex_lock(&lock2);
      //解锁 
      pthread_mutex_unlock(&lock1);
      pthread_mutex_unlock(&lock2);
      return NULL;
    }
    
    void* ThreadB(void* arg)
    {
      (void)arg;
      pthread_detach(pthread_self());
      //获取互斥锁2 
      pthread_mutex_lock(&lock2);
      sleep(3);
    
      //获取互斥锁1 
      pthread_mutex_lock(&lock1);
    
      pthread_mutex_unlock(&lock2);
      pthread_mutex_unlock(&lock1);
      return NULL;
    }
    
    int main()
    {
      //互斥锁初始化 
      pthread_mutex_init(&lock1,NULL);
      pthread_mutex_init(&lock2,NULL);
      pthread_t tid[2];//模拟两个执行流 
      //创建两个线程 
      int ret = pthread_create(&tid[0],NULL,ThreadA,NULL);
      if(ret < 0)
      {
        perror("pthread_create A error");
      }
      ret = pthread_create(&tid[1],NULL,ThreadB,NULL);
      if(ret < 0)
      {
        perror("pthread_create B error");
      }
      //主线程进行等待 
      while(1)
      {
        sleep(1);
        printf("i am main thread\n");
      }
    
      //互斥锁销毁 
      pthread_mutex_destroy(&lock1);
      pthread_mutex_destroy(&lock2);
      return 0;
    }
    

    在这里插入图片描述
    再用gdb 调试一下,看看线程阻塞的地方

    查看所有线程调用堆栈的信息 --> thread apply all bt
    切换到某一个执行流 --> t [执行流编号]
    在某一个执行流中查看该执行流调用堆栈的信息 --> f [堆栈编号] 可以跳转到具体的堆栈

    查看两个锁所占有的线程号(执行流号)
    在这里插入图片描述
    查看两个线程(执行流)发生阻塞的位置
    在这里插入图片描述

    死锁产生的条件

    1. 每一把锁只能同时被一个执行流占用。互斥条件
    2. 已经占有互斥锁的执行流不能申请其他互斥锁。请求与保持
    3. 多个执行流在请求锁资源时,形成了一个闭环。循环等待条件
    4. 只有拥有互斥锁的线程才可以释放该互斥锁的资源。不可剥夺

    避免死锁的方式

    1. 每个线程加锁的顺序一致。让每个线程都是按照同一个顺序去加互斥锁。
    2. 使用完互斥锁后,及时释放锁资源
    3. 一次性分配互斥锁。在加锁之前,先判断该执行流执行过程中需要加的所有互斥锁是否都是空闲的,然后一次性给这些互斥锁都加锁,如果有锁资源被占用,就等待锁资源齐全后再进行加锁。
    4. 避免产生死锁的几种情况

    破坏请求与保持情况:

    1. 静态分配,每个进程开始执行时就申请完所需要的资源。(一次性分配互斥锁)
    2. 动态分配,每个线程在申请他所需要的资源时,它本身不占用系统资源

    破坏不可剥夺条件:
    当线程不能获得所需要的资源时,就让这个线程陷入等待状态,在等待的时候把该线程已经占有的资源隐式的释放到系统的资源列表中,让他所占有的资源可以被其他进程使用。
    这个等待的进程在他重新获得自己已有的资源以及新申请的资源才可以取消等待。

    破坏循环等待条件
    采用资源有序分配,将系统中的所有资源进行顺序编号,将紧缺的,稀少的用处大的资源采用较大的编号。
    在线程申请资源的时候,必须按照编号的顺序进行,一个线程只有获得较小编号的资源才可以申请较大编号的资源。

    展开全文
  • 文章目录Mutex概念RTX互斥锁API互斥锁使用步骤小结参考资料 Mutex 互斥锁(Mutex)是线程间实现资源同步的重要机制之一。 概念 Mutex stands for “Mutual Exclusion”. In reality, a mutex is a specialized ...

    Mutex

    互斥锁(Mutex)是线程间实现资源同步的重要机制之一。

    概念

    Mutex stands for “Mutual Exclusion”. In reality, a mutex is a specialized version of semaphore. Mutual exclusion (widely known as Mutex) is used in various operating systems for resource management. Many resources in a microcontroller device can be used repeatedly, but only by one thread at a time (for example communication channels, memory, and files). Mutexes are used to protect access to a shared resource. A mutex is created and then passed between the threads (they can acquire and release the mutex).

    在这里插入图片描述

    抓重点,互斥锁的特点如下:

    • 互斥锁是一种特殊的信号量。
    • 互斥锁被用于资源管理,管理那种一次只允许一个线程访问的共享资源,如通信通道,内存、文件等
    • 互斥锁被用于资源管理的本质就是保护共享资源。
    • 互斥锁被创建后,在线程之间传递;线程可以获取或者释放互斥锁

    A mutex is a special version of a semaphore. Like the semaphore, it is a container for tokens. But instead of being able to have multiple tokens, a mutex can only carry one (representing the resource). Thus, a mutex token is binary and bounded, i.e. it is either available, or blocked by a owning thread. The advantage of a mutex is that it introduces thread ownership. When a thread acquires a mutex and becomes its owner, subsequent mutex acquires from that thread will succeed immediately without any latency (if osMutexRecursive is specified). Thus, mutex acquires/releases can be nested.

    在这里插入图片描述

    这里谈到,互斥锁是一种特殊的信号量。什么是信号量?参考我的另一篇博客。互斥锁本质上是一个信号量,所以它就拥有信号量的特点:

    • 互斥锁是容器,不过只能拥有一个token,即管理一种资源。
    • 互斥锁相当于一个二进制位,只有两种状态,availableblocked
    • 互斥锁引入了线程所有权的概念,如果一个线程拥有了该互斥锁(且是递归锁),那么该线程下次获取该互斥锁时将不会有任何延迟。直到该线程释放这个互斥锁,否则别的线程无法拥有该互斥锁。

    RTX互斥锁API

    类型

    • osMutexAttr_t : 互斥锁属性结构体
    • osMutexId_t : 互斥锁ID

    RTX提供了三种特殊的互斥锁:递归锁,优先级继承锁,鲁棒锁

    【递归锁】

    #define osMutexRecursive      0x00000001U    // 递归锁,自己可以循环使用
    

    The same thread can consume a mutex multiple times without locking itself. Each time the owning thread acquires the mutex the lock count is incremented. The mutex must be released multiple times as well until the lock count reaches zero. At reaching zero the mutex is actually released and can be acquired by other threads.

    // 官方案例
    #include "cmsis_os2.h"
    osMutexId_t mutex_id; 
    const osMutexAttr_t Thread_Mutex_attr = {
      "myThreadMutex",     // human readable mutex name
      osMutexRecursive,    // attr_bits
      NULL,                // memory for control block   
      0U                   // size for control block
    };
     
    // must be called from a thread context
    void UseMutexRecursively(int count) {
      osStatus_t result = osMutexAcquire(mutex_id, osWaitForever);  // lock count is incremented, might fail when lock count is depleted
      if (result == osOK) {
        if (count < 10) {
          UseMutexRecursively(count + 1);
        }
        osMutexRelease(mutex_id); // lock count is decremented, actually releases the mutex on lock count zero
      }
    }
    

    【优先级继承锁】

    #define osMutexPrioInherit    0x00000002U    // 继承锁  继承等待线程中更高的优先级
    

    A mutex using priority inheritance protocol transfers a waiting threads priority to the current mutex owner if the owners thread priority is lower. This assures that a low priority thread does not block a high priority thread.

    优先级继承锁使得拥有该锁的线程可以继承等待线程中优先级比它高的线程的优先级(中文好绕~),这样做目的是防止低优先级的线程锁住高优先级的线程,避免了优先级反转现象。

    // 官方案例
    #include "cmsis_os2.h"
     
    osMutexId_t mutex_id;  
     
    const osMutexAttr_t Thread_Mutex_attr = {
      "myThreadMutex",     // human readable mutex name
      osMutexPrioInherit,  // attr_bits
      NULL,                // memory for control block   
      0U                   // size for control block
    };
     
    void HighPrioThread(void *argument) {
      osDelay(1000U); // wait 1s until start actual work
      while(1) {
        osMutexAcquire(mutex_id, osWaitForever); // try to acquire mutex
        // do stuff
        osMutexRelease(mutex_id);
      }
    }
     
    void MidPrioThread(void *argument) {
      osDelay(1000U); // wait 1s until start actual work
      while(1) {
        // do non blocking stuff
      }
    }
     
    void LowPrioThread(void *argument) {
      while(1) {
        osMutexAcquire(mutex_id, osWaitForever);
        osDelay(5000U); // block mutex for 5s
        osMutexRelease(mutex_id);
        osDelay(5000U); // sleep for 5s
      }
    }
    

    【鲁棒锁】

    #define osMutexRobust         0x00000008U    // 鲁棒锁  线程结束,则自动释放互斥锁
    

    Robust mutexes are automatically released if the owning thread is terminated (either by osThreadExit or osThreadTerminate). Non-robust mutexes are not released and the user must assure mutex release manually.

    如果一个线程拥有鲁棒锁,那么该线程结束后,会自动释放锁,避免了资源不可访问现象发生。

    // 官方案例
    #include "cmsis_os2.h"
     
    osMutexId_t mutex_id;
     
    const osMutexAttr_t Thread_Mutex_attr = {
      "myThreadMutex",     // human readable mutex name
      osMutexRobust,       // attr_bits
      NULL,                // memory for control block   
      0U                   // size for control block
    };
     
    void Thread(void *argument) {
      osMutexAcquire(mutex_id, osWaitForever);
      osThreadExit();
    }
    

    函数

    在这里插入图片描述

    osMutexNew

    osMutexId_t osMutexNew(const osMutexAttr_t* attr)
    // 输入
    * 互斥锁属性结构体指针,默认为NULL
    // 输出
    * 互斥锁ID
    

    osMutexGetName

    const char* osMutexGetName(osMutexId_t mutex_id)
    // 输入
    * mutex_id : 互斥锁ID
    // 输出
    * 互斥锁名字
    

    osMutexAcquire

    osStatus_t osMutexAcquire(osMutexId_t mutex_id, uint32_t timeout)
    // 输入
    * mutex_id : 互斥锁ID
    * timeout : 超时设定
    // 输出
    * 函数执行状态码:osOK、osErrorTimeout、osErrorResource、osErrorParameter、osErrorISR
    

    osMutexRelease

    osStatus_t osMutexRelease(osMutexId_t mutex_id)
    // 输入
    * mutex_id : 互斥锁ID
    // 输出
    * 函数执行状态码:osOK、osErrorResource、osErrorParameter、osErrorISR
    

    osMutexGetOwner

    osThreadId_t osMutexGetOwner(osMutexId_t mutex_id)
    // 输入
    * mutex_id: 互斥锁ID
    // 输出
    * 线程ID
    

    osMutexDelete

    osStatus_t osMutexDelete(osMutexId_t mutex_id)
    // 输入
    * mutex_id : 互斥锁ID
    // 输出
    * 函数执行状态码: osOK、osErrorResource、osErrorParameter、osErrorISR
    

    互斥锁使用步骤

    1.声明互斥锁属性结构体

    static const osMutexAttr_t mutex_attr = {
       .name = "mutex_1",
    };
    

    属性结构体中可以声明为三种特殊锁之一。

    在这里插入图片描述

    2.创建互斥锁句柄

    osMutexId_t mutex_id;
    

    3.创建互斥锁

    mutex_id = osMutexNew(&mutex_attr);
    

    4.释放互斥锁

    osMutexRelease(mutex_id)
    

    其实,在前面的官方案例中,使用步骤已经很明确地给出啦~

    小结

    本文简单介绍了RTX中互斥锁这一资源管理对象,重点如下:

    • 互斥锁的概念
    • 互斥锁的实现原理,三种特殊锁,函数接口等
    • 互斥锁的使用步骤

    参考资料

    ☞官方资料:互斥锁

    展开全文
  • 锁是一个常见的同步概念,我们都听说过加锁(lock)或者解锁(unlock...也便是常说的互斥锁。 尽管名称不含lock,但是称之为锁,也是没有太大问题的。mutex无疑是最常见的多线程同步方式。其思想简单粗暴,多线程共享一
  • Linux互斥锁

    2021-11-10 20:07:56
    Linux互斥锁信号量与互斥锁的比较信号量互斥锁自旋锁与互斥锁的比较自旋锁互斥锁互斥锁相关API定义互斥锁初始化互斥锁互斥锁加锁尝试获取互斥锁互斥锁解锁 信号量与互斥锁的比较 是同步还是独占? 信号量 强调信号...
  • python同步,互斥锁,死锁

    千次阅读 2019-04-12 20:20:44
    互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。 threading模块中定义了Lock类,可以方便的处理锁定: # 创建锁 mutex = threading.Lock() # 锁定 mutex.acquire() # 释放 ...
  • QMutexLocker构造函数接受一个互斥量作为参数并将其锁定,QMutexLocker的析构函数则将此互斥量解锁,所以在QMutexLocker实例变量的生存期内的代码段得到保护,自动进行互斥量的锁定和解锁。 #ifndef MYTHREAD_H #...
  • Linux 互斥锁详解
  • Redis 增加互斥锁

    2021-10-22 15:20:23
      缓存击穿后,多个线程会同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁锁住它。   其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的...
  • 1、互斥锁和自旋锁 基础的两种就是互斥锁和自旋锁,有很多⾼级的锁都是基于它们实现的,加锁的目的就是保证共享资源在任意时间⾥,只有⼀个线程访问,这样就可以避免多线程导致共享数据错乱的问题。 当一个线程加锁...
  • 总结: 》》表明使用的互斥锁已经已经将资源number给锁住,thread1使用func1中的资源number时,thread2阻塞,等thread1将func1中的number资源使用完之后,thread2才使用func1中的number资源;同理,func2中的number...
  • 互斥锁(互斥量):允许程序员锁住某个对象,使得的每次只有一个线程访问它。 为了控制对关键代码的访问,必须在进入这段代码之前锁住一个互斥量,在完成关键代码执行后解锁。 (二)互斥锁的调用接口 头文件:...
  • 1.基本概念读写锁:是计算机程序的...因为只是读的话并不需要互斥锁锁住数据,只有写操作的时候需要互斥锁,而读写结合的时候,也是需要加锁的,不然的话会导致读的数据不一定是期望的。对于RWMutex的规则如下:...
  • 本文不对自旋锁和互斥锁的概念做阐述,重点分析它们之间的区别和自旋锁的使用场景。 自旋锁和互斥锁的区别 a. 互斥锁加锁失败后,线程会释放 CPU,给其他线程;... 如果我们明确知道被锁住的代码的执行时间很短,那
  • 最常用的就是互斥锁,当然还有很多种不同的锁,比如自旋锁、读写锁、乐观锁等,不同种类的锁自然适用于不同的场景。 如果选择了错误的锁,那么在一些高并发的场景下,可能会降低系统的性能,这样用户体验就会非常差...
  • 条件变量和互斥锁

    2021-01-06 17:20:00
    一直都有一个问题,就是条件变量为什么要和互斥锁一起使用,今天看了一篇文章,并结合APUE这本书,知道了其中的原因。 函数pthread_cond_wait()有几步操作:1。判断条件2.如果条件满足,继续执行;如果条件不满足,...
  • 互斥锁作用的理解

    万次阅读 多人点赞 2018-08-13 00:47:30
    在学习线程控制的时候,接触到了互斥锁这个概念,下面讲讲我了解到的互斥锁的作用 互斥锁的创建 1.pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; 2.pthread_mutex_t mutex; pthread_mutex_init(&...
  • 什么是同步?什么是互斥? 什么是异步执行?为什么需要代码异步执行? 什么是信号量?什么互斥锁什么是自旋锁? 互斥锁和信号量的区别是什么互斥锁和自旋锁的区别是什么
  • 6.互斥锁产生死锁

    2021-08-21 23:53:38
    1.假如程序当中有一个执行流因为结束了当前线程而没有进行解锁操作,由于他没有进行解锁操作,就会使其他想要获取互斥锁的线程进行阻塞,从而产生死锁 2.当程序中有多个互斥锁存在的时候,两个或者多个已经上锁的...
  • LINUX 互斥锁 学习

    2021-01-07 10:38:20
    每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。 使用互斥锁(互斥)可以使线程按顺序执行。通常,互斥锁通过确保一次只有一个线程执行代码的临界段来同步多...
  • 自旋锁和互斥锁的区别

    千次阅读 2020-09-17 08:28:10
    互斥锁是一种独占锁,当线程A加锁成功后,此时互斥锁已经被线程A独占了,只要线程A没有释放手中的锁,线程B就会失败,就会释放掉CPU给其他线程,线程B加锁的代码就会被阻塞。 互斥锁加锁失败而阻塞是由操作系统内核...
  • Java——互斥锁

    万次阅读 2019-04-02 20:07:53
    * 互斥锁 * 当使用synchroinzed锁住多段不同的代码片段, * 但是这些同步块使用的同步监视器对象是同一个时,那么这些代码 * 片段之间就是互斥的。多个线程不能同时执行他们。 * @author Administrator * */ ...
  • Linux线程-互斥锁

    2021-09-03 16:15:32
    一、什么互斥锁 互斥锁是另一种用于多线程中的同步访问方法,它允许程序锁住某个对象,使得每次只能有一个线程访问它。为了控制对关键代码的访问,必须在进入这段代码之前锁住一个互斥量,然后在完成操作之后
  • 互斥锁与读写锁

    千次阅读 2019-05-21 15:28:12
    1、互斥锁 锁是一种很形象的说法:就像一个房间只能一个人,任何人进去之后就把门锁上了,其他人都不可以进去,知道进去的人重新解锁,既是释放了这个锁资源为止。对于互斥锁的操作无非就是:定义互斥锁变量,初始...
  • 这篇文章介绍Linux下线程同步与互斥机制--互斥锁,在多线程并发的时候,都会出现多个消费者取数据的情况,这种时候数据都需要进行保护,比如: 火车票售票系统、汽车票售票系统一样,总票数是固定的,但是购票的终端...
  • 可重入锁(递归锁) & 互斥锁属性设置

    千次阅读 2018-10-10 19:21:51
    前言: 上一次刷博客的时候,看到了自旋锁,通过学习Linux内核,对自旋锁有了一定的了解。...最常见的进程/线程的同步方法有互斥锁(或称互斥量Mutex),读写锁(rdlock),条件变量(cond),信号量(Semophore)等。...
  • 自旋锁替代互斥锁使用场景

    千次阅读 2018-11-22 16:47:45
    自旋锁替代互斥锁使用场景
  • 互斥锁用来锁住同一个资源,每次仅允许一个线程加锁(lock)锁定某个资源,其他线程等待,当线程解锁(unlock),才允许其他线程获取锁。 java中最常见的两种锁:synchronized 和 ReentrantLock 这里考虑优先使用...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 42,277
精华内容 16,910
关键字:

互斥锁锁住了什么

友情链接: PT6311-A-C.rar