系统编程-同步互斥机制

bcc0729 / 2023-08-31 / 原文

同步互斥机制

  1. 同步概念
    所谓同步,即同时起步,协调一致。不同对象,对于“同步”的理解方式不一样(略有不同)。如设备同步,是指在两个设备之间规定一个共同的时间作为参考;数据同步,是指让两个或多个数据库内容保持一致,或者按需要部分保持一致;文件同步,是指两个或者多个文件夹的文件保持一致等等
    但是在编程中,通信中说的同步与生活中大家印象中同步是不一样的。“同”,协同,协助,互相配合,主旨在协同步调。按照预定先后次序运行。

  2. 线程同步,指一个线程发出来某一个调用时,在没有得到结果之前,该调用不返回。同时其它线程为了保证数据的一致性,不能调用该功能。说白了就是线程在执行任务时有先后顺序,为了防止线程资源被抢占。“同步”的目的,是为了避免数据混乱,解决与时间有关的错误。实际上,不仅线程间需要同步,进程间,信号间等等都需要信号同步机制。

  3. 处理同步互斥方式有哪些

  • 信号量---------->进程
  • 有名信号量---------->进程
  • 无名信号量---------->线程
  • 互斥锁---------->线程
  • 读写锁---------->线程

同步互斥方式之有名信号量

  1. 有名信号量的特点
    有名信号量与信号量非常相似,但是信号量的值只能是0/1,而有名信号量可以是0~+∞
    信号量使用空间+数据来处理互斥,而有名信号量只使用数据来处理
  2. 有名信号量的函数接口
  • 创建并打开一个有名信号量------->sem_open()
#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <semaphore.h>
		// 函数作用:初始化并且打开一个有名信号量
		sem_t *sem_open(
			const char *name,       // 参数1:有名信号量的名字,要求必须以“/”开头
			int oflag            // 参数2:O_CREAT----->如果不存在就创建     O_CREAT|O_EXCL----->不存在就创建,存在就报错
		);
	   
	   
		sem_t *sem_open(const char *name, int oflag,
		mode_t mode,     // 参数3:八进制权限,例如0777
		unsigned int value    // 参数4:有名信号的起始值
		);
		// 成功返回:有名信号量的地址,失败返回:SEM_FAILED----->NULL/(sem_t*)-1
       Link with -pthread.

注意:如果oflag中有O_CREAT这个选项,则这个mode与value必须要填

  1. 有名信号量的P操作
  • P操作:资源数-1操作------->sem_wait()
#include <semaphore.h>

	int sem_wait(sem_t *sem);     //参数:有名信号量的地址
	// 返回值:成功0------>资源-1,失败-1

  1. 有名信号量的V操作
  • V操作:资源数+1操作------->sem_post()
#include <semaphore.h>

	int sem_post(sem_t *sem);      //参数:有名信号量的地址
	// 返回值:成功0------>资源+1,失败-1
	Link with -pthread.

  1. 关闭有名信号量--------sem_close()
#include <semaphore.h>

	int sem_close(sem_t *sem);//参数:有名信号量的地址
	// 返回值:成功0,失败-1
	
	Link with -pthread.

  1. 删除有名信号量--------sem_unlink()
#include <semaphore.h>

	int sem_unlink(const char *name);   //参数:有名信号量的名字
	// 返回值:成功0,失败-1
	Link with -pthread.

实操(有名信号量+共享内存):
写端

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/shm.h>
#include <fcntl.h>
#include <string.h>

#define SEM_NAME "/semname1"

int main(int argc, char const *argv[])
{
	// 1.获取key键值
	key_t key = ftok(".", 10);
	// 2.根据key值,获取共享内存的ID号
	int shm_id = shmget(key, 1024, IPC_CREAT|0666);
	// 3.根据ID号将共享内存映射到进程的虚拟内存空间内
	char* shm_p = shmat(shm_id, NULL, 0);
	if((void*)-1 == shm_p)
	{
		perror("shamat");
		return -1;
	}

	sem_t* sem_p = sem_open(SEM_NAME, O_CREAT, 0777, 0);
	if(SEM_FAILED == sem_p)
	{
		perror("sem_open");
		return -2;
	}

	while(1)
	{
		// 从键盘获取数据,存储到共享内存中
		scanf("%s", shm_p);
		// 有名信号量的V操作
		sem_post(sem_p);
		// 退出操作
		if(!strncmp(shm_p, "exit", 4))
			break;
	}

	// 解除映射
	shmdt(shm_p);

	// 删除映射空间
	shmctl(shm_id, IPC_RMID, NULL);

	// 关闭有名信号量
	sem_close(sem_p);

	// 删除有名信号量
	sem_unlink(SEM_NAME);

	return 0;
}

