Loading

Unix同步方式

重要的一句话进程终止时系统不会自动释放所持有的锁,互斥锁读写锁Posix信号量。进程终止时内核总是自动清理的唯一同步锁类型是fcntl记录锁 。进程终止时(无论自愿与否),内核会对其上仍然打开着的所有 有名信号量 自动执行 sem_close 信号量关闭操作(不是释放)。

System V信号量,应用程序可以选择进程终止时内核是否自动清理某个信号量。

同步

同步(英语:synchronization),指在一个系统中所发生的事件(event)之间进行协调,在时间上出现一致性与统一化的现象。

为允许在线程或进程间共享数据,同步通常是必需的。本文涉及到的同步方式有:

  • 互斥锁
  • 互斥锁与条件变量
  • 读写锁
  • 记录锁
  • Posix 信号量

互斥锁

互斥锁指代相互排斥,是最基本的同步形式。互斥锁用于保护 临界区 ,以确保任何时刻只有一个线程(或进程)在执行其中的代码。伪代码如下:

lock_mutex(mutex);		// 1.获取互斥锁
do_something();			// 2.访问临界区
unlock_mutex(mutex);	// 3.释放互斥锁

尽管我们说互斥锁保护的是临界区,实际上保护的是在临界区中的被操作的数据,也就是共享数据。

互斥锁是 协作性锁 。也就是说操作流程应该是在实际操作前获取互斥锁,但是没有办法防止某个线程(或进程)不先获取该互斥锁就操作数据。

初始化互斥锁

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
  • 动态分配的,或者分配在共享内存区中的互斥锁必须使用 pthread_mutex_init 初始化。静态分配的可以初始化为常量 PTHREAD_MUTEX_INITIALIZER
  • attr = NULL ,则使用默认属性。

销毁互斥锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

获取锁

// 阻塞,直到互斥锁解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 非阻塞,立即返回
int pthread_mutex_trylock(pthread_mutex_t *mutex);
  • 阻塞方式,若已有另外进程对互斥锁上锁,将阻塞到该互斥锁解锁
  • 非阻塞方式,立即返回,若已有另外进程对互斥锁上锁,返回 EBUSY 错误

释放锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

互斥锁属性

int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
// 获取共享属性
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *restrict attr, int *restrict pshared);
// 设置共享属性
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr, int pshared);
  • PTHREAD_PROCESS_SHARED ,指定互斥锁在不同进程间共享
  • PTHREAD_PROCESS_PRIVATE,指定互斥锁只能在单个进程内不同线程间共享(默认)

互斥锁与条件变量

当期待的条目未准备好时,消某线程持有锁阻塞等待,锁住整个临界区。这样的情况还可能造成死锁,这不是我们想要的。我们的期望是在线程等待其他条件的时候,释放已获得的互斥锁。这时条件变量就完美符合我们的要求。

互斥锁用于上锁,条件变量则用户等待。伪代码如下:

lock_mutex(mutex);		// 1.获取互斥锁
wait(cond, mutex);		// 2.等待条件
do_something();			// 3.访问临界区
unlock_mutex(mutex);	// 4.释放互斥锁

初始化条件变量

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
  • 动态分配的,或者分配在共享内存区中的互斥锁必须使用 pthread_cond_init 初始化。静态分配的可以初始化为常量 PTHREAD_COND_INITIALIZER

销毁条件变量

int pthread_cond_destroy(pthread_cond_t *cond);

若有线程等待,则返回 EBUSY 错误。

等待条件成立

// 阻塞等待
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
// 定时等待 允许设置阻塞时间限制
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

struct timespec {
    time_t	tv_sec; 	// 秒
    long	tv_nsec;	// 纳秒
}
  • 每个条件变量总是与一个互斥锁关联,当条件不满足时,阻塞等待条件变量满足,并释放已获得的该互斥锁(原子操作)
  • 当被唤醒,函数返回时,解除阻塞并重新申请获取互斥锁
  • pthread_cond_timedwait ,允许线程就阻塞设置一个时间限制,该限制为 绝对时间 ,超时返回 ETIMEDOUT 错误
  • 使用绝对时间的好处:如果函数过早返回,那么同一函数无须改变其时间参数的内容就能再次被调用。

告知条件满足

// 至少唤醒一个等待该条件的线程
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒等待该条件的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);
  • pthread_cond_signal 至少唤醒一个等待该条件的线程
  • pthread_cond_broadcast 唤醒等待该条件的所有线程,会造成 ”惊群“。

