以太工坊

以太工坊

以太工坊

实验内容

实验内容一:以数据划分的方式并行计算PI值

实验内容二:CPU多核编程——线程池开发
要求基于生产者—消费者模式进行框架开发,具体工作需求可以简化,但需要有线程管理和同步。

计算Pi

思路简述

依据莱布尼兹公式,通过多线程计算较多的次数,逼近$\pi$ 。用多线程的方式进行数据划分、即每个线程分担处理部分数据,从而进行加速。
同时由于多线程访问全局的结果可能会有冲突,因此使用互斥量和信号量组织线程有序将局部结果加到全局结果中。

代码实现

#include <pthread.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <math.h>  

const int BLOCK_SIZE = 100000;  
double sum = 0;  
int num_threads;  
// 定义互斥量和条件变量  
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;  
void *calculate_block(void *thread_id)  
{  
    long id = (long)thread_id;  
    int start = id * BLOCK_SIZE;  
    int end = start + BLOCK_SIZE;  
    double block_sum = 0;  
    for (int i = start; i < end; i++) {  
        double term = pow(-1, i) / (2 * i + 1);  
        block_sum += term;  
    }  
    pthread_mutex_lock(&mutex);  
    sum += block_sum;    
    pthread_mutex_unlock(&mutex);  
    pthread_cond_signal(&cond);  
}  

int main()  
{  
    num_threads = 8;  
    pthread_t threads[num_threads];  
    for (long i = 0; i < num_threads; i++)
        pthread_create(&threads[i], NULL, calculate_block, (void *)i);  
    for(int i = 0; i < num_threads; i ++)
        pthread_join(threads[i], NULL);
    printf("%lf", sum * 4);
}

线程池设计

线程池实现

使用一个任务队列作为生产者和消费者之间的缓冲区,任务队列中的每一个元素包含要执行的函数和函数参数,对应代码如下:

typedef struct task_t {
    void (*func)(void *);
    void *arg;
} task_t;

线程池包含一个任务队列、若干线程、互斥量与信号量以及其它关键属性,定义如下:

typedef struct thread_pool_t {
    pthread_t threads[MAX_THREADS];
    int num_threads;
    task_t queue[MAX_QUEUE];
    int front, rear, size;
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    bool shutdown;
} thread_pool_t;

在生产者方面,thread_pool_enqueue函数用于将任务添加到任务队列中。当生产者生产了一个任务后,它首先会通过线程池的互斥锁pool->mutex来保护任务队列,防止多个线程同时修改任务队列,然后使用条件变量pool->cond来通知消费者有新的任务到达。

bool thread_pool_enqueue(thread_pool_t *pool, void (*func)(void *), void *arg){
    pthread_mutex_lock(&pool->mutex);
    if (pool->size == MAX_QUEUE) {
        pthread_mutex_unlock(&pool->mutex);
        return false;
    }
    task_t task = { .func = func, .arg = arg };
    pool->queue[pool->rear] = task;
    pool->rear = (pool->rear + 1) % MAX_QUEUE;
    pool->size++;
    pthread_cond_signal(&pool->cond);
    pthread_mutex_unlock(&pool->mutex);
    return true;
}

在消费者方面,thread_pool_worker函数用于从任务队列中取出任务并执行。当消费者从任务队列中取出一个任务时,它会使用互斥锁pool->mutex来保护任务队列。如果任务队列为空,消费者将会被条件变量pool->mutex阻塞,等待生产者添加新的任务到任务队列中。

void *thread_pool_worker(void *arg){
    thread_pool_t *pool = (thread_pool_t *)arg;
    while (true) {
        pthread_mutex_lock(&pool->mutex);
        while (pool->size == 0 && !pool->shutdown)
            pthread_cond_wait(&pool->cond, &pool->mutex);
        if (pool->size == 0 && pool->shutdown) {
            pthread_mutex_unlock(&pool->mutex);
            pthread_exit(NULL);
        }
        task_t task = pool->queue[pool->front];
        pool->front = (pool->front + 1) % MAX_QUEUE;
        pool->size--;
        pthread_mutex_unlock(&pool->mutex);
        task.func(task.arg);
    }
}