读端

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <signal.h>
#include <sys/shm.h>
#include <fcntl.h>
#include <string.h>

#define SEM_NAME "/semname1"

int main(int argc, char const *argv[])
{
	// 1.获取key键值
	key_t key = ftok(".", 10);
	// 2.根据key值,获取共享内存的ID号
	int shm_id = shmget(key, 1024, IPC_CREAT|0666);
	// 3.根据ID号将共享内存映射到进程的虚拟内存空间内
	char* shm_p = shmat(shm_id, NULL, 0);
	if((void*)-1 == shm_p)
	{
		perror("shamat");
		return -1;
	}

	sem_t* sem_p = sem_open(SEM_NAME, O_CREAT, 0777, 0);
	if(SEM_FAILED == sem_p)
	{
		perror("sem_open");
		return -2;
	}

	while(1)
	{
		// 有名信号量的P操作
		sem_wait(sem_p);
		printf("rev: %s\n", shm_p);
		// 退出操作
		if(!strncmp(shm_p, "exit", 4))
			break;
	}

	// 解除映射
	shmdt(shm_p);

	// 删除映射空间
	shmctl(shm_id, IPC_RMID, NULL);

	// 关闭有名信号量
	sem_close(sem_p);

	// 删除有名信号量
	sem_unlink(SEM_NAME);

	return 0;
}

同步互斥方式之无名信号量

  1. 什么是无名信号量
    一般作用于线程之间的互斥,由于是无名信号量,所以是没有名字的,不能用sem_open()打开
  2. 无名信号量的函数接口
  • 定义一个无名信号量
    sem_t sem-------无名信号不是一个文件,是一个变量
  • 初始化一个无名信号量------>sem_init()
#include <semaphore.h>

	int sem_init(
		sem_t *sem,       // 参数1:无名信号量的地址
		int pshared,     // 参数2: 0----->作用于线程之间,非0------>作用于进程之间
		unsigned int value     // 参数3:无名信号量的起始值
	);
	// 返回值:成功0,失败-1

	Link with -pthread.

3.无名信号量的操作

  • P操作:资源数-1操作------->sem_wait()
#include <semaphore.h>

	int sem_wait(sem_t *sem);     //参数:无名信号量的地址
	// 返回值:成功0------>资源-1,失败-1

  1. 无名信号量的V操作
  • V操作:资源数+1操作------->sem_post()
#include <semaphore.h>

	int sem_post(sem_t *sem);      //参数:无名信号量的地址
	// 返回值:成功0------>资源+1,失败-1
	Link with -pthread.

  1. 销毁无名信号量--------sem_destroy()
#include <semaphore.h>

	int sem_destroy(sem_t *sem);	//参数:无名信号量的地址

	Link with -pthread.

练习1:有一个进程,创建5个线程出来,每一个线程任务都是一样的
任务:将"helloworld"每隔1s打印一个字符。-->10s
练习2:有一个进程,创建5个线程出来,每一个线程任务都是一样的(使用无名信号量去处理同步互斥)
任务:将"helloworld"每隔1s打印一个字符。-->10s

  • 练习1
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* func(void* arg)
{
	char* p = "helloworld";
	while(*p)
	{
		printf("%c", *p);
		++p;
		fflush(stdout);
		sleep(1);
	}
	pthread_exit(NULL);
}

int main(int argc, char const *argv[])
{
	pthread_t thread[5];
	for (int i = 0; i < 5; ++i)
	{
		pthread_create(&thread[i], NULL, func, NULL);
	}

	for (int i = 0; i < 5; ++i)
	{
		pthread_join(thread[i], NULL);
	}

	puts("");


	return 0;
}
  • 练习2
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>

sem_t sem;

void* func(void* arg)
{
	sem_wait(&sem);

	char* p = "helloworld";
	while(*p)
	{
		printf("%c", *p);
		++p;
		fflush(stdout);
		sleep(1);
	}
	puts("");
	sem_post(&sem);
	pthread_exit(NULL);
}


