-->

线程的同步和互斥


线程的互斥和同步

使用线程太多也会造成一定的困扰,就比如同时访问临界资源的使用,就不知道是哪一个线程更改了这个临界资源,导致读取的时候,让读出来的数据也不知道是什么,那么我们需要如何避免这个问题呢?

  • 答:只需要让一个线程在访问临界资源的时候,其他的线程都不可以访问这个资源,从根源上解决问题。

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;
}
posted @ 2024-06-03 08:52  wuju  阅读(23)  评论(0)    收藏  举报