基于进程信号量的多线程同步机制研究与实现
1 信号量
1.1 原理与概念
信号量机制本质是对于资源的预订操作,线程或者进程预订了之后,确保未来有一段时间,资源是属于我的。
对于预订资源,会有一个最小单位,资源都是以这个最小单位为整体被使用的。
信号量需要做到:
- 限制进来的进程数(保证每一个进来请求使用资源的进程都有一块资源)
- 合理的分配资源
这里,由于是信号量的前导,我们简单的把信号量理解为一个计数器(是由OS维护的)。
我们这里对于这个信号量的计数器的设计,提出几个问题?
1.计数器能不能简单的设计成一个整型变量?
不行,因为整型变量在经过进程创建之后,任意一个进程对他进行改变的时候,会发生写时拷贝,导致两个进程看到的不是同一个计数器,这样信号量的第一个目的,限制进入的进程数也就失效了。
2.count++和count--不是原子的。
3.申请sem和释放sem来保护临界资源,是规则。这个规则的由来?
这个规则就是,程序员之间规定的规则,再使用多进程访问临界资源的时候,需要代码这样来保护临界资源。
4.所有的进程要访问临界资源,都需要先申请信号量,那么所有进程都需要看到同一个信号量,说明了信号量本身就是一个临界资源。那么我们需要利用临界资源去保护另一个临界资源,为了防止临界资源保护的嵌套,我们就需要保证信号量这个临界资源是安全的。
所以,信号量的申请(++)和信号量的释放(--)这两个操作都是原子的。
5.如果,信号量的初始值是1?
那么,这个信号量不就是一个二元信号量(不就是一把锁吗)
6.我们前面提到了信号量也需要合理的分配资源,那么由谁来做呢?
这里,也是由程序员,在代码部分来完成这项目标。
7.pv操作
我们把原子性的申请信号量称为p操作,原子性的释放信号量称为v操作。
2 信号量的接口
2.1 初始化信号量:sem_init()
sem_init是Posix信号量操作中的一个函数,用于初始化一个定位在sem的匿名信号量。以下是对sem_init函数的详细解析:
一、函数原型
二、参数说明
- sem:指向sem_t类型的信号量对象的指针,该对象将被初始化。
- pshared:指明信号量的共享属性。如果其值为0,则信号量将被进程内的线程共享;如果其值为非零,则信号量将在进程之间共享,此时信号量应定位在共享内存区域中。
- value:指定信号量的初始值。这个值表示信号量可用的资源数目或信号灯的数目。
三、返回值
- 成功时,sem_init函数返回0。
- 失败时,sem_init函数返回-1,并将errno设置为合适的值以指示错误原因。
四、注意事项
- 初始化一个已经初始化的信号量,其结果未定义。因此,在调用sem_init之前,应确保信号量对象未被初始化。
- 当pshared为非零时,信号量必须存在于共享内存中,否则无法实现进程间共享。
- 使用完信号量后,应调用sem_destroy函数来销毁它,以释放相关资源。
2.2 信号量等待:sem_wait()
sem_wait
是 POSIX 信号量(semaphore)API 中的一个函数,用于对信号量进行 (wait/down)操作。这个函数会阻塞调用它的线程,直到信号量的值大于零,然后它会将信号量的值减一并继续执行。如果信号量的值已经是零,则调用 sem_wait
的线程会被阻塞,直到信号量的值被其他线程通过 sem_post
增加。
一、函数原型
二、参数
sem
:指向sem_t
类型的信号量对象的指针。这个信号量对象应该是之前通过sem_init
或sem_open
初始化的。
三、返回值
- 成功时,
sem_wait
返回 0。 - 失败时,
sem_wait
返回 -1,并设置errno
来指示错误的原因。可能的错误包括EINVAL
(无效的参数,即sem
不是有效的信号量),EINTR
(操作被信号中断),EDEADLK
(死锁条件,如果信号量是通过sem_init
初始化为线程间共享且调用线程已经拥有该信号量),以及ENOSYS
(如果系统不支持这个操作,尽管在现代 POSIX 系统中这不太可能)。
四、使用场景
sem_wait
通常用于保护临界区(critical section),确保同一时间只有一个线程(或进程,取决于信号量的共享属性)可以进入和执行临界区内的代码。这有助于避免多线程程序中的竞态条件(race conditions)和数据不一致问题。
五、注意事项
- 在调用
sem_wait
之前,应确保信号量已经被正确初始化。 - 如果信号量被设置为线程间共享(
sem_init
的pshared
参数非零),则所有操作该信号量的线程都应该在同一个进程内,或者信号量应该位于共享内存区域中,以便在不同进程间共享。 - 在不再需要信号量时,应调用
sem_destroy
来销毁它(对于通过sem_init
初始化的信号量)或sem_close
和sem_unlink
(对于通过sem_open
创建的命名信号量)。
2.3 信号量申请:sem_post
sem_post
是 POSIX 信号量(semaphore)API 中的一个函数,用于对信号量进行 V(signal/up)操作。这个函数会增加信号量的值,并可能唤醒一个或多个因为调用 sem_wait
而被阻塞的线程。
一、函数原型
二、参数
sem
:指向sem_t
类型的信号量对象的指针。这个信号量对象应该是之前通过sem_init
(对于匿名信号量)或sem_open
(对于命名信号量)初始化的。
三、返回值
- 成功时,
sem_post
返回 0。 - 失败时,
sem_post
返回 -1,并设置errno
来指示错误的原因。可能的错误包括EINVAL
(无效的参数,即sem
不是有效的信号量)。
四、使用场景
sem_post
通常与 sem_wait
一起使用,以实现线程间的同步。当一个线程完成了对临界区的访问后,它会调用 sem_post
来增加信号量的值,从而可能允许其他被阻塞的线程进入临界区。
五、注意事项
- 在调用
sem_post
之前,应确保信号量已经被正确初始化。 - 如果信号量被设置为线程间共享(
sem_init
的pshared
参数非零),则所有操作该信号量的线程都应该在同一个进程内,或者信号量应该位于共享内存区域中,以便在不同进程间共享。然而,需要注意的是,POSIX 信号量 API 并不直接支持跨进程的匿名信号量;跨进程共享通常是通过命名信号量(使用sem_open
)来实现的。 sem_post
的操作是原子的,即它不会被其他线程的sem_wait
或sem_trywait
调用中断。
2.4 信号量销毁:sem_destroy()
sem_destroy
是 POSIX 信号量(semaphore)API 中的一个函数,用于销毁一个由 sem_init
初始化的匿名信号量。这个函数会释放与信号量相关联的任何资源,并且只有在没有线程等待该信号量时才能成功调用。
一、函数原型
二、参数
sem
:指向sem_t
类型的信号量对象的指针。这个信号量对象应该是之前通过sem_init
初始化的。
三、返回值
- 成功时,
sem_destroy
返回 0。 - 失败时,
sem_destroy
返回 -1,并设置errno
来指示错误的原因。可能的错误包括EBUSY
(信号量当前正在被使用,即有线程正在等待它),以及EINVAL
(无效的参数,即sem
不是有效的信号量)。
四、使用场景
sem_destroy
应该在信号量不再需要时被调用,以释放与其相关联的资源。这通常发生在程序结束或信号量完成其同步任务之后。
五、注意事项
- 在调用
sem_destroy
之前,应确保没有线程在等待该信号量。如果有线程在等待,sem_destroy
将返回 -1 并设置errno
为EBUSY
。 sem_destroy
只能用于由sem_init
初始化的匿名信号量。对于通过sem_open
创建的命名信号量,应使用sem_close
和sem_unlink
来关闭和删除它们。- 一旦信号量被销毁,就不能再对它进行任何操作(如
sem_wait
、sem_post
等),否则会导致未定义的行为。