linux锁的介绍

3 篇文章 0 订阅
2 篇文章 0 订阅

锁的类型

互斥量(Mutex)  互斥量


是实现最简单的锁类型。如果在释放操作执行前发生定时器超时,则互斥量也会释放代码块或共享存储区供其他线程访问。当有异常发生时,可使用try-finally语句来确保互斥量被释放。定时器状态或try-finally语句的使用可以避免产生死锁。

对互斥量进行加锁以后,其他试图再次对互斥量枷锁的线程将会被阻塞,知道当前线程释放该互斥锁。在多线程的情况下,每次只有一个线程可以执行。


如果试图对一个互斥量加锁两次,那么它自身就会陷入死锁状态。如果两个线程都在互相请求另一个线程的资源,那么这两个线程都无法向前运行,于是就产生了死锁。如果线程要同时对两个互斥量进行加锁,先锁A,再锁B,但是如果有其它的线程以另一个相反的顺序进行加锁,就有可能出现死锁。

总体来讲, 有几个不成文的基本原则:

对共享资源操作前一定要获得锁.
完成操作以后一定要释放锁.
尽量短时间地占用锁.
如果有多锁, 如获得顺序是ABC连环扣, 释放顺序也应该是ABC.
线程错误返回时应该释放它所获得的锁.

int pthread_mutex_lock(pthread_mutex_t*mutex);
int pthread_mutex_trylock(pthread_mutex_t*mutex);
返回值: 成功则返回0, 出错则返回错误编号.
说明: 具体说一下trylock函数, 这个函数是非阻塞调用模式, 也就是说, 如果互斥量没被锁住,trylock函数将把互斥量加锁, 并获得对共享资源的访问权限; 如果互斥量被锁住了,trylock函数将不会阻塞等待而直接返回EBUSY, 表示共享资源处于忙状态

自旋锁 


自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,即在标志寄存器中关闭/打开中断标志位,不需要自旋锁)。


自旋就是自己连续的循环等待。如果你有抱着你的爱人旋转的经历,那么你应该知道一件事情,为了安全,你不能旋转太久,你的爱人如果头昏,也想你早日释放。是的,自旋的缺点,就是它频繁的循环直到等待锁的释放,将它用于可以快速完成的代码中才好。


事实上,自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话,最好使用信号量。
1自旋锁实际上是忙等锁

  当锁不可用时,CPU一直循环执行“测试并设置”该锁直到可用而取得该锁,CPU在等待自旋锁时不做任何有用的工作,仅仅是等待。因此,只有在占用锁的时间极短的情况下,使用自旋锁才是合理的。当临界区很大或有共享设备的时候,需要较长时间占用锁,使用自旋锁会降低系统的性能。
2 自旋锁可能导致系统死锁

  引发这个问题最常见的情况是递归使用一个自旋锁,即如果一个已经拥有某个自旋锁的CPU想第二次获得这个自旋锁,则该CPU 将死锁。此外,如果进程获得自旋锁之后再阻塞,也有可能导致死锁的发生。copy_from_user()、copy_to_user()和 kmalloc()等函数都有可能引起阻塞,因此在自旋锁的占用期间不能调用这些函数。代码清单7.2给出了自旋锁的使用实例,它被用于实现使得设备只能被最多一个进程打开。

3.自旋锁使用注意事项
由于自旋锁最主要特性是不断反复循环测试,注定下列场景需注意:
1.临界区代码不要存在睡眠情况(主要因为发生睡眠不可预知睡眠多长时间,另外长时间睡眠,导致即将进入临界区其他线程,长时间得不到自旋锁,无休止自旋,从而导致死锁),所以临界区调用导致睡眠函数,不能选择自旋锁。
2.保证进入临界区的线程,不发生内核抢占。(这一点不必担心,持有自旋锁情况,Linux内核不进行抢占)
3.临界区代码,执行时间不能太长。(因为其他线程,如果要进入话,导致自旋,过多消耗CPU资源)
4.选择自旋锁时,也要注意中断情况(上半部分中断(硬件中断)和下半部分中断(软中断),中断会抢占即中断到来时,打断目前临界区代码执行,转往执行中断代码),当中断要进入自旋锁保护临界区代码时,将导致线程与中断发生死锁可能。被自旋锁保护的临界区代码执行时是不能被被其他中断中断。

从这几个特性可以归纳出一个共性:被自旋锁保护的临界区代码执行时,它不能因为任何原因放弃处理器,中断,睡眠等都可能放弃CPU。

==========================================================================