条件变量属性设置

int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);
// 获取共享属性
int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr, int *restrict pshared);
// 设置共享属性
int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);
  • PTHREAD_PROCESS_SHARED ,指定条件变量在不同进程间共享
  • PTHREAD_PROCESS_PRIVATE,指定条件变量只能在单个进程内不同线程间共享(默认)

条件满足信号丢失问题

当条件满足,向一个条件变量发送信号时,若没有线程(或进程)等待该条件,则信号将会丢失。丢失和不再告知条件满足。

测试代码:cond_signal_ignore.cpp

测试结果:

[root@localhost sync]# ./signal_ignore 
主线程:子线程创建完毕
主线程:获取到互斥锁
主线程:发出 signal... 
主线程:释放掉互斥锁
子线程:获取到互斥锁
子线程:尝试获取条件变量...
^C

读写锁

与互斥量类似,但读写锁允许更高的并行性。其特性为:读时共享,写时独占(写优先级高)

【读写锁】分配规则:共享-独占 上锁(读锁--共享锁,写锁--独占锁)

  1. 若无线程持有给定的读写锁 ,那么任意数目的线程可持有该读写锁
  2. 当且仅当无线程持有读写锁 读写 时,才分配读写锁用于

基本操作:

// 静态分配赋值常量 PTHREAD_RWLOCK_INITIALIZER
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

// 读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
// 写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
// 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

属性操作:

int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr, int *restrict pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr, int pshared);
  • PTHREAD_PROCESS_SHARED ,在不同进程间共享
  • PTHREAD_PROCESS_PRIVATE,只能在单个进程内不同线程间共享(默认)

fcntl记录上锁

假设我们现在要设置某个锁,以实现对文件访问的同步。上面谈到的锁作用于整个文件,粒度较大,我们期望这个锁只作用与文件的 记录(即字节范围) 上。

Unix内核没有文件内的记录这一概念,但内核提供的上锁特性却用 记录上锁(record locking)这一术语来描述。

粒度:用于标记能够锁住的对象的大小。通常情况下粒度越小,允许同时使用的用户数就越多。Posix 记录上锁的粒度为单字节。

Posix fcntl记录上锁

#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* struct flock *arg */ );

struct flock {
    short l_type;    /* 锁类型: F_RDLCK, F_WRLCK, F_UNLCK */
    short l_whence;  /* 偏移基准: SEEK_SET, SEEK_CUR, SEEK_END */
    off_t l_start;   /* 相对偏移起始位置 */
    off_t l_len;     /* 字节数  0--表示直到文件末尾*/
    pid_t l_pid;     /* 进程ID(仅 F_GETLK) */
};

cmd 参数:

  • F_SETLK

    • 获取l_type = F_RDLCKl_type = F_WRLCK )或释放l_type = F_UNLCK)由 arg 指向的 flock 结构所描述的锁。
    • 不阻塞,若无法将该锁授予调用进程,则立即返回 EACCESS 或 EAGAIN 错误。
  • F_SETLKW阻塞,作用同 F_SETLK

  • F_GETLK

    • 检查有 arg 指向的锁以确定是否有某个已存在的锁会妨碍将新锁授予调用进程
      • 当前没有锁,返回 l_type = F_UNLCK
      • 已存在锁,返回锁信息,包括持有该锁的进程ID
    • 提供该命令的原因:当指向 F_SETLK 命令返回错误时,获取导致错误文档某个锁信息(当然,连续使用两个命令不是原子操作,两个命令之间可能被解锁)

记录锁几个注意点

  1. 进程对文件上锁的类型受文件打开方式影响。
  2. 一个进程可以对某个文件的特定字节范围多次发出 F_SETLK 或 F_SETLKW 命令。每次成功与否取决于 其他进程 当时是否锁住该字节范围以及锁的类型,与本进程先前是否锁住该字节范围无关。
  3. 对于一个文件的任意字节,最多只能存在一种类型的记录锁(读锁或写锁)。一个给定字节可以有多个读锁,但只能有一个写锁。
  4. 进程终止内核自动清理 fcntl 记录锁,对于一个打开着某个文件的进程来说,当关闭文件的所有描述符或进程本身终止时,与该文件关联的所有记录锁都被删除。
  5. 不能继承,记录锁不能通过 fork 由子进程继承,记录锁跟进程ID紧密关联。
  6. 记录上锁不应该同标准I/O一块使用,因为标准I/O函数库会执行内部缓冲。应使用 read 和 write 系统调用。

