同步和互斥的POSIX支持(读写锁、信号量和记录锁)

一、读写锁

1.基本概念

当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。因而有时候将读和写访问区分开来是有益处的。
读写锁就提供了这样的能力。通过读写锁,可以对受保护的共享资源进行并发读取和独占写入。其规则为:
  1. 只要没有线程持有某个特定的读写锁用于写,那么任意数目的线程都可以持有该读写锁进行读
  2. 仅当没有任何线程持有某个特定的读写锁用于写或读时,才能分配该读写锁给某个线程让其进行写
读写锁适用于有很多并发读请求,同时写比较少的场景。由于读写锁允许多个读者并存因而提高了并发性;同时由于任意时刻只有一个写者可以持有读写锁,又可以保护数据在写期间不被其它读者或写者所干扰。

2.API

POSIX定义的读写锁的数据类型是: pthread_rwlock_t
  1. #include <pthread.h>   
  2. int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); 成功返回0,其它返回值表示出错  
  3. int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock ); 成功返回0,其它返回值表示出错  
  4. int pthread_rwlock_timedrdlock(pthread_rwlock_t *rwlock, const struct timespec *abs_timeout); 成功返回0,其它返回值表示出错  
  5. int pthread_rwlock_reltimedrdlock_np(pthread_rwlock_t *rwlock, const struct timespec *abs_timeout); 成功返回0,其它返回值表示出错  
  6. int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); 成功返回0,其它返回值表示出错  
  7. int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock ); 成功返回0,其它返回值表示出错  
  8. int  pthread_rwlock_timedwrlock(pthread_rwlock_t  *rwlock, const struct timespec *abs_timeout); 成功返回0,其它返回值表示出错  
  9. int pthread_rwlock_reltimedwrlock_np(pthread_rwlock_t *rwlock, const struct timespec *abs_timeout); 成功返回0,其它返回值表示出错  
  10. int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); 成功返回0,其它返回值表示出错  
  11. int pthread_rwlock_unlock (pthread_rwlock_t *rwlock); 成功返回0,其它返回值表示出错  
  12. int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); 成功返回0,其它返回值表示出错  
  13. int pthread_rwlockattr_init(pthread_rwlockattr_t *attr); 成功返回0,其它返回值表示出错  
  14. int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr); 成功返回0,其它返回值表示出错  
  15. int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared); 成功返回0,其它返回值表示出错  
  16. int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr, int *pshared); 成功返回0,其它返回值表示出错  

1)初始化读写锁

如果读写锁变量是静态的则可以直接用PTHREAD_RWLOCK_INITIALIZER来初始化它,比如:
static pthread_rwlock_t my_rwlock = PTHREAD_RWLOCK_INITIALIZER
如果读写锁变量是动态分配的,则必须在使用它之前用pthread_rwlock_init来初始化它。
pthread_rwlock_init用来初始化rwlock所指向的读写锁,如果attr为NULL则会使用缺省的属性初始化读写锁,否则使用指定的attr初始化读写锁。
使用PTHREAD_RWLOCK_INITIALIZER 宏与动态分配具有null 属性的 pthread_rwlock_init等效,不同之处在于PTHREAD_RWLOCK_INITIALIZER 宏不进行错误检查。

2)在读写锁上获取读锁(读锁定)

pthread_rwlock_rdlock用来读锁定指定的读写锁。
  1. 如果没有写者持有该锁,并且没有写者阻塞在该锁上,则调用线程会获取读锁。
  2. 如果没有写者持有该锁,但是有写者阻塞在该锁上,则调用线程是否能获取该锁是不确定的。
  3. 如果有写者持有该锁,则调用线程无法获取该锁。
  4. 如果在进行调用时,调用线程已经写锁定了该锁,则结果是不确定的。
  5. 如果调用pthread_rwlock_rdlock时所指定的读写锁未初始化,则结果是不确定的。