int main(int argc, char const *argv[])
{
	sem_init(&sem,0,1);

	pthread_t thread[5];

	for (int i = 0; i < 5; ++i)
	{
		pthread_create(&thread[i], NULL, func, NULL);
	}

	for (int i = 0; i < 5; ++i)
	{
		pthread_join(thread[i], NULL);
	}
	puts("");

	sem_destroy(&sem);


	return 0;
}

stdout和stderr都是标准输出到屏幕,stdout是有缓冲区的,stderr是没有缓冲区的

同步互斥方式之互斥锁

  1. 什么是互斥锁
  • 互斥锁是专门用于处理线程互斥的一种方式,它有两种状态:上锁状态/解锁状态。如果互斥锁处于上锁状态,那么再上锁会阻塞,知道这把锁解开之后,才能上锁。解锁状态依然继续解锁,不会阻塞
  1. 注意
  • 同一时刻,只有一个线程持有该锁
  • 当线程A对某一全局变量加锁访问,线程B在访问前尝试加锁,会拿不到锁,线程B阻塞。一个线程C不去加锁,直接去访问该全局变量,依然可以访问,但会出现数据混乱
  • 所以,互斥锁实际上给操作系统提供了一把“建议锁“(又称“协同锁”),建议程序中有多线程访问共享资源的使用该机制。但是没有强制限定。因此即使有了mutex锁,如果有线程不按规则来访问数据,依然会造成数据踩踏
  1. 关于线程互斥锁函数接口
  • 定义互斥锁变量----->pthread_mutex_t
    pthread_mutex_t mutex
    pthread_mutex_t类型,其本质是一个结构体。
  • 初始化互斥锁------>pthread_mutex_init()
#include <pthread.h>
	int pthread_mutex_init(
		pthread_mutex_t *mutex,  // 参数1:互斥锁变量的地址
		const pthread_mutexattr_t *mutexattr // 参数2:普通属性,NULL
	);
	返回值:成功0, 失败:非0错误码
  • 上锁------>pthread_mutex_lock()
#include <pthread.h>
	int pthread_mutex_lock(pthread_mutex_t *mutex);  // 参数1:互斥锁变量的地址
	返回值:成功0, 失败:非0错误码
  • 解锁------>int pthread_mutex_unlock()
#include <pthread.h>
	int pthread_mutex_unlock(pthread_mutex_t *mutex);  // 参数1:互斥锁变量的地址
	返回值:成功0, 失败:非0错误码
  • 销毁------>int pthread_mutex_destroy()
#include <pthread.h>
	int pthread_mutex_destroy(pthread_mutex_t *mutex);  // 参数1:互斥锁变量的地址
	返回值:成功0, 失败:非0错误码
  1. lock和unlock
    lock尝试加锁,如果加锁不成功,线程阻塞,阻塞到持有该互斥量的其它线程解锁为止。unlock主动解锁函数,同时将阻塞在该锁上的所有线程全部唤醒,至于哪个线程先被唤醒,取决于优先级,调度。默认是谁先被阻塞,谁先被唤醒
    例如:P1,P2,P3,P4使用一把mutex锁。P1加锁成功,其他线程都阻塞,直到P1解锁。P1解锁后,P2,P3,P4均被唤醒,并自动
    再次尝试加锁。
    可以理解为:mutex锁init成功初值为1。lock功能是将mutex--。而unlock则将mutex++;

  2. lock和trylock
    lock加锁失败会阻塞,等待锁释放。trylock加锁失败会直接返回错误号(如:EBUSY)

实操

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int main_val = 0;
pthread_mutex_t mutex;

void* func1(void* arg)
{
	pthread_mutex_lock(&mutex);
	main_val = 100;
	sleep(2);

	printf("%d\n", main_val);
	pthread_mutex_unlock(&mutex);
	pthread_exit(NULL);
}

void* func2(void* arg)
{
	pthread_mutex_lock(&mutex);
	sleep(1);
	main_val = 200;
	
	printf("%d\n", main_val);
	pthread_mutex_unlock(&mutex);
	pthread_exit(NULL);
}

int main(int argc, char const *argv[])
{
	pthread_t tid1;
	pthread_t tid2;

	pthread_create(&tid1, NULL, func1, NULL);
	pthread_create(&tid2, NULL, func2, NULL);

	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);

	return 0;
}

死锁

  1. 线程试图对同一互斥量加锁两次
  2. 线程1拥有A锁,请求获得B锁;线程2拥有B锁,请求获得A锁