操作系统(四):多线程编程

一、题目1

4.22

4.22 Write a multithreaded program that calculates various statistical values for a list of numbers. This program will be passed a series of numbers on the command line and will then create three separateworker threads. One thread will determine the average of the numbers, the second will determine the maximum value, and the third will determine the minimum value. For example, suppose your program is passed the integers

90 81 78 95 79 72 85

The program will report

The average value is 82

The minimum value is 72

The maximum value is 95

The variables representing the average, minimum, andmaximum values will be stored globally. The worker threads will set these values, and the parent thread will output the values once the workers have exited. (We could obviously expand this program by creating additional threads that determine other statistical values, such as median and standard deviation.)

大致翻译:写一个程序,创建三个线程,这三个线程分别计算传入参数的平均值、最大值及最小值,当一个线程完成工作之后,父线程就会立即输出结果。额外地,你可以计算其他统计量,比如中位数和标准差。

知识点:线程的创建,线程的并行。

二、解题啦1

建立一个线程的流程:

  1. pthread_t tid; //创建线程的标识符
  2. pthread_attr_t attr; //创建线程的属性
  3. pthread_attr_init(&attr); //设置线程的属性(一般用默认值)
  4. pthread_create(&tid, &attr, runner, arr); //创建一个线程,并与线程函数绑定;
  5. pthread_join(tid, NULL); //等待线程1完成
以下为代码c41.c
#include <pthread.h>
#include <stdio.h>

int ave, max, min; //全局变量存储平均值、最大值、最小值 
int count; //设置参数总数 

void *runner1(void *param);
void *runner2(void *param);
void *runner3(void *param);

int main(int argc, char **argv){
   count = argc-1; //参数总数 
   
   pthread_t tid1, tid2, tid3; //创建线程的标识符
   pthread_attr_t attr1, attr2, attr3; //创建线程的属性
   
   if (argc <= 1){ //没有输入参数,报错 
   	fprintf(stderr, "no numbers!\n");
   	return -1;
   } 
   
   /*创建一个数组存参数,注意要把argv[0]不存入,因为它是该程序本身,后面的才是参数*/
   int *arr = (int *)malloc(sizeof(int)*(count));
   if(arr == NULL){
   	fprintf(stderr, "malloc() failed!\n");
   	return -1;
   } 
    
   int i;
   for (i = 0; i < count; i++){
   	arr[i] = atoi(argv[i+1]); //atoi:字符串转整数 
   }
   
   /*设置线程属性(采用默认值)*/
   pthread_attr_init(&attr1);
   pthread_attr_init(&attr2);
   pthread_attr_init(&attr3);
   
   /*创建线程*/
   pthread_create(&tid1, &attr1, runner1, arr);
   pthread_join(tid1, NULL);  //等待线程1完成 
   
   pthread_create(&tid2, &attr2, runner2, arr);
   pthread_join(tid2, NULL);
   
   pthread_create(&tid3, &attr3, runner3, arr);
   pthread_join(tid3, NULL);

   printf("ave = %d\n", ave);
   printf("max = %d\n", max);
   printf("min = %d\n", min);
   
   free(arr); //释放内存
    
   return 0;
}

void *runner1(void *param){ //算平均值 
   int *arr = (int *)param;
   int sum = 0;
   int i;
   
   for(i = 0; i<count; i++){
   	sum += arr[i];
   }  
   
   ave = sum/count;
   
   pthread_exit(0);  //线程终止回到父线程 
}

void *runner2(void *param){ //算平均值 
   int *arr = (int *)param;
   max = arr[0]; 
   int i;
   
   for(i = 1; i<count; i++){
   	if(arr[i] > max){
   		max = arr[i];
   	}
   }  
   
   pthread_exit(0);  //线程终止回到父线程 
}

void *runner3(void *param){ //算平均值 
   int *arr = (int *)param;
   min = arr[0]; 
   int i;
   
   for(i = 1; i<count; i++){
   	if(arr[i] < min){
   		min = arr[i];
   	}
   }  
   
   pthread_exit(0);  //线程终止回到父线程 
}
gcc -g c41.c -o c41 -lpthread   //编译
./c41 2 3 4     //编译, 2,3,4为输入的参数
三、题目2

