线程的同步与互斥

思考:由于进程中创建出来的线程是并发执行的,也就是可能会出现线程之间抢占资源的情况,请问应该如何避免?
回答:可以利用线程间的同步和互斥机制,来达到线程对资源的有序访问,同步指的是控制两个进度使之有先有后,次序可控,而互斥指的是控制两个进度使之互相排斥,不同时运行。
读写锁:
思考:互斥锁是为了防止多个线程对共享资源访问时破坏资源,但是如果某些线程只是为了获取共享资源的值,而不打算修改共享资源,这样所有的线程对共享资源访问的时候都使用互斥锁,就会导致程序的效率下降,请问如何解决这种问题?
回答:对于互斥锁而言,凡是涉及临界资源的访问一律加锁,这在并发读操作的场景下会大量浪费时间。要想提高访问效率,就必须要将对资源的读写操作加以区分:读操作可以多任务并发执行,只有写操作才进行恰当的互斥。Linux系统就提供了一种读写锁。
image
(1)初始化读写锁
image
(2)加锁解锁操作
读写锁最大的特点是对即将要做的读写操作做了区分:读操作可以共享,因此多条线程可以对同一个读写锁加多重读锁。写操作天然互斥,因此多条线程中只能有一条线程拥有写锁。
image
如果只对数据进行读操作,那么就填加读锁。如果要对数据进行写操作,那么就填加写锁。
image
注意:读写锁适合多个线程对共享资源进行读操作和写操作,并且写操作没有读操作频繁的情况,另外要注意,如果多条线程对资源进行读取时,可以添加多把读操作锁。
注意:如果有一条线程准备对资源进行修改,则不应该有其他的线程再对资源进行读或者写,就相当于写操作和读操作是互斥的。
另外,如果已经有一条线程获取了写操作锁,那其他线程就无法获取写操作锁,就需要进行阻塞等待,所以就相当与写操作和写操作是互斥的。
练习:

点击查看代码
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>


//临界资源,应该使用volatile进行修饰,防止编译器对该变量进行优化
volatile int data = 10;  

//读写锁对象,必须是全局变量
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;



//子线程B的任务,格式是固定的
void * task_B(void *arg)
{
	//线程任务应该是死循环,并且不会退出
	while(1)
	{
		//获取读操作的锁
		pthread_rwlock_rdlock(&rwlock);

		//对临界资源进行读操作
		printf("I am Thread_B,data = %d\n",data);
		sleep(1);

		//释放读操作的锁
		pthread_rwlock_unlock(&rwlock);	
	}
}

//子线程C的任务,格式是固定的
void * task_C(void *arg)
{
	//线程任务应该是死循环,并且不会退出
	while(1)
	{
		//获取读操作的锁
		pthread_rwlock_rdlock(&rwlock);

		//对临界资源进行读操作
		printf("I am Thread_C,data = %d\n",data);
		sleep(1);

		//释放读操作的锁
		pthread_rwlock_unlock(&rwlock);		
	}
}

//主线程  A
int main(int argc, char const *argv[])
{	
	//1.对创建的读写锁对象进行初始化
	pthread_rwlock_init(&rwlock,NULL);

	//2.创建子线程	
	pthread_t thread_B;
	pthread_t thread_C;

	pthread_create(&thread_B,NULL,task_B,NULL); 
	pthread_create(&thread_C,NULL,task_C,NULL); 

	//3.进入死循环,主线程需要对临界资源进行修改
	while(1)
	{
		//主线程会阻塞等待,10s会解除阻塞
		sleep(10);

		//获取写操作的锁
		pthread_rwlock_wrlock(&rwlock);

		//对临界资源进行读操作
		data += 20;
		printf("I am main_Thread,data = %d\n",data);
		sleep(5);

		//释放写操作的锁
		pthread_rwlock_unlock(&rwlock);	
	}

	return 0;
}

POSIX信号量
信号量可以用于多个不同进程间或者同一个进程中多个不同线程间进行同步的方案,Linux系统提供了POSIX信号量和IPC对象中的信号量集供用户使用。
POSIX信号量一般用于描述一种共享资源的状态,Linux系统把POSIX信号量分为两种:一种是POSIX匿名信号量,另一种则是POSIX具名信号量,两者区别如下所示:
(1)POSIX匿名信号量
Linux系统中的信号量可以用于进程间通信,也可以用于同一个进程中多个线程的通信,而POSIX匿名信号量则是专门在进程中的多个线程间进行使用的一种信号量,也就是只存在于内存中。
A.初始化POSIX匿名信号量
Linux系统中提供了一个名称叫做sem_init()的函数接口,用户利用该接口可以对POSIX匿名信号量进行初始化。
image
B.对POSIX信号量进行P/V
对于信号量而言,都是对资源的一种数量的表示,所以进程或者线程在有限的资源进行访问时都需要进行申请和释放,所以Linux系统下提供了两个函数来实现POSIX匿名信号量的P/V操作,分别是sem_wait()和sem_post()。
image
image
(2)POSIX具名信号量
POSIX匿名信号量主要是在进程中的多条线程中使用,而POSIX具名信号量则主要是在多个进程中使用,并且可以存储在根文件系统的 /dev/shm 目录下,可以被系统中任意有权限的进程访问。
A.创建或者打开POSIX具名信号量
由于POSIX具名信号量有自己的文件名称,所以Linux系统提供了一个名称叫做sem_open()的函数接口,用户可以利用该接口创建或者打开POSIX具名信号量。
image
B.POSIX具名信号量进行P/V操作
对于信号量而言,都是对资源的一种数量的表示,所以进程或者线程在有限的资源进行访问时都需要进行申请和释放,所以Linux系统下提供了两个函数来实现POSIX具名信号量的P/V操作,分别是sem_wait()和sem_post()。
image
image
注意:如果POSIX具名信号量使用完成后,应该及时关闭,Linux系统提供了名字叫做sem_close()的函数实现该操作。
image