线程池的启动包括初始化任务队列、信号量、启动消费者线程;线程池的关闭最重要的是等待所有线程运行结束;两者实现如下:

void thread_pool_init(thread_pool_t *pool, int num_threads) {
    pool->num_threads = num_threads;
    pool->front = pool->rear = pool->size = 0;
    pool->shutdown = false;
    pthread_mutex_init(&pool->mutex, NULL);
    pthread_cond_init(&pool->cond, NULL);
    for (int i = 0; i < num_threads; i++)
        pthread_create(&pool->threads[i], NULL, thread_pool_worker, pool);
}

void thread_pool_shutdown(thread_pool_t *pool) {
    pthread_mutex_lock(&pool->mutex);
    pool->shutdown = true;
    pthread_cond_broadcast(&pool->cond);
    pthread_mutex_unlock(&pool->mutex);
    for (int i = 0; i < pool->num_threads; i++)
        pthread_join(pool->threads[i], NULL);
    pthread_mutex_destroy(&pool->mutex);
    pthread_cond_destroy(&pool->cond);
}

设计一个简单的任务,输出任务id与线程id,并放到线程池运行:

void my_task(void *arg){
    int *num = (int *)arg;
    printf("Task %d by thread %lu\n", *num, pthread_self());
    free(num);
}

int main() {
    thread_pool_t pool;
    thread_pool_init(&pool, 8);
    for (int i = 0; i < 20; i++) {
        int *num = malloc(sizeof(int));
        *num = i;
        thread_pool_enqueue(&pool, my_task, num);
    }
    printf("main thread %lu\n", pthread_self());
    thread_pool_shutdown(&pool);
    return 0;
}

运行结果

在ubuntu 20中编译gcc tp.c -o tp -lpthread,运行结果描述如下:

Task 0 by thread 140422668744478
Task 4 by thread 140422668744456
Task 5 by thread 140422668744434
...

线程池

使用线程池完成矩阵相加$a+b=c$,相应的任务函数如下:

void add_matrix(void *arg) {
    int *num = (int *)arg;
    int st = *num, ed = st + 5;
    for (int i = st; i < ed; i++)
        c[i] = a[i] + b[i];
    printf("Task [add_matrix] %d by thread %lu, range = %d ~ %d \n", *num /5, pthread_self(), st, ed);
    free(num);
}

中英对照表

English 中文
cross production 叉积
determinantion 行列式
eigenvalue 特征值

Vector

The introduction of numbers as coordinates is an act of violence.

AND on the flip side, it gives people a language to descrbie space and the manipulation of space using numbers that can be crunched and run through a computer.
暴论:线代让程序员可以操纵空间。

image.png
是基向量(basis vector),任何向量都可以看成其线性组合。
image.png
共线的向量,线性相关(Linearly dependent),张成的空间就是一条线(或原点);
不共线的向量,线性无关(Linearly independent),张成空间就是所有向量的集合;

Matrix

kind of Linear transformation

好在线性代数只涉及线性变换;
image.png
矩阵可以理解为一种变换;
ac表示一个基变换后的位置,bd也是,因此1001等价于没变换;
知道基是怎样变换的,就知道了所有向量是怎么变换的;
正交变换是基向量保持长度且相互正交的变换(刚体运动);

Matrix Multiply

image.png
单个矩阵是线性变换,那矩阵相乘就是复合变换。矩阵相乘的集合意义就在于使两个线性变换相继作用(apply one transformation then another)
非方阵代表跨纬度的变换;

Determinant