Project 1—Sudoku Solution Validator

A Sudoku puzzle uses a 9 × 9 grid in which each column and row, as well as each of the nine 3 × 3 subgrids, must contain all of the digits 1 ⋅ ⋅ ⋅ 9. Figure 4.26 presents an example of a valid Sudoku puzzle. This project consists of designing a multithreaded application that determines whether the solution to a Sudoku puzzle is valid.

There are several different ways of multithreading this application. One suggested strategy is to create threads that check the following criteria:

• A thread to check that each column contains the digits 1 through 9

• A thread to check that each row contains the digits 1 through 9

• Nine threads to check that each of the 3 × 3 subgrids contains the digits 1 through 9

This would result in a total of eleven separate threads for validating a Sudoku puzzle. However, you are welcome to create even more threads for this project. For example, rather than creating one thread that checks all nine columns, you could create nine separate threads and have each of them check one column.

multiprocessing 版本查看 multiprocessor configuration_二维数组


Figure 4.26 Solution to a 9 x 9 sudoku puzzle

大致翻译:使用三个线程对一个给定的数独解答进行验证。正确的数独解答满足三个特征:(一个线程)每一行1-9有且只有一个,(一个线程)每一列1-9有且只有一个,(九个线程)每个九宫格1-9有且只有一个。使用三个线程分别验证上述三个特性。

知识点:(4.22的全部知识点)、二维数组的寻址、局部地址到全局地址的映射、二维数组到一维数组的地址映射。

参考链接:https://zhuanlan.zhihu.com/p/81863628

四、解题2
  1. 万能传参:void* param
    当你传参的时候,先转化为泛型,在用的时候你就把它转化为你想要的类型,但是也有限制:那就是只能在指针之间转化,不能在指针和非指针之间进行转化。

开始做题啦

开三个线程的操作和前面是一样的,这道题的重点在于二维数组的寻址操作。

首先我们来复习一个知识:

二维数组在内存中的储存是一维线性的。

什么意思呢?

比如你声明一个数组:

int a[3][3];

那么以下语句的结果为true:

int *ptr = a;
&a[1][0] == ptr+1*3+0;

这里为什么要写成1*3+0呢,是因为这是第一行的第0个元素,第一行总共有3个元素。

如果还不理解试一下将二维数组用Z字连接法变成一个一维数组(就是第0行的尾接第1行的头,第1行的尾接第二行的头,以此类推……如同在二维矩阵上画一个Z字)。

有了这个预备知识之后,我们就可以用一维的指针去寻二维数组的址了。

首先前向声明三个线程函数:

void *checkCol(void *param);
void *checkRow(void *param);
void *checkGrid(void *param);

在main里声明这个数组

int arr[9][9]={
        {6,2,4,5,3,9,1,8,7},
        {5,1,9,7,2,8,6,3,4},
        {8,3,7,6,1,4,2,9,5},
        {1,4,3,8,6,5,7,2,9},
        {9,5,8,2,4,7,3,6,1},
        {7,6,2,3,9,1,4,5,8},
        {3,7,1,9,5,6,8,4,2},
        {4,9,6,1,8,2,5,7,3},
        {2,8,5,4,7,3,9,1,6}
    };

然后嘛,首先我们来验证列的,我们三个线程都采用一种掩码的方式来判断是否1-9仅出现1次(因为每列/每行/每个小九宫格有9个数,所以1~9不可能有不出现的数字同时没有出现超过1次的数字)。

首先给大家上代码直观地看一下吧:

#include <string.h>
void *checkCol(void *param){
    int mask[9];
    memset(mask,0,sizeof(int)*9);
    int *arr = param;/*Get the first element address*/
    int i = 0;
    int j = 0;
    for(;j < 9;j++){
        for(i = 0;i < 9;i++){/*For each col*/
            /*printf("%d ",*(arr+i*9+j));*/
            if (mask[*(arr+i*9+j)-1] == 1){/*Array begins from 0*/
                printf("Sudoku col check failed.\n");
                pthread_exit(0);
            }
            else if(mask[*(arr+i*9+j)-1] == 0)
                mask[*(arr+i*9+j)-1]++;
        }
        /*printf("\n");*/
        memset(mask,0,sizeof(int)*9);
    }
    printf("Sudoku col check success.\n");
    pthread_exit(0);
}

mask的9位对应1~9,由于数组从0开始,所以数组索引要-1才可以对准。

我们可以看到,我们首先使用memset将掩码数组清0,然后获得二维数组的首地址。然后根据我们刚才得出来的公式我们可以知道:

multiprocessing 版本查看 multiprocessor configuration_数组_02

所以只需要以col作为大索引,就可以列优先遍历整个数组了。

相同地,以row为大索引,就可以行优先遍历整个数组了。

重点在于掩码判断语句:

if (mask[*(arr+i*9+j)-1] == 1){/*Array begins from 0*/
     printf("Sudoku col check failed.\n");
     pthread_exit(0);
}
else if(mask[*(arr+i*9+j)-1] == 0){
     mask[*(arr+i*9+j)-1]++;
}

这里第一个if判断有没有出现过。如果有就可以知道整个数独已经填错了。第二个if用于给掩码位标识为1。

行判断的同理,模仿列判断的函数就可以方便地写出。

九宫格的判断是我有点想说的一个有趣的函数。

我这里不给出它的实现,只对几个语句说一说。

首先,我们知道,每个九宫格的左上角顶点相对于全局的坐标,我们是可以通过该公式求出来的:

multiprocessing 版本查看 multiprocessor configuration_二维数组_03

这个语句很有意思,我们知道,在计算机科学中,除运算符得到的是整数结果,其余数通过模运算符得到。那么大家可以写出几个通过这个公式中得出的(x,y):

multiprocessing 版本查看 multiprocessor configuration_数组_04

看回二维矩阵,可以发现这些都是每个九宫格左上角的顶点。根据前面我们提的二维矩阵坐标映射到一维向量坐标,我们有:

multiprocessing 版本查看 multiprocessor configuration_操作系统_05

那么我们得到九宫格的顶点了,就可以通过长度为3的二重循环迭代得到整个九宫格的点的坐标了。

for(j=0;j<3;j++)/*For row*/
{
    for(k=0;k<3;k++)/*For col*/
    {
        /*printf("%d ",*(firstaddr+j*9+k));*/
        if(mask[*(firstaddr+j*9+k)-1] == 1)
            {
                printf("Soduku grid check failed.\n");
                pthread_exit(0);
            }
        else if (mask[*(firstaddr+j*9+k)-1] == 0)
                mask[*(firstaddr+j*9+k)-1]++;
    }
            /*printf("\n");*/
}

这里的(j,k)是九宫格的局部坐标,映射回全局就是j*9+k,得到基于九宫格顶点地址的偏移后的全局一维坐标。

如果还感觉理解困难的,可以去自己画一个3*3的小矩阵,并亲自推一下全局坐标和局部坐标,二维坐标和一维坐标之间的相互映射。

如果你想通过可执行文件参数输入数组或者scanf输入数组,在你的代码中自己实现处理数据的语句即可。


要用9个线程判断9个九宫格是否判断正确。就使用代码复用的方法,考虑到我们前文说的void *param可以通过结构体的包装传入任意参数,我们可以定义以下的结构体:

typedef struct structs{
    void *arr;
    int num;
}st;

这里的num指第n个九宫格,按照左上到右下的方式编号,也就是

1 2 3

4 5 6

7 8 9

然后我们在checkGrid里面对参数进行解包(将任意参数包装成struct我称之为打包,那么打包的逆操作就是解包):

st *ptr = (st *)param;
int *arr =(int *)(ptr->arr);
int i
i = ((st *)param)->num-1;

这样子就可以用原来的checkGrid来验证九宫格啦!