如果调用线程未获取读锁,则它将阻塞直到它获取了该锁。为避免写者饥饿,允许将写者的优先级设置的高于读者。
一个线程可以在一个读写锁上多次执行读锁定。线程可以成功调用pthread_rwlock_rdlock n 次,但是之后该线程必须调用 pthread_rwlock_unlock() n 次才能解除锁定。
信号处理程对这里的等待是透明的:线程信号处理程序可以处理传送给等待读写锁的线程的信号。从信号处理程序返回后,线程将继续等待读写锁,就好像线程未被中断一样。

3)在指定的时间之前在读写锁上获取读锁(读锁定)

pthread_rwlock_timedrdlock用来读锁定指定的读写锁,如果必须等待以获取读锁,则最多等待指定的时长后就不再等待。
如果锁可以立即被获得,那么时间参数就不会被检查。
信号处理程对这里的等待是透明的:如果等待被信号处理程序打断了,则从信号处理程序返回后,会继续等待就像没有被打断一样。
如果在进行调用时,调用线程已经写锁定了该锁则可能导致死锁。
pthread_rwlock_reltimedrdlock_np与pthread_rwlock_timedrdlock基本相同,它们唯一的区别在于前者使用相对时间间隔而不是将来的绝对时间作为其最后一个参数的值。

4)尝试在读写锁上以非阻塞的方式获取读锁(读锁定)

pthread_rwlock_tryrdlock用于尝试以非阻塞的方式来在读写锁上获取读锁。如果有任何的写者持有该锁或有写者阻塞在该读写锁上,则立即失败返回。

5)在读写锁上获取写锁(写锁定)

pthread_rwlock_wrlock用来写锁定指定的读写锁。
  1. 如果没有写者持有该锁,并且没有写者读者持有该锁,则调用线程会获取写锁。否则线程阻塞。
  2. 如果在进行调用时,调用线程已经写锁定或读锁定了该锁,则结果是不确定的。
  3. 如果调用pthread_rwlock_rdlock时所指定的读写锁未初始化,则结果是不确定的。
如果调用线程未获取写锁,则它将阻塞直到它获取了该锁。为避免写者饥饿,允许将写者的优先级设置的高于读者。
一个线程可以在一个读写锁上多次执行读锁定。线程可以成功调用pthread_rwlock_rdlock n 次,但是之后该线程必须调用 pthread_rwlock_unlock() n 次才能解除锁定。
信号处理程对这里的等待是透明的:线程信号处理程序可以处理传送给等待读写锁的线程的信号。从信号处理程序返回后,线程将继续等待读写锁,就好像线程未中断一样。

6)在指定的时间之前在读写锁上获取写锁(写锁定)

pthread_rwlock_timedwrlock用来写锁定指定的读写锁,如果没有写者持有该锁,并且没有写者读者持有该锁,则调用线程会获取写锁,否则将等待,但是最多等待指定的时长后就不再等待。
pthread_rwlock_reltimedwrlock_np与pthread_rwlock_timedwrlock基本相同,它们唯一的区别在于前者使用相对时间间隔而不是将来的绝对时间作为其最后一个参数的值。

7)尝试在读写锁上以非阻塞的方式获取读锁(读锁定)

pthread_rwlock_trywrlock用于尝试以非阻塞的方式来在读写锁上获取写锁。如果有任何的读者或写者持有该锁,则立即失败返回。

8)尝试在读写锁上以非阻塞的方式获取读锁(读锁定)

pthread_rwlock_destroy用于销毁一个读写锁,并释放所有相关联的资源(所谓的所有指的是由pthread_rwlock_init自动申请的资源)

9)初始化读写锁属性

pthread_rwlockattr_init使用缺省的属性来初始化读写锁属性指定的读写锁属性对象。
如果指定的读写锁属性对象已经初始化,则结果是不确定的。

10)销毁读写锁属性

pthread_rwlockattr_destroy可用来销毁读写锁属性对象。
在再次调用 pthread_rwlockattr_init重新初始化该对象之前,使用该读写锁的结果是不确定的。

11)设置/获取读写锁的作用范围属性