image.png
这就是行列式determinant!👆
1x1的小格子,经过矩阵变换后的面积等于对应行列式的值。
image.png
如果行列式为0,全都压扁了,是一个不可逆的变换,矩阵也是不可逆的。
行列式的值可以是负的,你的面积也能是负的吗?面积等于绝对值,负数代表空间orientation发生了改变(一张纸翻转了);

不过面积不能说明一切,高维是其它东西;

Inverse matrices & Column space & Rank

矩阵不只是操纵空间,还可以用来解方程组。

image.png
把方程组(equation system)转换成矩阵相乘,自然又回到操作空间的传统艺能来了;
在矩阵的作用下变换成,那用逆变换找到原来的就是求解方程组的过程;
image.png
当行列式不为0时,求逆矩阵即可解方程组;
image.png

当行列式为0时,方程组仍然可能有解,前提是幸存于压缩后的空间(列空间)里;

关于秩的解释,视频 8分钟左右真的太精妙了。
秩代表变换后(列)空间的维度;(在方程组中,矩阵的秩刚好就是约束条件的个数)
所有可能的的集合就是列空间Column space;
变换后落在原点的向量集合,就是零空间null space 或者叫做 核kernel;
SVM中的核方法?

Duality of Dot Product

传统理解向量点积的方式为投影,但为了理解对偶性,先忘掉。
对偶性指的是自然而又出乎意料的对应关系。
Vector is the physical embodiment of a linear transformation.(向量是线性变换的载体)
对偶性的理解对理解希尔伯特空间、PCA IDA至关重要。

image.png
image.png

向量和对应的1×n矩阵之间有奇妙的关系,1×n矩阵代表的变换等价于与n×1的向量做点积;每一个向量都是某个矩阵的化身;每一个矩阵都与某个向量对应着;

Cross Product

image.png
传统的解释如上图

Change of Basis

两个处于不同坐标系的人,该怎样交流?把他人的基向量放到自己的坐标系,得到变换矩阵即可。

image.png
对于另一个坐标系中的向量,先用一个变换转换(左三)成我们自己坐标系的向量,再在我们自己的坐标系中进行变换(左中),最后把变换结果转换成他的坐标(左一);
表达式代表着一种转移作用,这种矩阵的乘积仍然是一种变换,是对于其他人来说的。

Eigenvectors & Eigenvalues

Eigenvector.gif

作用:特征值为1的特征向量就是旋转轴
计算:,移项之后,即,即找到一个可以压缩空间的向量
旋转变换特征向量在复向量空间中,剪切变换仅一个特征向量;
也有特征值唯一,特征向量不共线的情况(如将所有向量拉伸两倍)

image.png
对角矩阵,所有的基向量都是特征向量,对角线上的值就是对应的特征值
image.png
如果有一天,想把两个不共线的特征向量[1 0] [0 1]作为新的坐标系的基,这个基变换的过程就是相似对角化;得到的矩阵必然是对角矩阵,且值为特征值。这样的特征向量也叫做特征基(eigenbasis);
为什么要大费周章去进行特征基变换呢,比如说上面这个 ,计算100次这样的变换会非常复杂,变换一下后可以快速得到结果 ,再变回来就是了。

Vector Spaces

行列式、特征向量等与所选的坐标系无关…
函数求导也可以用矩阵完成…
所以向量究竟是什么?

image.png
向量不是个东西。
公理不是什么自然法则,是数学家定义的规则,联系了数学家与使用数学工具的人;向量可以是任何东西,点、箭头、函数、奇怪的生物…,只要他们满足这些公理定义的规则。
问向量是什么,就相当于问“1”是什么,是没有意义的。

Cramer Rule

求解行列式,计算机使用克莱姆法则,人用高斯消元法;但克莱姆法则有意思的多。

image.png
一种独特的表示坐标的方法: y = area/1 , x = area / 1;

image.png

这样变换之后的y就还是以绿色基为底的四边形的面积,这正好契合行列式的几何意义;
这时四边形的面积,绿色的基不变(行列式第一列),高度变为变换后的42.
这就是克莱姆法则的几何意义。

0%