c语言并行程序设计之路(三)(信号量与同步)

0.前言

c语言并行程序设计之路(二)(忙等待与锁)中提到,忙等待总是浪费CPU的资源,但它能事先确定线程执行临界区代码的顺序;如果采用互斥量,那么那个线程先进入临界区以及此后的顺序由系统随机选取。

因为加法计算是可交换的,所以(pi)计算程序的结果不受线程执行顺序的影响。但是,仍然有情况需要控制线程进入临界区的顺序,比如矩阵乘法。

1.问题描述

假设有t个线程,线程0向线程1发送消息,线程1向线程2发送消息,...,线程t-2向线程t-1发送消息,线程t-1向线程0发送消息。当一个线程“接收”一条消息后,它打印消息并终止。为了实现消息的传递,分配了一个char*类型的共享数组,每个线程初始化信息后,就设定这个共享数组中的指针指向要发送的消息。为了避免引用到没被定义的指针,主线程将共享数组中的每项先设定为NULL

2.程序

2.1程序1.0

//message1.c
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#define ll long long
#define MSG_MAX 233
#define MAXN 1000
char* messages[MAXN];
int thread_count;
void* Send_msg(void* rank) {

	ll my_rank = (ll) rank;
	ll dest = (my_rank+1)%thread_count;
	ll source = (my_rank+thread_count-1)%thread_count;
	
	char* my_msg = malloc(MSG_MAX*sizeof(char));
	
	sprintf(my_msg,"Hello to %lld from %lld",dest,my_rank);
	messages[dest] = my_msg;

	if(messages[my_rank] != NULL)//P0
		printf("Thread %lld > %s
",my_rank,messages[my_rank]);//P1
	else//P2
		printf("Thread %lld > No message from %lld 
",my_rank,source);//P3

//	while(messages[my_rank] == NULL); //Q0
//	printf("Thread %lld > %s
",my_rank,messages[my_rank]);//Q1

	return NULL;
}

int main(int argc,char* argv[]) {

	ll thread;
	thread_count = strtol(argv[1],NULL,10);
	pthread_t* thread_handles = (pthread_t*)malloc(thread_count*sizeof(pthread_t));

	for(thread=0; thread<thread_count; ++thread) {
		pthread_create(&thread_handles[thread],NULL,Send_msg,(void*)thread);
	}
	for(thread=0; thread<thread_count; ++thread) {
		pthread_join(thread_handles[thread],NULL);
	}

	free(thread_handles);
}

当在双核系统上运行多于两个线程的程序时,有些消息始终未收到。例如,在线程t-1把消息复制到message数组前,最先运行的线程0早已结束。

image-20200325153557505

这时候,如果把上面代码中的P0~P3部分替换为Q0~Q1部分的话,问题可以得到解决:

image-20200325153724184

当然,这个方法有着所有使用忙等待程序都有的问题:耗时。

2.2程序2.0

image-20200325154906086

//message2.c
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>
#include<semaphore.h>
#define ll long long
#define MSG_MAX 233
#define MAXN 1000
char* messages[MAXN];
int thread_count;
sem_t* semaphores;
void* Send_msg(void* rank) {

	ll my_rank = (ll) rank;
	ll dest = (my_rank+1)%thread_count;
//	ll source = (my_rank+thread_count-1)%thread_count;
	
	char* my_msg = malloc(MSG_MAX*sizeof(char));

	sprintf(my_msg,"Hello to %lld from %lld",dest,my_rank);
	messages[dest] = my_msg;

	sem_post(&semaphores[dest]);

	sem_wait(&semaphores[my_rank]);

	printf("Thread %lld > %s
",my_rank,messages[my_rank]);//Q1

	return NULL;
}

int main(int argc,char* argv[]) {

	ll thread;
	thread_count = strtol(argv[1],NULL,10);
	pthread_t* thread_handles = (pthread_t*)malloc(thread_count*sizeof(pthread_t));
	semaphores = (sem_t*)malloc(thread_count*sizeof(sem_t));

	for(thread=0; thread<thread_count; ++thread) {
		sem_init(&semaphores[thread],0,0);
	}
	for(thread=0; thread<thread_count; ++thread) {
		pthread_create(&thread_handles[thread],NULL,Send_msg,(void*)thread);
	}
	for(thread=0; thread<thread_count; ++thread) {
		pthread_join(thread_handles[thread],NULL);
	}
	for(thread=0; thread<thread_count; ++thread) {
		sem_destroy(&semaphores[thread]);
	}

	free(thread_handles);
}

image-20200325162202946

上面的代码不涉及临界区。问题不是一段代码一次只能被一个线程执行,而变成了线程my_rank在线程source发出消息前一直被阻塞。这种一个线程需要等待另一个线程执行某种操作的同步方式,有时称为生产者-消费者同步模型

3后记

信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在 哪里)。而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这 个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的

原文地址:https://www.cnblogs.com/wangzhebufangqi/p/12567041.html