Linux线程安全
一、POSIX有名信号量
1.POSIX 有名信号量的一般使用步骤:
1,使用 sem_open( )来创建或者打开一个有名信号量。
2,使用 sem_wait( )和 sem_post( )来分别进行 P 操作和 V 操作。
3,使用 sem_close( )来关闭他。
4,使用 sem_unlink( )来删除他,并释放系统资源。
2.sem_open()函数
功能
创建、打开一个POSIX有名信号量
头文件
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
原型
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);
参数
name 信号量的名字,必须以正斜杠"/"开头
oflag
O_CREATE 如果该名字对应的信号量不存在,则创建
O_EXCL 如果该名字对应的信号量已存在,则报错
mode 八进制读写权限,比如0666
value 初始值
返回值
成功 信号量的地址
失败 SEM_FAILED
备注
跟open()类似,当oflag中包含O_CREATE时,该函数必须提供后两个参数
3.P、V操作sem_wait\sem_post
功能
对POSIX有名信号量进行P、V操作
头文件
#include <semaphore.h>
原型
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem)
参数
sem 信号量指针
返回值
成功 0 失败 -1
4.关闭删除有名信号量
功能
关闭、删除POSIX有名信号量
头文件
#include <semaphore.h>
原型
int sem_close(sem_t *sem);
int sem_unlink(const char *name);
参数
sem 信号量指针
name 信号量名字
返回值
成功 0 失败 -1
5.test
j.c
#include "myhead.h"
#define MSGSIZE 128 //单个消息最大字节数
#define PROJ_PATH "." //使用当前路径来产生消息队列的键值key
#define PROJ_ID_SEM 11
#define PROJ_ID_SHM 12
#define SHMSIZE 1024 //单个消息最大字节数
// struct sembuf
// {
// unsigned short sem_num;信号量元素序号(数组下标)
// short sem_op;/*操作参数*/
// short sem_flg;/*操作选项*/
// };
#define SEM_NAME1 "/mysemname1"
int main(int argc, char const *argv[])
{
//信号量、共享内存的键值
key_t key1 = ftok(PROJ_PATH,PROJ_ID_SEM);
key_t key2 = ftok(PROJ_PATH,PROJ_ID_SHM);
if(key1 == -1 || key2 == -1)
{
perror("ftok()");
exit(-1);
}
//获取SHM的ID,并将之映射到本进程虚拟内存空间中
int shm_id = shmget(key1, SHMSIZE, IPC_CREAT | 0777);
char *shmAddr = shmat(shm_id,NULL,0);
if(shmAddr == (void *)-1 ||shm_id == -1)
{
perror("shmat()/shmget()");
exit(-1);
}
//打开信号量
sem_t *s1 = sem_open(SEM_NAME1,O_CREAT,0777,0);
bzero(shmAddr,SHMSIZE);
while(1)
{
scanf("%s",shmAddr);
getchar();
sem_post(s1);
if(!strcmp(shmAddr,"quit"))
break;
}
return 0;
}
r.c
#include "myhead.h"
#define MSGSIZE 128 //单个消息最大字节数
#define PROJ_PATH "." //使用当前路径来产生消息队列的键值key
#define PROJ_ID_SEM 11
#define PROJ_ID_SHM 12
#define SHMSIZE 1024 //单个消息最大字节数
// struct sembuf
// {
// unsigned short sem_num;信号量元素序号(数组下标)
// short sem_op;/*操作参数*/
// short sem_flg;/*操作选项*/
// };
#define SEM_NAME1 "/mysemname1"
int main(int argc, char const *argv[])
{
//信号量、共享内存的键值
key_t key1 = ftok(PROJ_PATH,PROJ_ID_SEM);
key_t key2 = ftok(PROJ_PATH,PROJ_ID_SHM);
if(key1 == -1 || key2 == -1)
{
perror("ftok()");
exit(-1);
}
//获取SHM的ID,并将之映射到本进程虚拟内存空间中
int shm_id = shmget(key1, SHMSIZE, IPC_CREAT | 0777);
char *shmAddr = shmat(shm_id,NULL,0);
if(shmAddr == (void *)-1 ||shm_id == -1)
{
perror("shmat()/shmget()");
exit(-1);
}
//打开信号量
sem_t *s1 = sem_open(SEM_NAME1,O_CREAT,0777,0);
bzero(shmAddr,SHMSIZE);
while(1)
{
// sem_wait(s1);
// sem_trywait(s1); //不阻塞等待
struct timespec *timeout;
clock_gettime(CLOCK_REALTIME,&timeout);
timeout.tv_sec+=5;
int ret = sem_timedwait(s1,&timeout);
if(ret < 0)
{
printf("超时!重新等待\n");
continue;
}
if(!strcmp(shmAddr,"quit"))
break;
printf("%s\n",shmAddr);
}
shmdt(shmAddr);
sem_close(s1);
sem_unlink(SEM_NAME1);
shmctl(shm_id,IPC_RMID,NULL);
return 0;
}
二、POSIX无名信号量
解决的是一个进程内部的线程间的同步互斥,那么也许不需要使用有名信号量,因为这些线程共享同一个内存空间,我们可以定义更加轻量化的、基于内存的(不在任何文件系统内部)无名信号量来达到目的。
1.POSIX无名信号量的使用步骤:
1,在这些线程都能访问到的区域定义这种变量(比如全局变量),类型是 sem_t。
2,在任何线程使用它之前,用 sem_init( )初始化他。
3,使用 sem_wait( )/sem_trywait( )和 sem_post( )来分别进行 P、V 操作。
4,不再需要时,使用 sem_destroy( )来销毁他。
2.sem_jinit\sem_destroy初始化、销毁POSIX无名信号量
功能
初始化、销毁POSIX无名信号量
头文件
#include <semaphore.h>
原型.
int sem_jinit(sem_ _t *sem, int pshared, unsigned int value);
int sem_destroy(sem_ _t *sem);
参数
sem 信号量指针
pshared 该信号量的作用范围: 0为线程间,非0为进程间
value 初始值
返回值
成功 0 失败 -1
无名信号量一般用在进程内的线程间,因此 pshared 参数一般都为 0。
3.test
#define _GNU_SOURCE
#include "myhead.h"
sem_t data; //读资源
sem_t space; //写资源
int i;
//线程任务函数
void *func1(void *arg)
{
while(1)
{
sem_wait(&space); //申请空间-1
sleep(1);
i++;
printf("线程1 i = %d\n", i);
sem_post(&data); //存数据+1
}
}
void *func2(void *arg)
{
while(1)
{
sem_wait(&data); //申请数据-1
sleep(1);
i++;
printf("线程2 i = %d\n", i);
sem_post(&space); //加空间+1
}
}
int main(int argc, char const *argv[])
{
//初始化信号量
sem_init(&data, 0, 0);
sem_init(&space, 0, 1);
//定义两个线程号
pthread_t pid1, pid2;
pthread_create(&pid1, NULL, func1, NULL);
pthread_create(&pid2, NULL, func2, NULL);
pthread_join(pid1, NULL);
pthread_join(pid2, NULL);
return 0;
}
3.三种信号量区别:
system-V 信号量和 POSIX 信号量named-sem 和unnamed-sem:
1,sys-V 信号量较古老,语法艰涩。POSIX 信号量简单,轻量。
2,sys-V 信号量可以对代表多种资源的多个信号量元素同一时间进行原子性的 P/V 操作,POSIX 信号量每次只能操作一个信号量。
3,sys-V 信号量和 named-sem 是系统范围的资源,进程消失之后继续存在,而unnamed-sem 是进程范围的资源,随着进程的退出而消失。
4,sys-V 信号量的 P/V 操作可以对信号量元素加减大于 1 的数值,而 POSIX 信号量每次 P/V 操作都是加减 1。
5,sys-V 信号量甚至还支持撤销操作——一个进程对 sys-V 信号量进行 P/V 操作时可以给该操作贴上需要撤销的标识,那么当进程退出之后,系统会自动撤销那些做了标识的操作。而 POSIX 信号没有此功能。
6,sys-V 信号量和 named-sem 适合用在进程间同步互斥,而 unamed-sem 适合用在线程间同步互斥。
总的来说system-V 的信号量功能强大,强大到臃肿啰嗦,如果在现实工作中不需要那些高级功能,建议使用接口清晰、逻辑简单的 POSIX 信号量。
三、互斥锁和读写锁
一个共享资源在任意时刻最多只能有一个线程在访问,这样的逻辑被称为“互斥”。这时,有一种更加方便和语义更加准确的工具来满足这种逻辑,他就是互斥锁。
1.使用互斥锁步骤:
1、先定义一个互斥锁变量
2、对互斥锁进行初始化
3、对资源进行上锁和解锁
4、不在使用时,直接销毁锁
pthread_mutex_t m; // 定义一个互斥锁变量
pthread_mutex_init(&m, NULL);//初始化互斥锁
pthread_mutex_lock(&m);//上锁
pthread_mutex_unlock(&m);//解锁
pthread_mutex_destroy(&m);//销毁锁
只上一把锁,解锁多次,不会出现死锁
2.互斥锁使用过程中容易出现死锁:
1、只上锁,不解锁,会出现死锁
2、某个线程上锁过后,在没有解锁的时候,就被异常杀死,那么获取同一个锁的其他线程会卡死
3、当多个线程交替上锁解锁两个锁的时候,有可能出现死锁
4、一个任务在上锁之后,未解锁之前,再次上同一把锁,也会出现死锁
建议:
为线程创建取消例程,释放有可能没有释放的锁
尽量避免在一组上锁解锁过程中,再次上锁同一把锁或者其他锁
互斥锁保护的临界区不要太长,也不要嵌套太多函数
3.test
#include "myhead.h"
pthread_mutex_t m; // 定义一个互斥锁变量
void output(const char *string)
{
const char *p = string;
while(*p!='\0')
{
fprintf(stderr, "%c",*p);
usleep(100);
p++;
}
}
void *routine(void *arg)
{
pthread_mutex_lock(&m);
output("message deliverd by child.\n");
pthread_mutex_unlock(&m);
pthread_exit(NULL);
}
int main(int argc, char const *argv[])
{
// 在任何线程使用该互斥锁之前必须先初始化
pthread_mutex_init(&m, NULL);
pthread_t tid;
pthread_create(&tid, NULL, routine, NULL);
// 在访问共享资源(标准输出设备)之前,加锁
pthread_mutex_lock(&m);
output("info output from parent.\n");
pthread_mutex_unlock(&m); // 解锁
// 阻塞等待子线程退出
pthread_join(tid,NULL);
pthread_mutex_destroy(&m);//销毁互斥锁
return 0;
}
互斥锁使用非常简便,但他也有不适用的场合——假如要保护的共享资源在绝大多数的情况下是读操作,就会导致这些本可以一起读的线程阻塞在互斥锁上,资源得不到最大的利用。
互斥锁的低效率,是因为没有更加细致地区分如何访问共享资源,一刀切地在任何时候都只允许一条线程访问共享资源,而事实情况是读操作可以同时进行,只有写操作才需要互斥,因此如果能根据访问的目的——读或者写,来分别加读锁(可以重复加)或者写锁(只允许一次一个),就能就能极大地提高效率(尤其是存在大量读操作的情况下)。
读写锁的操作几乎跟互斥锁一样,唯一的区别的是在加锁的时候可以选择加读或者写锁
#include "myhead.h"
static pthread_rwlock_t rwlock;
int global = 0;
char *str;
void thread_exit(void *arg)
{
printf("routine1 call free\n");
free(arg);
}
void *routine1(void *arg)
{
str = calloc(100,sizeof(char));
strcpy(str,(char*)arg);
pthread_cleanup_push(thread_exit,(void *)str);
// 对共享资源进行写操作之前,必须加写锁(互斥锁)
pthread_rwlock_wrlock(&rwlock);
global++;
printf("I am %s, now global=%d\n", str, global);
// 访问完之后释放该锁
pthread_rwlock_unlock(&rwlock);
pthread_exit(NULL);
pthread_cleanup_pop(1);
}
void *routine2(void *arg)
{
// 对共享资源进行写操作之前,必须加写锁(互斥锁)
pthread_rwlock_wrlock(&rwlock);
global+=100;
printf("I am %s, now global=%d\n", (char *)arg, global);
// 访问完之后释放该锁
pthread_rwlock_unlock(&rwlock);
pthread_exit(NULL);
}
void *routine3(void *arg)
{
// 对共享资源进行读操作之前,可以加读锁(共享锁)
pthread_rwlock_rdlock(&rwlock);
printf("I am %s, now global=%d\n", (char *)arg, global);
// 访问完之后释放该锁
pthread_rwlock_unlock(&rwlock);
pthread_exit(NULL);
}
void main_exit(void)
{
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
printf("main进程推出,销毁读写锁\n");
}
int main(int argc, char const *argv[])
{
// alarm(5);
pthread_rwlock_init(&rwlock,NULL);
// 创建三条线程,对共享资源同时进行读写操作
pthread_t t1,t2,t3;
pthread_create(&t1,NULL,routine1,"thread 1");
pthread_create(&t2,NULL,routine2,"thread 2");
pthread_create(&t3,NULL,routine3,"thread 3");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
atexit(main_exit); //注册main进程退出函数
exit(0);
}
四、条件变量
上图中,线程一旦发现余额为 0,就会进入等待睡眠,与此同时必须先释放互斥锁,如果带着锁去睡大觉,那么结果是谁也别想修改余额,大家都进入无限期的阻塞之中,相反从等待队列中出来的时候必须先持有互斥锁,因为出来后又要马上访问余额这个共享资源的。特别注意的是,有两把锁头是在框框里面的,这表示当一条线程进入某个条件变量的等待队列中等待,以及从该等待队列中出来时,分别对互斥锁的解锁和加锁都是自动完成的,这是为什么说条件变量跟互斥锁是配套使用的原因。
1.初始化、销毁条件变量
功能
初始化、销毁条件变量
头文件
#include <semaphore.h>
原型
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
参数
cond 条件变量
attr 条件变量的属性,一般始终为0
返回值
成功 0 失败 -1
跟其他的同步互斥机制一样,条件变量的开始使用之前也必须初始化。初始化函数中的属性参数 attr 一般不使用,设置为 NULL 即可。当使用 pthread_cond_destroy( )销毁一个条件变量之后,他的值变得不确定,再使用必须重新初始化。
2.进入条件变量等待队列同时对获取配套的互斥锁
功能
进入条件变量等待队列,同时对获取配套的互斥锁
头文件
#include <semaphore.h>
原型
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
参数
cond 条件变量
mutex 互斥锁
abstime 超时时间限制
返回值
成功 0 失败 -1
以上两个函数功能是一样的,区别是 pthread_cond_timedwait( )可以设置超时时间。着重要注意的是:一旦进入条件变量 cond 的等待队列,互斥锁 mutex 将立即被加锁。
3.唤醒全部,或者一个条件变量等待队列中的线程
功能
唤醒全部,或者一个条件变量等待队列中的线程
头文件
#include <semaphore.h>
原型
int pthread_cond broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
参数
cond 条件变量
返回值
成功 0 失败 -1
以上两个函数用来唤醒阻塞在条件变量等待队列里的线程,顾名思义,broadcast 用来唤醒全部的线程,相对地 signal 只唤醒一个等待中的线程。
注意:
被唤醒的线程并不能立即从 pthread_cond_wait( )中返回,而是必须要先获得配套的互斥锁。
4.test
#include "myhead.h"
int balance = 0; // 所有线程共享的 "余额"
pthread_mutex_t m;
pthread_cond_t v;
void *routine(void *args)
{
// 加锁,取钱
pthread_mutex_lock(&m);
while(balance < 100) // 若余额不足,则进入等待睡眠,顺便解锁
pthread_cond_wait(&v, &m);
fprintf(stderr, "t%d: balance = %d\n", (int)(unsigned long)args, balance);
balance -= 100; // 取¥100 大洋
// 解锁,走人
pthread_mutex_unlock(&m);
pthread_exit(NULL);
}
int main(int argc, char const *argv[])
{
if(argc != 2)
{
printf("Usage: %s <threads-number>\n", argv[0]);
return 1;
}
pthread_mutex_init(&m, NULL);
pthread_cond_init(&v, NULL);
// 循环地创建若干条线程
pthread_t tid;
int i, thread_nums = atoi(argv[1]);
for(i=0; i<thread_nums; i++)
{
pthread_create(&tid, NULL, routine, (void *)(unsigned long)i);
}
pthread_mutex_lock(&m); // 要往账号打钱,先加锁
balance += (thread_nums * 100); // 根据线程数目,打入¥
pthread_cond_broadcast(&v); // 通知所有正在等待的线程
pthread_mutex_unlock(&m);
pthread_exit(NULL);
return 0;
}
五、线程安全机制的比较:
信号量(有名、无名):P/V,只要有资源数就可以P/V
互斥锁:通过加锁和解锁实现互斥,使用不当容易死锁
读写锁:使用上跟互斥锁一样,区别是上锁的时候有两种方式
条件变量:对互斥锁的一个加强,当某个条件产生的时候才能做某件事情