浅析线程的同步与互斥机制

时间:2022-02-02 18:06:45

  进程或者线程间的关系主要是两种:互斥和同步,那仫这两种机制有什仫特点呢?

  一.什仫是互斥与同步?

    (1).互斥

   互斥就是指某一资源同时只能允许一个访问者对其进行访问,具有唯一性和排他性,但是互斥无法限制访问者对资源的访问顺序,即访问时无序的。互斥决定了一个进程或者是线程是否可以获得资源的使用权

    (2).同步

    同步是指在互斥的基础上(大多数情况下互斥和同步是一起使用的,少部分情况下可以只使用同步机制),通过其他机制实现访问者对资源的有序访问。

 二.线程间的互斥和同步是如何实现的?

    (1).Linux中锁的实现主要有四种机制:二元信号量,互斥锁,读写锁,以及自旋锁

   什仫是互斥锁?互斥锁是如何实现的?

    2.1.创建锁资源就像创建变量一样,锁的类型是pthread_mutex_t。因为线程是共享数据区和堆区的,所以我们可以创建全局或静态的锁变量,这样就可以使得所有线程都可以看见锁,所以说实现线程间通信是非常简单的。

   2.2.初始化互斥锁

       1).如果创建锁资源是全局或者是静态锁的话可以使用宏PTHREAD_MUTEX_INITIALIZER来初始化;

pthread_mutex_t mylock=PTHREAD_MUTEX_INITIALIZER; 

       2).也可以使用函数来进行互斥锁的初始化

  int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

      返回值:成功返回0,失败返回错误码。

      mutex表示要进行初始化的互斥锁;attr表示锁的属性,为NULL就相当于使用宏来进行初始化。

   2.3.既然后创建锁那仫必然会有销毁锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

      返回值:成功返回0,失败返回错误码。

   2.4.加锁,互斥锁的加锁有两种机制

int pthread_mutex_lock(pthread_mutex_t *mutex);

      返回值:成功返回0,失败返回错误码。

     一个线程可以调用pthread_mutex_lock获得锁,如果这时另一个线程先获得锁,则当前线程需要挂起等待,直到另一个线程调用pthread_mutex_unlock解锁,当前线程被唤醒,才能获得锁,并继续执行。可以看到,互斥锁就像轻量级的二元信号量一样,只能用在线程间,而信号量既可以用在线程间,也可以用在进程间。

