实验一 进程(线程)同步与互斥的经典问题
1. 哲学家就餐问题的实现
2. 生产者消费者问题实现
Ubuntu下编译环境
熟悉Ubuntu系统下的多线程编程。
使用“Ctrl+Alt+T”打开终端;或者使用图形界面打开终端
使用gedit或vim命令(如果有)打开文本编辑器进行编码:“gedit xxx.c”
编译程序:“gcc 文件名.c -o 可执行程序名”(如果只输入gcc文件名.c,默认可执行程序名为a.out)使用线程库时,gcc编译需要添加-lpthread,
例如: gcc -o xxx xxx.c -lpthread
执行程序:./xxx
实验1-哲学家就餐问题的实现
实验目的:实现哲学家就餐问题,要求不能出现死锁。通过本实验熟悉Linux系统的基本环境,了解Linux下进程和线程的实现。
实验内容:在linux系统下实现教材2.5.2节中所描述的哲学家就餐问题。要求显示出每个哲学家的工作状态,如吃饭,思考。连续运行30次以上都未出现死锁现象。
实验原理
该问题是描述有五个哲学家共用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗和五只筷子,他们的生活方式是交替地进行思考和进餐。
平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。
进餐完毕,放下筷子继续思考。
实验分析
1) 关系分析。5名哲学家与左右邻居对其中间筷子的访问是互斥关系。
2) 为了实现哲学家对筷子的获取关系,显然这里有五个进程(线程)。实验的关键是如何让一个哲学家拿到左右两个筷子而不造成死锁或者饥饿现象。
3) 用信号量或者互斥量或者对5个筷子的互斥访问。 对哲学家按顺序从0~4编号,哲学家i左边的筷子的编号为i,哲学家右边的筷子的编号为(i+1)%5。
semaphore chopstick[5] = {1,1,1,1,1}; //定义信号量数组
pthread_mutex_t chopstick[5] //或者定义互斥量数组
POSIX线程函数
头文件:
#include<pthread.h>
数据类型:
pthread_t; 线程ID
线程相关函数:
pthread_t pthread_self();//返回调用线程的线程ID
int pthread_create(pthread_t *tidp, const pthread_attr_t attr, void *(*start_rtn)(void*), void *arg);
参数
tidp:指向线程ID的指针,当函数成功返回时将存储所创建的子线程ID
attr:用于定制各种不同的线程属性(一般直接传入空指针NULL,采用默认线程属性)
start_rtn:线程的启动例程,即新创建的线程从该函数开始执行。该函数只有一个参数,即arg
arg:作为start_rtn的第一个参数
返回值
成功返回0,出错时返回各种错误码
void pthread_exit(void *rval_ptr);rval_ptr指向线程返回值。
线程退出有以下几种情况:
线程从例程函数返回。
线程被其它线程取消。
线程调用pthread_exit()主动退出。
int pthread_join(pthread_t thread, void **rval_ptr);
返回值:成功返回0,错误返回错误编号
此函数用于等待指定线程(由参数thread指定)运行结束,期间,调用线程将会阻塞。 rval_ptr参数用于获取线程返回值。
POSIX中互斥量
#include <pthread.h>
数据类型:
pthread_mutex_t //互斥量
pthread_mutexattr_t //互斥量的属性
互斥量相关的函数:
int pthread_mutex_init(pthread_mutex_t *mutex,
const pthread_mutexattr_t *attr);//对一个互斥量进行初始化
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//销毁一个互斥量
返回值:成功则返回0,否则返回错误编号
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功则返回0,否则返回错误编号
这三个函数用于对互斥量进行加锁解锁。pthread_mutex_trylock是非阻塞的加锁函数,若加锁失败,则立即返回EBUSY。
互斥量示例
pthread_mutex_t mutex; // 互斥信号量, 一次只有一个线程访问缓冲
pthread_mutex_init(&mutex, NULL);
//1.加锁,保证互斥的访问缓冲区
pthread_mutex_lock(&mutex);
…//2.处理缓冲区的代码
//3.解锁
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&mutex);
信号量
#include<semaphore.h>
信号量数据类型:sem_t
主要函数:
sem_init(sem_t *sem, int pshared, unsigned int value);//初始化一个无名信号量
sem_destroy(sem_t *sem);//销毁一个无名信号量
返回值:成功返回 0;错误返回 -1,并设置errno 。
sem_post(sem_t *sem);//信号量值加1。若有线程阻塞于信号量sem,则调度器会唤醒对应阻塞队列中的某一个线程。
sem_wait(sem_t *sem);//若sem小于0,则线程阻塞于信号量sem,直到sem大于0。否则信号量值减1。
sem_trywait(sem_t *sem);//功能同sem_wait(),但此函数不阻塞,若sem小于0,直接返回。
返回值:成功返回0,错误返回-1,并设置errno 。
信号量示例
sem_t sem;
sem_init(&sem, 0, 1);//初始化一个值为1的信号量
sem_wait(&sem);//获取信号量,相当于P操作
//do somthing
sem_post(&sem);//释放信号量,相当于V操作
sem_destroy(&sem);//销毁一个无名信号量
加锁和P/V操作
1)加锁操作在临界资源已经被占用的情况下,仍要循环测试锁的状态,这将耗费大量的CPU时间
2)使用加锁法实现互斥时,有可能在进程使用临界区时造成不公平现象(即是某个进程一直占有临界区 ,其他进程永远无法使用)
实验1-主程序流程
(MAIN函数)
每位哲学家流程图
(哲学家行为模式即代码都一致)
实验1-解决死锁问题的思考
A.至多只允许四个哲学家同时进餐,以保证至少有一个哲学家能够进餐,最终总会释放出他所使用过的两支筷子,从而可使更多的哲学家进餐。
B。仅当哲学家的左右两支筷子都可用时,才允许他拿起筷子进餐。(AND信号量)
C。取左侧和右侧筷子的操作进行保护,使之成为一个原子操作
D。规定奇数号的哲学家先拿起他左边的筷子,然后再去拿他右边的筷子;而偶数号的哲学家则相反.
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <pthread.h>
#include <errno.h>
#include <math.h>
//筷子作为mutex
pthread_mutex_t chopstick[6] ;
void *eat_think(void *arg)
{char phi = *(char *)arg;int left,right; //左右筷子的编号switch (phi){case 'A':left = 5;right = 1;break;case 'B':left = 1;right = 2;break;case 'C':left = 2;right = 3;break;case 'D':left = 3;right = 4;break;case 'E':left = 4;right = 5;break;}int i;for(;;){usleep(800000); //思考pthread_mutex_lock(&chopstick[left]); //拿起左手的筷子printf("Philosopher %c fetches chopstick %d\n", phi, left);if (pthread_mutex_trylock(&chopstick[right]) == EBUSY){ //拿起右手的筷子pthread_mutex_unlock(&chopstick[left]); //如果右边筷子被拿走放下左手的筷子printf("Philosopher %c release chopstick %d\n", phi, left);continue;}// pthread_mutex_lock(&chopstick[right]); //拿起右手的筷子,如果想观察死锁,/把上一句if的所有语句注释掉,再把这一句加上printf("Philosopher %c fetches chopstick %d\n", phi, right);printf("Philosopher %c is eating.\n",phi);usleep(300000); //吃饭pthread_mutex_unlock(&chopstick[left]); //放下左手的筷子printf("Philosopher %c release chopstick %d\n", phi, left);pthread_mutex_unlock(&chopstick[right]); //放下右手的筷子printf("Philosopher %c release chopstick %d\n", phi, right);}
}
int main(){pthread_t A,B,C,D,E; //5个哲学家int i;for (i = 0; i < 5; i++){pthread_mutex_init(&chopstick[i],NULL);}pthread_create(&A,NULL, eat_think, "A");pthread_create(&B,NULL, eat_think, "B");pthread_create(&C,NULL, eat_think, "C");pthread_create(&D,NULL, eat_think, "D");pthread_create(&E,NULL, eat_think, "E");pthread_join(A,NULL);pthread_join(B,NULL);pthread_join(C,NULL);pthread_join(D,NULL);pthread_join(E,NULL);return 0;}
实验结果
实验二进程(线程)同步与互斥的经典问题
生产者消费者问题实现
实验二、生产者/消费者问题的实现
实验目的
掌握进程、线程的概念,熟悉相关的控制语。
掌握进程、线程间的同步原理和方法。
掌握进程、线程间的互斥原理和方法。
掌握使用信号量原语解决进程、线程间互斥和同步方法。
实验原理
n个缓冲区的缓冲池作为一个共享资源,当生产者进程从数据源—文件中读取数据后将会申请一个缓冲区,并将此数据放入缓冲区中。
消费者从一个缓冲区中取走数据,并将其中的内容打印输出。
当生产者进程正在访问缓冲区时,消费者进程不能同时访问缓冲区,因此缓冲区是个互斥资源。
实验步骤
分配具有n个缓冲区的缓冲池,作为共享资源。
定义两个资源型信号量empty 和full,empty信号量表示当前空的缓冲区数量,full表示当前满的缓冲区数量。
定义互斥信号量mutex,当某个进程访问缓冲区之前先获取此信号量,在对缓冲区的操作完成后再释放此互斥信号量。以此实现多个进程对共享资源的互斥访问。
创建3个进程(或者线程)作为生产者,4个进程(或者线程)作为消费者。创建一个文件作为数据源,文件中事先写入一些内容作为内容。
编写代码实现生产者进程的工作内容,即从文件中读取数据,然后申请一个empty信号量,和互斥信号量,然后进入临界区操作将读取的数据放入此缓冲区中。并释放empty信号量和互斥信号量。
编写代码实现消费者者进程的工作内容,即先申请一个full信号量,和互斥信号量,然后进入临界区操作从缓冲区中读取数据并打印输出。
流程图
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h> // 实现多线程的头文件
#include <semaphore.h> // 实现信号量定义的头文件 #define producerNumber 3 // 生产者的数目
#define consumerNumber 4 // 消费者的数目
#define M 6 // 缓冲区数目int in = 0; // 生产者放置产品的位置
int out = 0; // 消费者取产品的位置int buff[M] = {0}; // 缓冲初始化为0, 开始时没有产品sem_t empty_sem; // 信号量的数据类型为结构sem_t,它本质上是一个长整型的数,同步信号量, 当满了时阻止生产者放产品
sem_t full_sem; // 同步信号量, 当没产品时阻止消费者消费
pthread_mutex_t mutex; // 互斥信号量, 一次只有一个线程访问缓冲int producer_id = 0; // 生产者id
int consumer_id = 0; // 消费者id/* 打印缓冲情况 */
void print()
{
int i;
for(i = 0; i < M; i++)printf("%d ", buff[i]);
printf("\n");
}/* 生产者方法 */
void *product()
{
int id = ++producer_id;
int count=30;
while(count--)
{// 用sleep的数量可以调节生产和消费的速度,便于观察sleep(1);sem_wait(&empty_sem); // 满不放pthread_mutex_lock(&mutex); // 实现互斥in = in % M;buff[in] = rand()%10;printf("生产者%d向%d号缓冲区放入了一个数据%d: \t", id, in, buff[in]); print(); ++in;pthread_mutex_unlock(&mutex);sem_post(&full_sem);
}
}/* 消费者方法 */
void *prochase()
{
int id = ++consumer_id;
int count=30;
while(count--)
{sleep(1);sem_wait(&full_sem); // 空不取pthread_mutex_lock(&mutex); // 实现互斥out = out % M;printf("消费者%d从%d号缓冲区取出一个数据%d: \t", id, out, buff[out]);buff[out] = 0;print();++out;pthread_mutex_unlock(&mutex);sem_post(&empty_sem);
}
}int main()
{
pthread_t id1[producerNumber]; // 声明生产者线程的ID数组
pthread_t id2[consumerNumber]; // 声明消费者线程的ID数组
int i;
int ret1[producerNumber];
int ret2[consumerNumber];// 初始化同步信号量
int ini1 = sem_init(&empty_sem, 0, M);
int ini2 = sem_init(&full_sem, 0, 0); // 同上初始化的描述//初始化互斥信号量的函数
int ini3 = pthread_mutex_init(&mutex, NULL); // 创建producerNumber个生产者线程
for(i = 0; i < producerNumber; i++)
{ret1[i] = pthread_create(&id1[i], NULL, product, NULL);
}
//创建consumerNumber个消费者线程
for(i = 0; i < consumerNumber; i++)
{ret2[i] = pthread_create(&id2[i], NULL, prochase, NULL);
}
//销毁线程
for(i = 0; i < producerNumber; i++)
{pthread_join(id1[i],NULL);//pthread_join()函数来使主线程阻塞以等待其他线程退出
}for(i = 0; i < consumerNumber; i++)
{pthread_join(id2[i],NULL);
}exit(0);
}
实验3-利用管道实现进程的通信
每个进程各自有不同的用户地址空间
进程之间要交换数据必须通过内核(内核开辟的缓冲区)
内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)
管道是一种最基本的IPC机制,由pipe函数创建:
#include<unistd.h>
int pipe(int fd[2]);
返回值:成功返回0,错误返回-1
参数fd为管道描述符,同文件描述符,可以使用文件I/O函数(close, read, write)进行操作。
fd[0]为管道读端;fd[1]为管道写端。如不需要,可以关闭相应端的描述符。
子进程写父进程读
3. 父进程关闭写端,子进程关闭读端
实验内容
1.在Linux系统中使用系统调用fork()创建两个子进程,
2.使用系统调用pipe()建立一个管道,两个子进程分别向管道各写一句话:
Child process 1 is sending a message!Child process 2 is sending a message!
3. 父进程则从管道中读出来自于两个子进程的信息,显示在屏幕上。然后分别结束两个子进程的运行。
int pipe(int fd[2]);pid_t fork();int close(int fd);//关闭由文件描述符fd指定的文件ssize_t read(fd, void *buf, size_t count);//读取管道内容ssize_t write(fd, void *buf, size_t count);//向管道写内容
要求及分析
要求1:在Linux平台下实现。
要求2:fork创建两个子进程
子进程( fork函数让子进程完整地拷贝了父进程的整个地址空间)都有管道的读端和写端。所以在相关进程中最好关掉不用的那一端。
要求3:“父进程先接收子进程P1发来的消息,然后再接收子进程P2发来的消息。”
存在两个同步问题:
两个子进程和父进程之间(先子写后父读)
子进程1和子进程2之间(先1写,再2写)
使用waitpid()函数实现父进程等待子进程运行完毕后从管道中读取数据并打印,只有子进程将数据写入管道后,父进程才能够执行打开管道操作。
流程图
实验步骤
步骤1:创建一个管道。
步骤2:创建子进程1,向管道中写入“Child process 1 is sending a message!”,并做好跟父进程的同步执行。
步骤3:创建子进程2,向管道中写入“Child process 2 is sending a message!”,并做好跟父进程的同步执行。
步骤4:父进程从管道中读取数据,并打印输出。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>int main() {int fd[2];if (pipe(fd) == -1) //创建管道return -1;pid_t p1, p2;if ((p1 = fork()) == 0) { //子进程p1char* msg1 = "Child process 1 is sending a message!\n";close(fd[0]); //关闭管道读端write(fd[1], msg1, strlen(msg1)); //向管道写数据return 0;}else if (p1 > 0) { //父进程waitpid(p1, NULL, 0); //同步1-等子进程1先写完if ((p2 = fork()) == 0) { //子进程p2char* msg2 = "Child process 2 is sending a message!\n";close(fd[0]); //关闭管道读端write(fd[1], msg2, strlen(msg2)); //向管道写数据return 0;}else if (p2 > 0) { //父进程waitpid(p2, NULL, 0); //同步2-等子进程2先写完char msg[100];close(fd[1]); //关闭管道写端read(fd[0], msg, 100); //从管道读数据printf("%s", msg);return 0;}}elsereturn -1;
}