劝告性上锁

Posix 记录上锁称为 劝告性上锁 (advisory locking)。其含义是内核维护着由各个进程上锁的所有文件的正确信息,但是它不能防止一个进程写一个读锁定文件,或者读一个写锁定文件。(类似互斥锁的协作性)

强制性上锁

有些系统提供另一种类型文档记录上锁,称为 强制性上锁 (mandatory locking)。使用强制性锁后,内核检查每个 read 和 write 请求,已验证起草组不会干扰由某个进程持有的某个锁。

若有干扰,对于阻塞式,冲突的 read 或 write 将把进程投入睡眠,直到锁释放。对于非阻塞式,返回 EAGAIN 错误。

强制性上锁也会导致不一致的数据,若下图:

Posix信号量

信号量是一种用于提供进程(或线程)间同步手段的原语。

  • Posix有名信号量:由与文件系统中的路径名对应的名字来标识(并不要求它们真正的存放在文件系统内的某个文件)。
  • Posix基于内存的信号量:即无名信号量,

信号量、互斥锁和条件变量间的差异

  1. 互斥锁总是由给它上锁的线程(或进程)解锁,信号量的挂出却不必由执行过它的等待操作的同一线程(或进程)执行。
  2. 互斥锁要么被锁在,要么被解开(二值状态,类似于 二值信号量 ,其值或为0或为1)。
  3. 信号量有一个与之关联的状态(计数值),信号量的挂出操作总是被记住。当条件满足,向一个条件变量发送信号时,若没有线程(或进程)等待该条件,则信号将会丢失
  4. 在同步技巧(互斥锁、条件变量、读写锁、信号量)中,能够从信号处理程序中安全调用的唯一函数是 sem_post

Posix信号量函数调用

// 创建一个新的有名信号量,或打开一个已存在的有名信号量
sem_t *sem_open(const char *name, int oflag /* , mode_t mode, unsigned int value */);
// 关闭 sem_open 打开的有名信号量
int sem_close(sem_t *sem);
// 从系统中删除指定的有名信号量
int sem_unlink(const char *name);
  • 进程终止时(无论自愿与否),内核会对其上仍然打开着的所有 有名信号量 自动执行 sem_close 信号量关闭操作(不是释放)。
  • 关闭一个信号量斌没有将它从系统中删除,即,Posix有名信号量 至少是 随内核持续 的:即使当前没有进程打开着某个信号量,它的值仍然保持。
  • sem_unlink 类似于文件I/O的 unlink 函数,当引用计数还大于0时, name 就能从文件系统中删除,但是信号量的析构(不同于将它名字从文件系统中删除)要等到最后一个 sem_close 发生时为止。

sem_wait和sem_trywait

若信号量值大于0,将它减1。

int sem_wait(sem_t *sem);	// 睡眠等待
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

sem_post和sem_getvalue

int sem_post(sem_t *sem);
int sem_getvalue(sem_t *sem, int *sval);
  • sem_post:信号量值加1,若信号量值因此大于0,唤醒一个等待的进程(或线程)

    If the semaphore's value consequently becomes greater than zero, then another process or thread blocked in a sem_wait(3) call will be woken up and proceed to lock the semaphore.

  • sem_getvalue:获取指定信号量的当前值。

    若有不少于一个进程(或线程)等待,返回 valp 值:

    • = 0;
    • < 0,绝对值为等待的进程(或线程)数量。

sem_init和sem_destroy

Posix基于内存的信号量 操作。

// 初始化一个基于内存的信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
  • shared = 0 :进程内各线程共享。
  • shared != 0 :进程间共享。此时该信号量必须存放在某种类型的 共享内存区中 ,使用它的进程都要能访问该共享内存区。
  • 对于一个已初始化的信号量调用 sem_init ,其结果时未定义的。
  • 基于内存的信号量 具有 随进程 的持续性,其真正的持续性取决于存放信号量的内存区的类型
    • 单个进程内线程共享,持续性随进程;
    • 进程间共享,只要该共享内存区仍然存在,该信号量就继续存在。

信号量限制

Posix定义了两个信号量限制:

  • SEM_NSEMS_MAX:一个进程可同时打开着的最大信号量数;
  • SEM_VALUE_MAX:一个信号量的最大值。
posted @ 2021-02-06 16:53  JakeLin  阅读(174)  评论(0编辑  收藏  举报