您的位置:首页 > 其它

使用System V信号量实现多线程互斥

2011-05-21 11:15 330 查看
POSIX Thread中提供了非常强大的线程互斥机制, 如 pthread_mutex_XXXX / pthread_cond_XXXX 以及 semaphore(sem_wait/sem_post)等。在同一进程内的多线程编程非常方便灵活。但对于跨进程的线程互斥问题就变得麻烦了。

其实主流的Linux在内核中也实现了System V的IPC, 在跨进程的线程互斥方面实现起来更加简单。当然,同一进程容器内的多线程也可以使用。

信号量的创建

semget的man page: http://linux.die.net/man/2/semget

如果是在同一进程或者亲缘进程内使用

#include <sys/sem.h>
#include <sys/ipc.h>
int semid = semget( IPC_PRIVATE, 1, 0);

这样就创建了一个无名信号量集合,其中semget的第2个参数表示这个信号量集合中有1个信号量。

而对于非亲缘关系的多进程内线程使用的信号量,则一般这样创建
#include <sys/sem.h>
#include <sys/ipc.h>
key_t  ipckey = ftok("/tmp/foo", 42);

int semid = semget( ipckey, 1,  IPC_CREAT | 0666);

ftok函数用于创建System V 的 IPC key, 其中第一个参数必须是存在的文件路径且具备可访问权限。
第2个参数(proj_id)必须是大于0的数,且这个数只有低8位起作用。
ftok通过计算指定文件的索引节点的index和第2个参数proj_id,来创建一个唯一的IPC key.

例如, /tmp/foo对应的文件索引号是0x100000, 那么proj_id等于42(0x2A), 那么得到的key就是 0x2A100000
也就是说,对于不同的进程,只要ftok的2个参数相同,则得到的IPC key的值也会是相同的。

而对于同一个IPC key, semget保证在不同的进程中能够或得到相同的信号量集合.
而这个信号量集合是一个内核数据结构,不会因为进程的退出而自动销毁,所以需要程序显式地进行销毁。

信号量的销毁

semctl的man page: http://linux.die.net/man/2/semctl

semctl( semid, 0, IPC_RMID, 0)


信号量的使用

信号量的使用主要是通过这2个函数, man page(http://linux.die.net/man/2/semop)
int semop(int semid, struct sembuf *sops, unsigned nsops);
int semtimedop(int semid, struct sembuf *sops, unsigned nsops, struct timespec *timeout);

首先了解一下sembuf 的结构

struct sembuf
{
unsigned short int sem_num; /* semaphore number */
short int sem_op;  /* semaphore operation */
short int sem_flg;  /* operation flag */
};


sem_num : 表面当前操作的是信号量集合中的第几个信号量,0表示第一个

sem_op : 操作数,见下文

sem_flg : IPC_NOWAIT标志表示此操作仅做测试,进阻塞当前线程; SEM_UNDO标志表示进程提前退出时撤销此操作

对于sem_op,分为下列几种情况

如果 sem_op 为 0,则测试 sem_num指定的信号量, 以确定它是否为 0。如果 sem_num指定的信号量 为 0,则运行下一个测试。如果 sem_num 不为 0,则在未设置 IPC_NOWAIT 时,操作将阻塞直至信号量变为 0,而在设置了 IPC_NOWAIT 时,则跳过其他测试直接返回。

如果 sem_op 是某个正数,则将信号量的值加上 sem_op 的值, 此操作永远都不会阻塞。

如果 sem_op 是一个负整数,并且 sem_num指定的信号量 的值大于或等于 sem_op 的绝对值,则从信号量的值减去该绝对值。

如果 sem_op 是一个负整数,并且 sem_num指定的信号量 的值小于 sem_op 的绝对值,则在 IPC_NOWAIT 标志设置的情况下立即停止测试的执行,而如果IPC_NOWAIT 标志没有设置则阻塞,直至信号量的值变得大于 sem_op 的绝对值

下面看2个具体的实例方便理解

1. 将信号量作为临界区使用,保证某段代码在同一时间内只能有一个线程进入执行

struct sembuf sem[2];
// 第一个操作
sem[0].sem_num = 0; // 操作信号量集合中的第一个信号量
sem[0].sem_op = 0; // 保证此信号量的值为0,否则阻塞
sem[0].sem_flg = SEM_UNDO; // 如果操作过程中进程退出则回滚
// 第二个操作
sem[1].sem_num = 0; // 操作信号量集合中的第一个信号量
sem[1].sem_op = 1; // 信号量值增1,阻塞其它线程进入
sem[1].sem_flg = SEM_UNDO; // 如果操作过程中进程退出则回滚
// 执行上述操作
if( semop( semid, sem, 2) == 0 ){

// To DO: 受保护的临界区代码
// .....
// 执行完成,信号量值减一,释放此锁,方便其它线程进入执行
sem[0].sem_op = -1;
semop( semid, sem, 1);
}


2. 将信号量作为可用资源数标识

这是一种很常见的情况,在生产者-消费者线程池模型中, 一组生产者线程从网络或者文件中读取到数据包并追加到工作任务队列;另一组消费者线程从工作任务队列中获取工作任务后执行。

在这种模式下,这些消费者线程需要根据工作任务队列中是否有需要处理的工作任务来及时地启动或者等待执行。

首先对于生产者线程, 当将数据包追加到工作任务队列后,可以将可用资源数增一,此操作永远都不会阻塞

sem[0].sem_num = 0; // 指定操作的是该信号量集合中的第一个
sem[0].sem_op = 1;  // 增1
sem[0].sem_flg = 0;
semop( semid, sem, 1);


其次对于消费者线程

struct sembuf sem[1];
sem[0].sem_num = 0; // 操作信号量集合中的第一个信号量
sem[0].sem_op = -1; // 如果队列中有多余一个的工作任务,则理解返回,否则阻塞等待。
sem[0].sem_flg = SEM_UNDO; // 如果操作过程中进程退出则回滚
// 执行上述操作
if( semop( semid, sem, 1) == 0 ){
// TO DO: 锁定队列,获取并移除队列中的第一个工作任务进行处理
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息