pthread_rwlockattr_setpshared用来设置读写锁的作用范围,设置的值存放在指定的读写锁属性对象中。
作用范围的取值及其含义:
  • PTHREAD_PROCESS_SHARED:该读写锁可以在多个进程中的线程之间共享。
  • PTHREAD_PROCESS_PRIVATE:仅初始化本读写锁的线程所在的进程内的线程才能够使用该读写锁。
pthread_rwlockattr_getpshared用来获取指定的读写锁属性对像上的作用范围属性。

二、信号量

1.基本概念

信号量是用来在进程间或者一个进程的多个线程之间进行同步/互斥的原语。
信号量一般用整数表示,经典的信号量上的操作是P操作(wait,down,lock)和V操作(post,up,unlock)。其使用方法为:
  1. 线程发起P操作尝试将信号量的值减1,如果信号量的值在发起该P操作之前为正值,则P操作成功返回,并且信号量的值被减1;否则线程阻塞直到信号量的值变为正值
  2. 线程完成自己需要在信号量保护下进行的操作
  3. 线程调用V操作将信号量的值加1
P、V操作必须以原子方式执行,不能再将其划分成子操作,即,在这些子操作之间不能对信号量执行其他操作。在P操作中,信号量的值在减小之前必须为正,从而确保生成的信号量的值不为负,并且比该值减小之前小 1。信号量通常用来协调对资源的访问,其中信号量计数会初始化为可用资源的数目。
有两种基本信号量:二进制信号量和计数信号量。二进制信号量的值只能是 0 或 1,计数信号量可以是任意非负值。二进制信号量在逻辑上相当于一个互斥锁。因而二进制的信号量可以像互斥锁那样来使用:
sem_wait(...)
临界区
sem_post(...)
但是二者存在一些区别,互斥锁一般应当仅由持有该锁的线程来解除锁定,但是由于不存在“持有信号量的线程”这一概念,所以,任何线程都可以执行V操作。
另外注意到由于信号量的V操作不一定要由发起P操作的线程来执行,因而V操作就可以由其它线程来执行,这就类似于条件变量的功能了:一个线程A等待某个条件被满足,另一个线程B通知线程A“你所等待的条件被满足了”。进一步的说信号量可用于异步事件通知,如用于信号处理程序中,并且不用象条件变量那样要求获取互斥锁。但是,信号量的效率不如互斥锁高。
缺省情况下,如果有多个线程正在等待信号量,则解除阻塞的顺序是不确定的。信号量在使用前必须先初始化,但是信号量没有属性。
总体上来说,信号量和互斥锁、条件变量之间有类似之处,但是也有区别,它们之间的区别:
  1. 互斥锁一般由锁定该锁的线程(即持有者)解锁,但是信号量的V操作不一定要由发起P操作的线程来执行,条件变量的唤醒一般由其它线程来执行
  2. 互斥锁要么是锁定的,要么是非锁定的,它类似于二进制信号量
  3. 只有条件变量会因为被信号打断而返回
  4. 由于信号量有一个相关的状态即其计数值,因而信号量上的V操作是被记住了的;但是对于条件变量来说如果发出signal时没有线程在等待该signal,则这个通知就会丢失
  5. 信号量不用像条件变量那样和互斥锁配合使用,但是其效率不如条件变量高
  6. 只有sem_post可以在信号处理程序中调用
POSIX定义了两种信号量:
  1. 命名信号量:命名信号量由符合Posix定义的IPC命名规则的名字标识,可以被用于进程之间或线程之间的同步互斥。命名信号具有属主用户 ID、组 ID 和保护模式。
  2. 未命名信号:未命名信号量在进程内存中分配,需要进行初始化,它可以被用于进程之间或线程之间的同步互斥,是否可用于多进程环境取决于信号的分配和初始化方式。

2.API