自旋锁最多只能被一个可执行线程持有。自旋锁不会引起调用者睡眠,如果一个执行线程试图获得一个已被持有的自旋锁,那么线程就会一直进行忙循环,一直等待下去,在那里看是否该自旋锁的保持者已释放了锁,"自旋"一词就是因此而得名。
由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
信号量和读写信号量适合于保持时间较长的情况,他们会导致调用者睡眠,因此只能在进程上下文使用(_trylock的变种能够在中断上下文使用);而自旋锁适合于保持时间非常短的情况,因为一个被争用的自旋锁使得请求他的线程在等待重新可用时自旋,特别浪费处理时间,这是自旋锁的要害之处,所以自旋锁不应该被长时间持有。在实际应用中自旋锁代码只有几行,而持有自旋锁的时间也一般不会超过两次上下方转换,因线程一旦要进行转换,就至少花费切出切入两次,自旋锁的占用时间如果远远长于两次上下文转换,我们就能让线程睡眠,这就失去了设计自旋锁的意义。
如果被保护的共享资源只在进程上下文访问,使用信号量保护该共享资源非常合适,如果对共享资源的访问时间非常短,自旋锁也能。不过如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。
自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是能被抢占的。自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。
一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有所有执行单元保持该锁,那么将即时得到锁;如果在获取自旋锁时锁已有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁。
无论是互斥锁,还是自旋锁,在所有时刻,最多只能有一个保持者,也就说,在所有时刻最多只能有一个执行单元获得锁。自旋锁的实现和体系结构密切相关,代码一般通过汇编实现,定义在文件,实际用到的接口定义在目录中

读写锁

一 综述
    在一些程序中存在读者写者问题,也就是说,对某些资源的访问会  存在两种可能的情况,一种是访问必须是排它行的,就是独占的意思,这称作写操作;另一种情况就是访问方式可以是共享的,就是说可以有多个线程同时去访问某个资源,这种就称作读操作。这个问题模型是从对文件的读写操作中引申出来的。
    读写锁比起mutex具有更高的适用性,具有更高的并行性,可以有多个线程同时占用读模式的读写锁,但是只能有一个线程占用写模式的读写锁,读写锁的三种状态:
1.当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞
2.当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是以写模式对它进行加锁的线程将会被阻塞
3.当读写锁在读模式的锁状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁的请求,这样可以避免读模式锁长期占用,而等待的写模式锁请求则长期阻塞。
    读写锁最适用于对数据结构的读操作次数多于写操作的场合,因为,读模式锁定时可以共享,而写模式锁定时只能某个线程独占资源,因而,读写锁也可以叫做个共享-独占锁。
    处理读者-写者问题的两种常见策略是强读者同步(strong reader synchronization)和强写者同步(strong writer synchronization).    在强读者同步中,总是给读者更高的优先权,只要写者当前没有进行写操作,读者就可以获得访问权限;而在强写者同步中,则往往将优先权交付给写者,而读者只能等到所有正在等待的或者是正在执行的写者结束以后才能执行。关于读者-写者模型中,由于读者往往会要求查看最新的信息记录,所以航班订票系统往往会使用强写者同步策略,而图书馆查阅系统则采用强读者同步策略。
    读写锁机制是由posix提供的,如果写者没有持有读写锁,那么所有的读者多可以持有这把锁,而一旦有某个写者阻塞在上锁的时候,那么就由posix系统来决定是否允许读者获取该锁。