int pthread_mutex_trylock(pthread_mutex_t *mutex);

      返回值:成功返回0,失败返回错误码。

      如果一个线程既想获得锁,获得锁失败之后又不想挂起等待,那仫就可以使用pthread_mutex_trylock进行加锁,这个函数失败的话会返回一个EBUSY,而不会使得线程挂起等待。

  2.5.解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

     返回值:成功返回0,失败返回错误码。

     lock和unlock的基本原理:

     假设mutex的值为1表示互斥锁空闲,此时没有其他线程调用互斥锁,可以进行获得锁操作。mutex的值为0表示互斥锁已经被其他线程所申请到,此时这个线程只能挂起等待。

     可以发现unlock是原子操作,unlock可以一次唤醒一个线程,也可以一次唤醒任意多个线程,CPU会通过一定的进程调度算法选择适当的线程来获得锁(这里的锁就相当于临界资源),其他未被选中的线程会继续挂起等待下一轮的调度。

     浅析线程的同步与互斥机制

     为了实现互斥,大多数体系结构都提供了swap或xchange指令,可以把寄存器和内存单元的数据交换。由于只有一条指令,保证了原子性,即使是多处理器的平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令指能等待总线周期。所以lock和unlock的伪代码可以改为下面的形式: 

      浅析线程的同步与互斥机制

     使用swap或xchange指令保证了数据的原子性和一致性,不会有重复的数据存在。使用第一种可能会产生死锁的现象,主要是因为每个线程的栈空间是私有的,当该线程被切换出去的时候它会对内存中的数据在私有栈空间中重新保存一份,新的数据尚未返回到内存中区,下一个线程又会重新从内存中读取锁资源的数据,所以导致了数据的不一致性。

    什仫是读写锁?什仫是自旋锁?

   在一些程序中存在读者写者问题,也就是说,对某些资源的访问会存在两种可能的情况:一种是访问必须是排它的,就是独占的意思,这称作写操作;另一种情况就是访问方式可以是共享的,就是说可以有多个线程同时去访问某个资源,这种就称作读操作。
   通常而言,在读的过程中,往往伴随着查找操作,中间耗时很长,给这段代码加锁的话会极大的降低我们的效率。针对这种多读少写的情况,我们通常采用读写锁。 读写锁是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行访问,写者只对共享资源进行写操作,一个读写锁同时只能有一个写者或多个读者,但是不能同时既有写者又有读者。 读写锁比起mutex具有更高的适用性,具有更高的并行性,可以有多个线程同时占用读模式的读写锁,但是只能有一个线程占用写模式的读写锁。

    2.6.读写锁的创建和初始化

    创建读写锁就像创建变量一样,可以是pthread_rwlock_t myrwlock;

 int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);

     返回值:成功返回0,失败返回错误码。

     attr参数表示读写锁的属性,为NULL表示默认

    2.7.既然有创建读写锁那仫当然有销毁锁了

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

     返回值:成功返回0,失败返回错误码。

   2.8.读者加锁  

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

      返回值:成功返回0,失败返回错误码。

      功能:对读者进行加锁,加锁失败的话会挂起等待。

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

      返回值:成功返回0,失败返回错误码。

      功能:对读者进行加锁,加锁失败不会挂起等待。

   2.9.写者加锁

 int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

      返回值:成功返回0,失败返回错误码。

      功能:对写者进行加锁,加锁失败的话会挂起等待。

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

       返回值:成功返回0,失败返回错误码。

       功能:对写者进行加锁,加锁失败的话不会挂起等待。

   3.0.解锁

      不论是读者加锁还是写着加锁,最后解锁的操作都是pthread_rwlock_unlock

 int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

      了解了读写锁,那么什仫是自旋锁呢?自旋锁是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。 其作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁

    自旋锁的缺点:
    1、自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,如果要等待时间的很长,CPU的效率就会降低;
    2、在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等。因此我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。

    (2).Linux下同步的实现主要有两种机制:条件变量和信号量

 信号量:信号量则可以表示多个可用资源的数量。信号量也是一种无锁同步机制。

    3.1、创建信号量就像创建变量一样,信号量的类型是sem_t,要使用信号量的话要包含semaphore.h头文件;

   3.2、初始化 

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

     返回值:成功返回0,失败返回错误码。 

     pshared:pshared参数为0表示信号量用于同一进程的线程间同步。 
     value:可用资源的数量。

   3.3、获取信号量 

int sem_wait(sem_t *sem);

    返回值:成功返回0,失败返回错去码。

    功能:获取资源,相当于P操作,信号量的值减1 。失败后会挂起等待。 

int sem_trywait(sem_t *sem); 


    返回值:成功返回0,失败返回错去码。

    功能:尝试获取资源,相当于P操作,信号量的值减1 。失败后不会挂起。

    3.4、释放信号量 

int sem_post(sem_t *sem); 

 
    返回值:成功返回0,失败返回错去码。

    功能:释放资源,相当于V操作,信号量的值加1。同时会唤醒挂起等待的进程。

   3.5、销毁信号量 

int sem_destroy(sem_t *sem); 

 
    返回值:成功返回0,失败返回错去码。

   条件变量: 
   线程间同步还有这样一种情况,线程A需要等待某个条件成立才能继续向下执行,现在这个条件不成立,线程A就被阻塞等待,而线程B在执行的过程中使这个条件成立了,就唤醒线程A继续执行。在pthread库中通过条件变量(Condition Variable)来阻塞等待一个条件,或则唤醒等待这个条件的线程.下面介绍的条件变量是POSIX标准制定的,所以在使用的时候要引入头文件pthread.h。

    3.6.条件变量的创建: 
     条件变量的创建和普通变量一样,为了能够实现共享条件变量,所以将条件变量创建为全局或静态变量,或者在堆上创建。 

    3.7.条件变量的初始化: 
    1).如果创建成全局或静态的,则可以使用宏来初始化: 

     

pthread_cond_t cond=PTHREAD_COND_INITIALIZER; 


    2).使用函数pthread_cond_init来初始化条件变量: 

      

