线程的同步和互斥
线程的互斥和同步
使用线程太多也会造成一定的困扰,就比如同时访问临界资源的使用,就不知道是哪一个线程更改了这个临界资源,导致读取的时候,让读出来的数据也不知道是什么,那么我们需要如何避免这个问题呢?
- 答:只需要让一个线程在访问临界资源的时候,其他的线程都不可以访问这个资源,从根源上解决问题。
Linux系统为我们提供了读写锁。
- 在一个线程更改临界资源时,其他的线程读或写都不可以继续访问,但是在一个线程读取的时候其他的线程也是可以读取的。总结只要有线程写入,那么其他的线程都是不可以写入和读取的,但是在一个线程读取的时候是可以有其他的线程读取的。
读写锁
- 初始化读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//第一个参数是摧毁的锁
//返回值,成功返回0,失败返回错误码
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
//第一个参数是初始化的锁
//第二个参数是读写锁的属性
//返回值,成功返回0,失败返回错误码
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
//初始化的锁,一定是在初始化的时候给的赋值
- 写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//第一个参数是需要上写锁的锁(这个函数不会阻塞(尝试上锁),如果没锁,也就是锁被其他的写锁)
//返回值,成功返回0,失败返回错误码
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//第一个参数是需要上写锁的锁(这个函数会阻塞,如果没锁,也就是锁被其他的写锁)
//返回值,成功返回0,失败返回错误码
- 读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//第一个参数是需要上读锁的锁(这个函数会阻塞,如果没锁,也就是锁被其他的写锁)
//返回值,成功返回0,失败返回错误码
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//第一个参数是需要上读锁的锁(这个函数不会阻塞(尝试上锁),如果没锁,也就是锁被其他的写锁)
//返回值,成功返回0,失败返回错误码
- 解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//第一个参数是需要解锁(读写都使用这一个)
//返回值,成功返回0,失败返回错误码
- Example
写一个程序,主线程读取实时时间,并且写入到文本中,在写两个线程,然后A线程从文本中读取出日期,线程B从文本中读取时分秒,并且输出到终端。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <sys/types.h>
#include <pthread.h>
//初始化读写锁
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
//用来记录最后一次写入文本的字节大小
volatile int rd_size;
// 线程A
void *task_A(void *arg)
{
FILE *fp;
char ch[128] = {'\0'};
char *p;
//让主线程先写入数据
sleep(5);
while (1)
{
// 读锁
pthread_rwlock_rdlock(&rwlock);
fp = fopen("text.txt", "ab+");
if (NULL == fp)
{
fprintf(stderr, "fopen is failed,error %d,%s\n", errno, strerror(errno));
return -1;
}
// 将光标偏移回这一行的开始
fseek(fp, -rd_size, SEEK_END);
// 读取文本中一行的数据
fgets(ch, sizeof(ch), fp);
fclose(fp);
// 分割出日期
p = strtok(ch, "|");
//解锁
pthread_rwlock_unlock(&rwlock);
printf("%s\n", p);
sleep(1);
}
}
// 线程B
void *task_B(void *arg)
{
FILE *fp;
char ch[128] = {'\0'};
char *p;
sleep(5);
while (1)
{
// 读锁
pthread_rwlock_rdlock(&rwlock);
fp = fopen("text.txt", "ab+");
if (NULL == fp)
{
fprintf(stderr, "fopen is failed,error %d,%s\n", errno, strerror(errno));
return -1;
}
// 将光标偏移回这一行的开始
fseek(fp, -rd_size, SEEK_END);
// 读取文本中一行的数据
fgets(ch, sizeof(ch), fp);
fclose(fp);
// 分割出时间
p = strtok(ch, "|");
p = strtok(NULL, "|");
//解锁
pthread_rwlock_unlock(&rwlock);
printf("%s\n", p);
sleep(1);
}
}
int main()
{
// 打开文件
FILE *fp;
time_t second = time(NULL);
struct tm *t = localtime(&second);
// 创建A线程读取文本中的年月日
pthread_t pthreadidA;
pthread_create(&pthreadidA, NULL, task_A, NULL);
// 创建B线程读取文本中的时分秒
pthread_t pthreadidB;
pthread_create(&pthreadidB, NULL, task_B, NULL);
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
// 定义变量存储写入字符
char ch[128] = {'\0'};
while (1)
{
second = time(NULL);
t = localtime(&second);
// 写锁
pthread_rwlock_wrlock(&rwlock);
//写入在自定义的缓冲区(数组)以便于计算它的大小,让读取的时候偏移回这么多个字节
sprintf(ch, "%d年%d月%d日|%2d:%2d:%2d\n", t->tm_year + 1900, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
fp = fopen("text.txt", "ab+");
if (NULL == fp)
{
fprintf(stderr, "fopen is failed,error %d,%s\n", errno, strerror(errno));
return -1;
}
//每一次写入都要将光标偏移到最后
fseek(fp, 0, SEEK_END);
//写入文本
fwrite(ch, 1, strlen(ch), fp);
//计算写入的大小
rd_size = strlen(ch);
fflush(fp);
fclose(fp);
//需要解锁,如果不解锁,后面所有的程序都不能使用,会导致死锁
pthread_rwlock_unlock(&rwlock);
sleep(5);
}
}
POSIX信号量
这个POSIX的全称叫做(Portable Operating System Interface)可移植操作系统接口,在上面讲述的信号量,这个可移植操作系统接口相当于上面的封装,把PV操作都给封装起来。
在这个信号量中,既可以迫使进程中的线程间的同步和互斥无名信号量,也可以使得进程与进程的互斥和同步有名信号量。
有名信号量是真实存在的一般而言创建的时候是一个文件在,一般都在这个目录下面/dev/shm,没有创建时是没有的。
- 初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
//第一个参数是初始化的信号量
//第二个参数,如果是0创建的信号量是线程之间的,如果是非0那么创建的信号量是可以在进程之间使用的
//第三个参数是信号的个数
//返回值,返回0成功,返回-1失败
int sem_destroy(sem_t *sem);
//第一个参数是需要摧毁的信号量
//返回值,返回0成功,返回-1失败
- 无名信号量
-
- 信号量的P操作
wait有等待的含义,只有p操作才会阻塞、等待。
int sem_wait(sem_t *sem);
//第一个参数是对哪一个信号量左P操作(会阻塞)
//返回值,返回0成功,返回-1失败
int sem_trywait(sem_t *sem);
//第一个参数是对哪一个信号量左P操作(尝试P操作,不会阻塞)
//返回值,返回0成功,返回-1失败
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
//第一个参数是对哪一个信号量左P操作(等待固定的时间,阻塞一会)
//第二个参数是阻塞的时间,是一个结构体如下,选择一个设置即可
struct timespec {
time_t tv_sec; /* Seconds 秒*/
long tv_nsec; /* Nanoseconds [0 .. 999999999] 纳秒*/
};
//返回值,返回0成功,返回-1失败
-
- 信号量的V操作
int sem_post(sem_t *sem);
//第一个参数是需要V操作的信号量
//返回值,返回0成功,返回-1失败
- 有名信号量
有名信号量的PV操作和上面的一样,创建或者打开一个有名信号量使用的方式不一样
-
- 创建或者打开一个有名信号量
//这个函数可以有两个或者四个参数
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag,mode_t mode, unsigned int value);
//第一个参数是信号量的名字,他会被自动的存放在/dev/shm
//第二个参数是标志位这个标志位使用的宏和open相同O_CREAT和O_EXCL,没有需求就是默认0
//第三个参数是权限一般都是0644
//第四个参数是信号的值
//返回值,返回信号量的地址,下面存有初始化好的信号量,返回SEM_FAILED出错
- Example
int main(int argc, char const *argv[])
{
// 创建共享内存
int shm_id = shmget(ftok(".", 4), 4, 0644 | IPC_CREAT | IPC_EXCL);
if (shm_id == -1)
{
fprintf(stderr, "shmget is failed,error %d,%s\n", errno, strerror(errno));
//如果已有共享内存就打开
shm_id = shmget(ftok(".", 4), 4, 0644);
if (shm_id == -1)
{
fprintf(stderr, "shmget is failed,error %d,%s\n", errno, strerror(errno));
return -1;
}
}
//获取共享内存的地址
int *shm_addr = shmat(shm_id, NULL, 0);
if (shm_addr == (void *)-1)
{
fprintf(stderr, "shmat is failed,error %d,%s\n", errno, strerror(errno));
return -2;
}
// 创建有名信号量
sem_t *sem = sem_open("posix_sem", O_CREAT | O_EXCL, 0644, 1);
if (sem == SEM_FAILED)
{
fprintf(stderr, "sem_open is failed,error %d,%s\n", errno, strerror(errno));
//信号量存在就打开
sem = sem_open("posix_sem", 0);
if (sem == SEM_FAILED)
{
fprintf(stderr, "sem_open is failed,error %d,%s\n", errno, strerror(errno));
return -3;
}
}
pid_t pid1;
while (1)
{
sem_wait(sem);
//把自己的ID写入共享内存中
pid1 = getpid();
*shm_addr = pid1;
sem_post(sem);
sleep(2);
}
return 0;
}
/*另一个程序main函数的whiel循环*/
sem_wait(sem);
//直接读取共享内存的数据
printf("%d\n", *shm_addr);
sem_post(sem);
sleep(1);
条件量
在线程中,如果没有条件量,那么放一个条件不满足的时候他也会一直判断你这个条件是否满足,非常占用CPU的使用率,使用条件量就可以使得党建天满足的时候才会执行,不满足一直处于挂起状态。
- 初始化条件量
int pthread_cond_destroy(pthread_cond_t *cond);
//第一个参数是用于摧毁的条件量
//返回值,成功返回0,失败返回错误码
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
//第一个参数是用于初始化的条件量
//第二个参数是属性,想要默认属性就是NULL
//返回值,成功返回0,失败返回错误码
//这个就是需要初始化的条件量,必须初始化赋值
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
- 条件量的挂起
使用条件量是需要定义互斥量的。
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; /* Seconds 秒*/
long tv_nsec; /* Nanoseconds [0 .. 999999999] 纳秒*/
};
//返回值,成功返回0,失败返回错误码
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
//调用这个函数,此线程会被挂起,只有等待其他对线程唤醒,CPU才会重新回来执行这个线程,这个线程也会自己解锁互斥锁。
//第一个参数是需要挂起的条件量
//第二个参数是互斥量
//返回值,成功返回0,失败返回错误码
- 条件的唤醒
注意挂起的时候,按照顺序挂起,先挂起的在队列前面。
int pthread_cond_broadcast(pthread_cond_t *cond);
//第一个参数,唤醒的挂起的条件量,广播唤醒,唤醒正在cond条件量所有的挂起状态
//返回值,成功返回0,失败返回错误码
int pthread_cond_signal(pthread_cond_t *cond);
//第一个参数,唤醒的挂起的条件量,唤醒挂起的第一个条件量
//返回值,成功返回0,失败返回错误码
- Example
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int x = 10,y = 20;
void * task_A(void * arg)
{
while(1)
{
pthread_mutex_lock(&mutex);//上锁
//当条件不满足是就会执行下面的循环语句然后被挂起
while( x<=y )
{
//在里面会自动解锁
pthread_cond_wait(&cond,&mutex);
}
pthread_mutex_unlock(&mutex);//解锁
}
}
void * task_B(void * arg)
{
while(1)
{
//延时5秒后让线程以解出挂起状态,然后由于没有改变xy,线程A又会处于挂起状态
sleep(5);
pthread_mutex_lock(&mutex);//上锁
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mutex);//解锁
}
}
int main()
{
pthread_t pthreadidA;
pthread_t pthreadidB;
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
pthread_create(&pthreadidA, NULL, task_A, NULL);
pthread_create(&pthreadidB, NULL, task_B, NULL);
pthread_exit(NULL);
return 0;
}

浙公网安备 33010602011771号