与信号量相关的数据结构类型是sem_t
  1. #include <semaphore.h>   
  2. int sem_init(sem_t *sem, int pshared, unsigned int value);成功返回0,其它返回值表示出错  
  3. int sem_destroy(sem_t *sem); 成功返回0,其它返回值表示出错  
  4. int sem_post(sem_t *sem); 成功返回0,其它返回值表示出错  
  5. int sem_wait(sem_t *sem); 成功返回0,其它返回值表示出错  
  6. int sem_trywait(sem_t *sem); 成功返回0,其它返回值表示出错  
  7. int sem_getvalue(sem_t *sem, int *value);成功返回0,其它返回值表示出错  
  8. sem_t *sem_open(const char *name, int oflag, .../* mode_t mode, unsigned int value */);成功时返回指向信号量的指针,否则返回SEM_FAILED.  
  9. int sem_close(sem_t *sem);成功返回0,其它返回值表示出错  
  10. int sem_unlink(const char *sem);成功返回0,其它返回值表示出错  
需要说明的是
sem_wait,sem_trywait,sem_post,sem_getvalue四个API对于命名信号和未命名信号都适用
sem_init和sem_destroy仅适用于未命名信号
sem_open,sem_close和sem_unlink仅适用于命名信号

1)初始化未命名信号 

sem_init用于将指定的信号量的初始值设置为value。
如果pshared的值为零,则该信号量只能由创建它的线程所在的进程内的线程使用。如果pshared的值不为零,则该信号量必须位于进程的共享内存内,信号量可以由所有可以访问这片共享内存的进程内的线程所使用。
多个线程不能初始化同一个信号量。
不能重新初始化其它线程正在使用的信号量。

2)销毁未命名信号 

sem_destroy用于销毁与 sem 所指示的未命名信号相关联的任何状态(但是不会释放存储信号量的内存)。

3)增加信号量(V操作)

sem_post实现V操作,用来以原子方式增加 sem 所指示的信号量额值。

4)基于信号量的计数值阻塞 

sem_wait实现P操作,它尝试将信号量的值减1,如果信号量的值在发起该P操作之前为正值,则立即返回,并且信号量的值被减1;否则将阻塞直到信号量的值变为正值

5)尝试基于信号量的计数值阻塞 

sem_wait实现P操作,它尝试将信号量的值减1,如果信号量的值在发起该P操作之前为正值,则立即返回,并且信号量的值被减1;否则将返回EAGAIN错误

6)返回指定信号量的当前计数值

sem_getvalue返回指定信号量的当前计数值。
如果指定的信号量当前处于阻塞状态,则返回值为0或者负值;如果是负值,则该值的绝对值等于当前阻塞在该信号量上的线程的数目

7)创建一个新的命名信号量或者打开已经存在的命名信号量

sem_open用于创建一个新的命名信号量或者打开已经存在的命名信号量,命名信号总是可用于进程间或线程间同步、互斥。
oflag可以为0,O_CREAT或O_CREAT|O_EXCL的取值及其含义
如果oflag包含O_CREAT,则需要指定第3和第4个参数,其中第三个参数的含义类似于文件的模式;第4个参数即为信号量的初始值
如果oflag未包含O_EXCL而是仅包含O_CREAT,则其含义是如果指定名字的信号量不存在就用指定的模式和初始值创建并初始化它,因而即使信号量已经存在也不算是错误
如果oflag同时包含O_EXCL和含O_CREAT,则如果指定名字的信号量已经存在就会返回错误

8)关闭指定的命名信号量

sem_close用来关闭用sem_open打开的命名信号量。
在进程终止时,所有由该进程打开的命名信号量都会自动被关闭,不管进程是主动退出的还是被“杀死的”。
但是关闭命名信号量并不会从系统删除该命名信号量。命名信号量至少具有内核持久性。
内核持久性是和资源的存在时间相关的一个概念,其相关的概念:
  • 内核持久性:指的是只要系统没有重启并且资源没有被显式的删除它,它就会一直存在。
  • 进程持久性:指的是只要还有任何一个打开该资源的进程还没有关闭它,它就会一直存在
  • 文件持久性:只要没有显式的删除资源,它就一直存在,即便系统重启也如此

9)删除指定的命名信号量

