线程间同步与互斥

来源:互联网 发布:管家的面 淘宝 编辑:程序博客网 时间:2024/06/06 08:39

一、同步与互斥概念
进程/线程之间的关系主要有两种,同步与互斥。
互斥:
互斥就是指某一资源同时只能允许一个访问者对其进行访问,具有唯一性和排他性,但是互斥无法限制访问者对资源的访问顺序,即访问时无序的。
对于线程来说,互斥就是说两个线程之间不可以同时运行,他们之间会相互排斥,必须等一个线程运行完毕之后,另一个才能运行。
同步:
同步是指在互斥的基础上(大多数情况),通过其他机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所写入资源的情况必定是互斥的,少数情况可以是指允许多个访问者同时访问资源。
对于线程来说,同步也是不能同时运行,但是它必须按照某种次序来运行相应。也就是按照一定的顺序运行线程,这种先后次序依赖于要完成的特定的任务。显然,同步是一种更复杂的互斥,互斥是一种特殊的同步。

二、线程间同步与互斥
1、mutex(互斥量)
多个线程同时访问共享数据时可能会发生冲突。
例如:两个线程都要把某个全局变量增加1,这个操作至少需要三条指令才能完成:
1、从内存读变量值到寄存器
2、寄存器的值加1
3、寄存器的值写回内存
假设两个线程在多处理平台上同时执行这三条指令,则可能导致下图所示的结果:
这里写图片描述
即时是在单处理器上,也有可能会发生同样的错误。
这里写图片描述
发生这样错误的原因就使两个线程是互斥的,而加1操作又不是原子的。要想解决这样的错误,我们就需要引入互斥锁,要想进入临界区的线程必须先获得锁才可以,这,也就是将上面加1的操作组成一个原子操作,要么都执行,要么都不执行,执行中间也不能被打断,也不会在其他处理器上并行这个操作。
下面介绍POSIX版本的互斥锁,这些函数都位于pthread共享库中,使用的时候要加pthread.h的头文件。

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

1.2、初始化锁:
如果创建的是全局或静态的锁的话,可以用宏PTHREAD_MUTEX_INITIALIZER初始化。也可以用函数初始化。
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
返回值:成功返回0,失败返回错误码。
attr:变量表示锁的属性。为NULL的话就相当于用宏初始化。

1.3、销毁锁:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
成功返回0,失败返回错误码。

1.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,而不会使线程挂起等待。注意这个函数调用一次,只会去申请一次锁资源。

1.4、解锁:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误码。

lock和unlock的原理:
假设mutex的值为1表示互斥锁空闲,这时调用lock可以获得锁。mutex值为0表示互斥锁已经被某个线程所使用,其他线程再调用lock的话,只能挂起等待。
可以看到,unlock是原子操作,unlock可以一次唤醒一个等待线程。也可以唤醒所有等待的线程让他们去竞争,竞争失败的线程继续挂起等待。
这里写图片描述

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

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

2.1、条件变量的创建:
条件变量的创建和普通变量一样,为了能够实现共享条件变量,所以将条件变量创建为全局或静态变量,或者在堆上创建。
例:int pthread_cond_t cond;

2.2、条件变量的初始化:
条件变量和mutex是非常相似的,如果创建成全局或静态的,则可以使用宏来初始化:
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
还可以使用函数来初始化:
int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *attr);
返回值:成功返回0,失败返回错误码。
attr:条件变量的属性。

2.3、条件变量的销毁:
int pthread_cond_destroy(pthread_cond_t *cond);
返回值:成功返回0,失败返回错误码。

2.4、条件变量的阻塞等待:
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,失败返回错误码。
可以预想到,在wait中肯定是要释放锁的。

2.5、条件变量的唤醒:
int pthread_cond_broadcast(pthread_cond_t *cond);
返回值:成功返回0,失败返回错误码。
broadcast可以唤醒所有在这个cond上等待的线程。

int pthread_cond_signal(pthread_cond_t *cond);
返回值:成功返回0,失败返回错误码。
singnal可以唤醒某个在条件变量cond上等待的线程。在signal里面肯定是要再次获取锁资源的。

