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早已结束。
这时候,如果把上面代码中的P0~P3部分替换为Q0~Q1部分的话,问题可以得到解决:
当然,这个方法有着所有使用忙等待程序都有的问题:耗时。
2.2程序2.0
//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);
}
上面的代码不涉及临界区。问题不是一段代码一次只能被一个线程执行,而变成了线程my_rank在线程source发出消息前一直被阻塞。这种一个线程需要等待另一个线程执行某种操作的同步方式,有时称为生产者-消费者同步模型。
3后记
信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在 哪里)。而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这 个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的