二 读写锁相关的API
1.初始化和销毁读写锁
    对于读写锁变量的初始化可以有两种方式,一种是通过给一个静态分配的读写锁赋予常值PTHREAD_RWLOCK_INITIALIZER来初始化它,另一种方法就是通过调用pthread_rwlock_init()来动态的初始化。而当某个线程不再需要读写锁的时候,可以通过调用 pthread_rwlock_destroy来销毁该锁。函数原型如下:
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *rwptr, const pthread_rwlockattr_t *attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwptr);
这两个函数如果执行成功均返回0,如果出错则返回错误码。
在释放某个读写锁占用的内存之前,要先通过pthread_rwlock_destroy对读写锁进行清理,释放由pthread_rwlock_init所分配的资源。
在初始化某个读写锁的时候,如果属性指针attr是个空指针的话,表示默认的属性;如果想要使用非默认属性,则要使用到下面的两个函数:
#include <pthread.h>
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockatttr_t *attr);
这两个函数同样的,如果执行成功返回0,失败返回错误码。
这里还需要说明的是,当初始化读写锁完毕以后呢,该锁就处于一个非锁定状态。
数据类型为 pthread_rwlockattr_t的某个属性对象一旦初始化了,就可以通过不同的函数调用来启用或者是禁用某个特定的属性。
2.获取和释放读写锁
读写锁的数据类型是pthread_rwlock_t,如果这个数据类型中的某个变量是静态分配的,那么可以通过给它赋予常值 PTHREAD_RWLOCK_INITIALIZAR来初始化它。pthread_rwlock_rdlock()用来获取读出锁,如果相应的读出锁已经被某个写入者占有,那么就阻塞调用线程。pthread_rwlock_wrlock()用来获取一个写入锁,如果相应的写入锁已经被其它写入者或者一个或多个读出者占有,那么就阻塞该调用线程;pthread_rwlock_unlock()用来释放一个读出或者写入锁。函数原型如下:
#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_unlock(pthread_rwlock_t *rwptr);
这三个函数若调用成功则返回0,失败就返回错误码。要注意的是其中获取锁的两个函数的操作都是阻塞操作,也就是说获取不到锁的话,那么调用线程不是立即返回,而是阻塞执行。有写情况下,这种阻塞式的获取所得方式可能不是很适用,所以,接下来引入两个采用非阻塞方式获取读写锁的函数pthread_rwlock_tryrdlock()和 pthread_rwlock_trywrlock(),非阻塞方式下获取锁的时候,如果不能马上获取到,就会立即返回一个EBUSY错误,而不是把调用线程投入到睡眠等待。函数原型如下:
#include <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwptr);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwptr);
同样地,这两个函数调用成功返回0,失败返回错误码。
三 实例
读者-写者模型来实现多线程同步问题(原文http://hi.baidu.com/tekuba/item/d67e82ce52444522e90f2ee8)
https://github.com/helianthuslulu/LINUX_IPC

条件锁


LINUX 环境下多线程编程肯定会遇到需要条件变量的情况,此时必然要使用pthread_cond_wait()函数。但这个函数的执行过程比较难于理解。
    pthread_cond_wait() 的工作流程如下(以MAN中的EXAMPLE为例):
       Consider two shared variables x and y, protected by the mutex mut, and a condition vari-
       able cond that is to be signaled whenever x becomes greater than y.

              int x,y;
              pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
              pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

       Waiting until x is greater than y is performed as follows:

              pthread_mutex_lock(&mut);
              while (x <= y) {
                      pthread_cond_wait(&cond, &mut);
              }
             
              pthread_mutex_unlock(&mut);

       Modifications on x and y that may cause x to become greater than y should signal the con-
       dition if needed:

              pthread_mutex_lock(&mut);
             
              if (x > y) pthread_cond_broadcast(&cond);
              pthread_mutex_unlock(&mut);

     这个例子的意思是,两个线程要修改X和 Y的值,第一个线程当X<=Y时就挂起,直到X>Y时才继续执行(由第二个线程可能会修改X,Y的值,当X>Y时唤醒第一个线程),即 首先初始化一个普通互斥量mut和一个条件变量cond。之后分别在两个线程中分别执行如下函数体:
              pthread_mutex_lock(&mut);
              while (x <= y) {
                      pthread_cond_wait(&cond, &mut);
              }
             pthread_mutex_unlock(&mut);

和:       

              pthread_mutex_lock(&mut)       
              if (x > y) pthread_cond_signal(&cond);
              pthread_mutex_unlock(&mut);
    其实函数的执行过程非常简单,在第一个线程执行到pthread_cond_wait(&cond,&mut)时,此时如果 X<=Y,则此函数就将mut互斥量解锁 ,再将cond条件变量加锁 ,此时第一个线程挂起 (不占用任何CPU周期)。
    而在第二个线程中,本来因为mut被第一个线程锁住而阻塞,此时因为mut已经释放,所以可以获得锁mut,并且进行修改X和Y的值,在修改之后,一个IF 语句判定是不是X>Y,如果是,则此时pthread_cond_signal()函数会唤醒第一个线程 ,并在下一句中释放互斥量mut。然后第一个线程开始从 pthread_cond_wait()执行,首先要再次锁mut , 如果锁成功,再进行条件的判断 (至于为什么用WHILE,即在被唤醒之后还要再判断,后面有原因分析),如果满足条件,则被唤醒 进行处理,最后释放互斥量mut 。

    至于为什么在被唤醒之后还要再次进行条件判断(即为什么要使用while循环来判断条件),是因为可能有“惊群效应”。有人觉得此处既然是被唤醒的,肯定 是满足条件了,其实不然。如果是多个线程都在等待这个条件,而同时只能有一个线程进行处理,此时就必须要再次条件判断,以使只有一个线程进入临界区处理。 对此,转来一段:


从上文可以看出:
1,pthread_cond_signal 在多处理器上可能同时唤醒多个线程,当你只能让一个线程处理某个任务时,其它被唤醒的线程就需要继续 wait,while循环的意义就体现在这里了,而且规范要求pthread_cond_signal至少唤醒一个pthread_cond_wait上 的线程,其实有些实现为了简单在单处理器上也会唤醒多个线程.
2,某些应用,如线程池,pthread_cond_broadcast唤醒全部线程,但我们通常只需要一部分线程去做执行任务,所以其它的线程需要继续wait.所以强烈推荐此处使用while循环.

       其实说白了很简单,就是pthread_cond_signal()也可能唤醒多个线程,而如果你同时只允许一个线程访问的话,就必须要使用while来进行条件判断,以保证临界区内只有一个线程在处理。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t count_lock;
pthread_cond_t count_nonzero;

unsigned count = 0;

void * decrement_count(void *arg) 
{
    pthread_mutex_lock (&count_lock);
    printf("decrement_count get count_lock\n");
    while(count==0) {
        printf("decrement_count count == 0 \n");
        printf("decrement_count before cond_wait \n");
        pthread_cond_wait( &count_nonzero, &count_lock);   //阻塞
        printf("decrement_count after cond_wait \n");      //等待increment_count函数的pthread_cond_signal完后再执行
   }

    count = count -1;
    pthread_mutex_unlock (&count_lock);
}

void * increment_count(void *arg)
{
    pthread_mutex_lock(&count_lock);
    printf("increment_count get count_lock\n");

    if(count==0) {

        printf("increment_count before cond_signal\n");
    
        pthread_cond_signal(&count_nonzero);    //发送信号唤起其它阻塞的线程,唤起decrement_count函数pthread_cond_wait后语句的执行
    
        printf("increment_count after cond_signal\n");

    }

    count=count+1;
    pthread_mutex_unlock(&count_lock);
}

 int main(void)
{
    pthread_t tid1,tid2;
    pthread_mutex_init(&count_lock,NULL);
    pthread_cond_init(&count_nonzero,NULL);
    pthread_create(&tid1,NULL,decrement_count,NULL);
    sleep(2);
    pthread_create(&tid2,NULL,increment_count,NULL);
    sleep(10);
    pthread_exit(0);
}


编译

gcc -g tiaojiansuo.c -o tiaojiansuo -lpthread
打印结果:
decrement_count get count_lock
decrement_count count == 0
decrement_count before cond_wait
increment_count get count_lock

increment_count before cond_signal

increment_count after cond_signal

decrement_count after cond_wait

信号量


一、什么是信号量
线程的信号量与进程间通信中使用的信号量的概念是一样,它是一种特殊的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作。如果一个程序中有多个线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行。

而只有0和1 两种取值的信号量叫做二进制信号量,在这里将重点介绍。而信号量一般常用于保护一段代码,使其每次只被一个执行线程运行。我们可以使用二进制信号量来完成这个工作。

二、信号量的接口和使用

信号量的函数都以sem_开头,线程中使用的基本信号量函数有4个,它们都声明在头文件semaphore.h中。

1、sem_init函数
该函数用于创建信号量,其原型如下:

   1. int sem_init(sem_t *sem, int pshared, unsigned int value);  

int sem_init(sem_t *sem, int pshared, unsigned int value);

该函数初始化由sem指向的信号对象,设置它的共享选项,并给它一个初始的整数值。pshared控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则信号量就可以在多个进程之间共享,value为sem的初始值。调用成功时返回0,失败返回-1.

2、sem_wait函数
该函数用于以原子操作的方式将信号量的值减1。原子操作就是,如果两个线程企图同时给一个信号量加1或减1,它们之间不会互相干扰。它的原型如下:

   1. int sem_wait(sem_t *sem);  

int sem_wait(sem_t *sem);

sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.

3、sem_post函数
该函数用于以原子操作的方式将信号量的值加1。它的原型如下:

   1. int sem_post(sem_t *sem);  

int sem_post(sem_t *sem);

与 sem_wait一样,sem指向的对象是由sem_init调用初始化的信号量。调用成功时返回0,失败返回-1.

4、sem_destroy函数
该函数用于对用完的信号量的清理。它的原型如下:

   1. int sem_destroy(sem_t *sem);  

int sem_destroy(sem_t *sem);

成功时返回 0,失败时返回-1.



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值