3、semaphore(信号量)
互斥锁变量的值非0即1,它可以看做一种资源的表示数量,初始化的时候mutex表示1,加锁获得资源的时候mutex减到0,表示不再拥有可用资源,解锁时释放该资源mutex重新加到1,表示又有一个可用资源。所以互斥锁可以看做是一个二元信号量。而信号量则可以表示多个可用资源的数量。

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

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);
功能:获取资源,相当于P操作,信号量的值减1 。失败后会挂起等待。
返回值:成功返回0,失败返回错去码。

int sem_trywait(sem_t *sem);
功能:尝试获取资源,相当于P操作,信号量的值减1 。失败后不会挂起。
返回值:成功返回0,失败返回错去码。

3.5、释放信号量
int sem_post(sem_t *sem);
功能:释放资源,相当于V操作,信号量的值加1。同时会唤醒挂起等待的进程。
返回值:成功返回0,失败返回错去码。

3.6、销毁信号量
int sem_destroy(sem_t *sem);
功能:销毁一个信号量。
返回值:成功返回0,失败返回错去码。

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

4.1、创建读写锁就像创建变量一样,它的类型是pthread_rwlock_t。
pthread_rwlock_t rwlock;

4.2、初始化:
int pthread_rwlock_init(pthread_rwlock_t *rwlock,const pthread_rwlockattr_t *attr);
返回值:成功返回0,失败返回错去码。
attr:表示读写锁的属性,为NULL表示默认。

4.3、销毁:
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
返回值:成功返回0,失败返回错去码。

4.4、读者加锁:
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
功能:对读者进行加锁,失败的话会挂起等待。
返回值:成功返回0,失败返回错去码。

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
功能:对读者进行加锁,失败的话不会挂起等待。
返回值:成功返回0,失败返回错去码。

4.5、写者加锁:
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
功能:对读者进行加锁,失败的话会挂起等待。
返回值:成功返回0,失败返回错去码。

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
功能:对读者进行加锁,失败的话不会挂起等待。
返回值:成功返回0,失败返回错去码。

4.5解锁:
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
返回值:成功返回0,失败返回错去码。

三、自旋锁
何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。
其作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远 高于互斥锁。虽然它的效率比互斥锁高,但是它也有些不足之处:
1、自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。
2、在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等。因此我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。

例:实现多个生产者多个消费者模型

#include<stdio.h>#include<pthread.h>#include<semaphore.h>#include<unistd.h>#define SIZE 10int databuf[SIZE];sem_t blanks;sem_t datas;pthread_mutex_t conslock=PTHREAD_MUTEX_INITIALIZER;pthread_mutex_t proclock=PTHREAD_MUTEX_INITIALIZER;void* producer(void *arg){    static int i=0;    while(1)    {           sem_wait(&blanks);           pthread_mutex_lock(&proclock);        databuf[i]=rand()%1000;        printf("thread_id:%lu  producer:%d  i:%d\n",pthread_self(),databuf[i],i);        i++;        i=i%SIZE;        sem_post(&datas);        sleep(1);        pthread_mutex_unlock(&proclock);    }    return NULL;}void* consumer(void *arg){    static int i=0;    while(1)    {        sem_wait(&datas);        pthread_mutex_lock(&conslock);        int data=databuf[i];           printf("thread_id:%lu  consumer: %d  i:%d\n",pthread_self(),data,i);        i++;        i=i%SIZE;        sem_post(&blanks);    //    sleep(1);        pthread_mutex_unlock(&conslock);       }    return NULL;}int main(){    sem_init(&blanks,0,SIZE);    sem_init(&datas,0,0);    pthread_t producer_id1,consumer_id1;       pthread_t producer_id2,consumer_id2;    pthread_t producer_id3,consumer_id3;    pthread_create(&producer_id1,NULL,producer,NULL);    pthread_create(&consumer_id1,NULL,consumer,NULL);    pthread_create(&producer_id2,NULL,producer,NULL);    pthread_create(&consumer_id2,NULL,consumer,NULL);    pthread_create(&producer_id3,NULL,producer,NULL);    pthread_create(&consumer_id3,NULL,consumer,NULL);    pthread_join(producer_id1,NULL);    pthread_join(consumer_id1,NULL);    pthread_join(producer_id2,NULL);    pthread_join(consumer_id2,NULL);    pthread_join(producer_id3,NULL);    pthread_join(consumer_id3,NULL);    sem_destroy(&blanks);    sem_destroy(&datas);    return 0;}
3 1
原创粉丝点击