sem_unlink用于从系统删除指定的命名信号量。它类似于文件系统的unliink函数,真正的删除只有在所有打开该命名信号量的线程都关闭了它之后才会发生。

三、记录锁

1.基本概念

除了POSIX定义的这些同步对象外,*nix系统还提供另外一种锁--记录锁。
记录锁用于对文件的访问控制,可以通过文件描述符找到被锁的文件,上锁是通过fcntl来完成的。记录锁可以用于相关联的进程之间也可以用于无关的进程之间。
记录锁的功能是:一个进程正在读或修改文件的某个部分时,可以阻止其他进程修改同一文件区。记录锁锁定的是文件的一个区域(也可能是整个文件)。记录锁通常维护在内核中,锁的持有者由锁持有者的进程ID标识。

2.API

  1. #include <fcnt1.h>   
  2. int fcnt1(int fd ,int cmd,.../* struct flock *arg */ ) ;  
  3. 对于记录锁, cmd是FGETLK、FSETLK或FSETLKW。第三个参数是一个指向flock结构的指针。  
  4. struct flock{  
  5.     short l_type;  
  6.     short l_whence;  
  7.     off_t l_start;  
  8.     off_t l_len;  
  9.     pid_t l_pid;  
  10. };  
flock结构的域极其含义:
  • l_type:所希望的锁类型,F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)或F_UNLCK(解锁一个区域)
  • l_start:要加锁或解锁的区域的起始地址,由l_start和l_whence两者决定。l_start是相对位移量(字节),l_whence则决定了相对位移量的起点。
  • l_whence:SEEK_SET,SEEK_CUR,SEEK_END.其含义类似于文件系统中的同名宏的含义
  • l_len: 加锁区域的长度。
关于加锁和解锁区域的还要注意:
  • 该区域可以在当前文件尾端处开始或越过其尾端处开始,但是不能在文件起始位置之前开始或越过该起始位置。
  • 如若l_len为0,则表示锁的区域从其起点(由l_start和l_whence决定)开始直至最大可能位置为止。也就是不管添写到该文件中多少数据,它都处于锁的范围。
  • 为了锁整个文件,通常的方法是将l_start设置为0,l_whence设置为SEEK_SET,l_len设置为0。
共享读锁(l_type为F_RDLCK)和独占写琐(F_RDLCK)的规则:
  • 多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定字节上的写锁则只能由一个进程独用。更进一步而言,如果在一个给定字节上已经有一把或多把读锁,则不能在该字节上再加写锁;如果在一个字节上已经有一把独占性的写锁,则不能再对它加任何读锁。
  • 加读锁时,该描述符必须是读打开;
  • 加写锁时,该描述符必须是写打开。
fcntl函数的三种命令:
  • F_GETLK:判断由arg所指定的锁是否被另外一把锁所排斥(阻塞)。如果存在一把锁,它阻止创建由arg所描述的锁,则这把现存的锁的信息写到arg指向的结构中。如果不存在这种情况,则除了将l_type设置为F_UNLCK之外,arg所指向结构中的其他信息保持不变。
  • F_SETLK:设置由arg所描述的锁。如果试图设置的锁不被规则所允许,则立即出错返回,此时errno设置为EACCES或EAGAIN。
  • F_SETLKW:它是F_SETLK的阻塞版本(命令名中的W表示等待(wait))。如果由于存在其他锁而导致由arg所要求的锁不能被创建,则调用进程睡眠。如果捕捉到信号则睡眠中断。
一个进程持有的记录锁会在进程关闭与该锁相关联的文件时或者进程终止时被自动删除。记录在进程调用fork时不会被继承。
记录锁不能和标准I/O库一起使用,因为标准I/O库使用了缓存。如果要使用记录锁,就要直接使用read和write。
记录锁也是一种协作性锁(某些系统支持强制性的记录锁。强制性锁机制中,内核对每一个open、read和write都要检查调用进程对正在存取的文件是否违背了某一把锁的作用。)。
原文地址:https://www.cnblogs.com/hehehaha/p/6332855.html