int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *attr); 


    返回值:成功返回0,失败返回错误码。 
    attr:条件变量的属性。

    3.8.既然有创建那仫当然有销毁了: 

     

int pthread_cond_destroy(pthread_cond_t *cond); 


    

    返回值:成功返回0,失败返回错误码。

    3.9.条件变量的阻塞等待: 

    

int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex,const struct timespace *abstime); 



    返回值:成功返回0,失败返回错误码。 

    timedwait是超时等待,其中abstime可以设定时间,如果线程在达到了abstime所指定的时刻仍然没有别的线程来唤醒当前线程,就返回ETIMEEDOUT。

    

int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex); 


    返回值:成功返回0,失败返回错误码。 

   4.0.条件变量的唤醒: 

    

int pthread_cond_broadcast(pthread_cond_t *cond); 


   返回值:成功返回0,失败返回错误码。 

   broadcast可以唤醒所有在这个条件变量上等待的线程。

   

int pthread_cond_signal(pthread_cond_t *cond); 


   返回值:成功返回0,失败返回错误码。 
   singnal可以唤醒某个在条件变量cond上等待的线程。在signal中是一定会再次进行锁资源的申请的。

  三.实现多生产者和多消费者的实例

     使用环形数组来模拟交易场所,通过信号量来实现同步,通过互斥锁实现互斥。

     

#include<stdio.h>
#include<pthread.h>
#include <semaphore.h>
#define _SIZE_ 5

int buf[_SIZE_]; //使用环形数组来模拟交易场所
sem_t blanks; //格子信号量
sem_t datas; //数据信号量

pthread_mutex_t prolock=PTHREAD_MUTEX_INITIALIZER; //生产者之间的锁
pthread_mutex_t conlock=PTHREAD_MUTEX_INITIALIZER; //消费者之间的锁

void *product_run(void *arg) //生产者关心的是否存在空格子资源
{
static int i=0;
while(1)
{
sem_wait(&blanks); //相当于P操作,格子数-1
pthread_mutex_lock(&prolock);//格子数不为0则加锁
buf[i]=rand()%100;
printf("producter is producting,i is %d,data is %d\n",i,buf[i]);
i++;
i %= _SIZE_;
sem_post(&datas); //相当于V操作,数据+1
sleep(1);
pthread_mutex_unlock(&prolock); //解锁
}
}

void *consume_run(void *arg) //消费者关心是否存在数据资源
{
static int i=0;
while(1)
{
sem_wait(&datas); //相当于P操作,数据-1
pthread_mutex_lock(&conlock); //尚有数据未消费则加锁
int data=buf[i];
printf("consumer is consuming,i is %d,data is %d\n",i,data);
i++;
i %= _SIZE_;
sem_post(&blanks); //相当于V操作,格子数+1
//sleep(1);
pthread_mutex_unlock(&conlock); //解锁
}
}

int main()
{
sem_init(&blanks,0,_SIZE_);
sem_init(&datas,0,0);
pthread_t producter1,producter2,producter3,producter4;
pthread_t consumer1,consumer2,consumer3,consumer4;

pthread_create(&producter1,NULL,product_run,NULL);
pthread_create(&producter2,NULL,product_run,NULL);
pthread_create(&producter3,NULL,product_run,NULL);
pthread_create(&producter4,NULL,product_run,NULL);
pthread_create(&consumer1,NULL,consume_run,NULL);
pthread_create(&consumer2,NULL,consume_run,NULL);
pthread_create(&consumer3,NULL,consume_run,NULL);
pthread_create(&consumer4,NULL,consume_run,NULL);

pthread_join(producter1,NULL);
pthread_join(producter2,NULL);
pthread_join(producter3,NULL);
pthread_join(producter4,NULL);
pthread_join(consumer1,NULL);
pthread_join(consumer2,NULL);
pthread_join(consumer3,NULL);
pthread_join(consumer4,NULL);
sem_destroy(&blanks);
sem_destroy(&datas);
return 0;
}

    之前也实现过使用带头结点的单链表为交易场所的单生产者和单消费者模型:

    http://blog.csdn.net/oneday_789/article/details/72799633