点击查看代码
#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <semaphore.h>
#include <sys/shm.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdlib.h>
#include <unistd.h>



#define SHM_SIZE  4

int main(int argc, char const *argv[])
{
	
	//1.打开一个共享内存,如果不存在则创建
	int shm_id = shmget(ftok(".",50),SHM_SIZE,IPC_CREAT|IPC_EXCL|0644);
	
	
	if (shm_id == -1)
	{
		if(errno == EEXIST)
		{
			//此时可以再次调用该函数打开共享内存
			shm_id = shmget(ftok(".",50),4,0644);
		}
		else
		{
			fprintf(stderr, "shmget error,errno:%d,%s\n",errno,strerror(errno));
			exit(1);
		}
	}

	//2.把打开的共享内存段映射到自己的进程空间中
	int * pshm = (int *)shmat(shm_id,NULL,0);

	//3.打开一个POSIX有名信号量,如果不存在则创建
	sem_t * psem = sem_open("named_sem", O_CREAT|O_EXCL,0644,0);

	if(psem == SEM_FAILED)
	{
		if(errno == EEXIST)
		{
			//此时可以再次调用该函数打开共享内存
			psem = sem_open("named_sem",0);
		}
		else
		{
			fprintf(stderr, "sem_open error,errno:%d,%s\n",errno,strerror(errno));
			exit(2);
		}
	}

	//4.进程A先对临界资源进行访问,然后对POSIX信号量进行V操作
	
	*pshm = getpid();

	sem_post(psem);
	

	return 0;
}
点击查看代码
#include <fcntl.h>           /* For O_* constants */
#include <sys/stat.h>        /* For mode constants */
#include <semaphore.h>
#include <sys/shm.h>
#include <stdio.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <stdlib.h>
#include <unistd.h>


#define SHM_SIZE  4

int main(int argc, char const *argv[])
{
	
	//1.打开一个共享内存,如果不存在则创建
	int shm_id = shmget(ftok(".",50),SHM_SIZE,IPC_CREAT|IPC_EXCL|0644);
	
	
	if (shm_id == -1)
	{
		if(errno == EEXIST)
		{
			//此时可以再次调用该函数打开共享内存
			shm_id = shmget(ftok(".",50),4,0644);
		}
		else
		{
			fprintf(stderr, "shmget error,errno:%d,%s\n",errno,strerror(errno));
			exit(1);
		}
	}

	//2.把打开的共享内存段映射到自己的进程空间中
	int * pshm = (int *)shmat(shm_id,NULL,0);

	//3.打开一个POSIX有名信号量,如果不存在则创建
	sem_t * psem = sem_open("named_sem", O_CREAT|O_EXCL,0644,0);

	if(psem == SEM_FAILED)
	{
		if(errno == EEXIST)
		{
			//此时可以再次调用该函数打开共享内存
			psem = sem_open("named_sem",0);
		}
		else
		{
			fprintf(stderr, "sem_open error,errno:%d,%s\n",errno,strerror(errno));
			exit(2);
		}
	}

	//4.死循环,防止进程终止
	while(1)
	{
		//进程B必须等待进程A把PID写入到共享内存中之后才可以输出共享内存段中的数据
		
		//进程B需要进行P操作
		sem_wait(psem);

		//访问临界资源
		printf("shared memory data is [%d]\n",*pshm);
	}
	
	return 0;
}

条件量
在许多场合中,程序的执行通常需要满足一定的条件,条件不成熟的时候,任务应该进入睡眠阻塞等待,条件成熟时应该可以被快速唤醒。另外,在并发程序中,会其他任务同时访问该条件,因此任何时候都必须以互斥的方式对条件进行访问。条件量就是专门解决上述场景的逻辑机制。
注意:条件和条件量是两个不同的东西,条件是指程序要继续运行所需要的前提条件,比如文件是否读完、内存是否清空等具体的场景限定。而条件量(即pthread_cond_t)要讨论的是一种同步互斥变量,专用于解决上述逻辑场景。
操作流程:在进行条件判断前,先加锁(防止其他任务并发访问),成功加锁后判断条件是否允许,若条件允许,则直接操作临界资源,然后释放锁。若条件不允许,则进入条件量的等待队列中睡眠,并同时释放锁。
在条件量中睡眠的任务,可以被其他任务唤醒,唤醒时重新判定条件是否允许程序继续执行,当然也是必须先加锁。
条件量一般要跟互斥锁(或二值信号量)配套使用,互斥锁提供锁住临界资源的功能,条件量提供阻塞睡眠和唤醒的功能。
(1)初始化条件量
image
(2)进入等待状态
image
(3)唤醒等待任务
image
(4)唤醒所有任务
image

posted @ 2025-08-11 21:08  Lè_Sage  阅读(32)  评论(0)    收藏  举报