多线程编程基础

线程

什么是线程
pthread : p_thread, 线程是进程的分支。线程本质上是一种轻量级的进程。
进程调度的代价太高,并且内存独立,通信很麻烦,为了解决这两个问题,引入了线程。

多个线程共享的是同一片进程的空间,所以线程之间的通信,变得简单。

进程调度的时间为什么成本高?

进程之间调度有时间局部性,CPU建立缓存的过程,可能是为了运行效率的提升,进程刚运行的时候,内存会调度出去,缓存也会被清空,代价大,线程是共享的内存,线程切换不需要置换内存。原先线程建立的信息,另一个线程也是可以用的。

接口: 线程在编译的时候需要链接 -pthread

  1. pthread_create
    int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
    thread 线程id
    pthread_attr_t 线程属性
    void *(*start_routine) 进入函数
    void *arg 是函数的参数
    pthread_create 开始一个新的线程,新的线程从函数进入,arg 是函数的唯一参数。
    线程有三种结束方式:
    1. 自己调用pthread_exit , 或者pthread_join
      pthread_join: 在 pthread_exit 的时候会返回一个值给join 函数,然后一个线程死亡,另一个线程一起死亡。
    2. 从start_routine 函数返回,不是子循环,等价调用pthread_exit
    3. 被取消 pthread_concel
    4. 任何线程调用exit , 或者主线程调用exit 会导致进程中所有线程都会死亡。也就意味着,如果一个线程导致内存崩溃,会导致所有线程都会死掉。

线程是谁生的/怎么产生的?
线程模型(线程是怎么产生的) 主流的线程模型有两种:1. 内核线程(由内核产生的), 2.用户线程(在用户空间产生的) 最早,计算机只有进程没有线程,为了制作出更好的并发,产生了线程。

pthread_t 不是一个数字类型,实现方式不同,各种模型也不一样,统一pthread_t 。所以判断线程相等,不可以用线程id 相等来判断, 可以使用pthread_equal(t1, t2) 来判断, 如果相等返回非0数,不等返回0

pthread_attr_t 结构体表明一个线程属性,可以用pthread_attr_init初始化, 如果在创建的时候,attr 为空,那么会赋予一个默认属性。(一般情况下一个默认属性就可以)
在返回之前,线程id 会保存在thread 中,用来指向线程,并且这个id 会用在该线程的其他操作之中。
新的线程会复制创建该线程的信号掩码,(待处理的信号为空,未发生的信号,不继承)
新的线程会继承创建该线程的环境。

  1. pthread_join
    int pthread_join(pthread_t thread, void **retval);
    pthread_join 函数会等待被指定的线程终结,如果被指定的线程已经结束,那么这个线程会立刻结束,或者说pthread_join 不会被阻塞 立刻返回。被指定的线程一定是可以join 的
    如果retval 不是空,那么pthread_join 会拷贝目标线程退出码到*retval 之中,如果目标线程被取消了,内那么retval 会放置一个PTHREAD_CANCELED
    如果有多个线程同时去join 一个线程,结果不知道会被哪个线程拿到,如果调用phread_join 的线程被取消了,那么目标线程仍然可以join 。
    成功返回0,失败返回错误码

  2. pthread_self
    pthread_t pthread_self() 获得现在运行的线程id

  3. pthread_detach
    int pthread_detach(pthread_t thread); 将id 标记的线程设定为已经分离的线程.(unjoinable 状态),标记分离的线程不需要使用pthread_join 去释放资源,系统会自动的释放资源。不可以分离一个已经分离的线程。

  4. pthread_exit
    void pthread_exit(void *retval)终结调用的线程,返回的值存在retval 中,如果这个线程是joinable 那么在另一个线程中调用pthread_join 时候也会返回这个值。
    任何清理的处理(这里指退出线程)都是基于pthread_cleanup_push(这里理解:清理线程有一个栈,退出的线程都放在栈中,通过pop 执行来以相反的顺序来清空),如果这个线程有任何线程特有数据(线程特有数据是啥?),那么在清理的操作已经执行之后,有一个自己销毁自己的函数以无序的顺序调用。
    当一个线程退出/结束。进程共享区(互斥锁,条件变量,信号量,和文件描述符)不会被释放。
    在最后一个线程结束的时候,进程结束通过调用exit 的方式,这个时候,进程共享资源被释放,然后函数atexit 被调用。

  5. pthread_cancel
    int pthread_cancel(pthread_t thread);当执行这个函数,会向目标线程发出一个请求,但是目标线程是否可以结束以及什么时候结束取决于目标线程的cancelability 的状态和 类型。
    一个线程的 cancelability 状态由 pthread_setcancelstate 取消,(可以通过这个函数来设置可以能否取消, 默认为可以取消),如果 这个线程当前状态是不可取消的,那么这个取消的请求会在队列中等待,直到这个线程可以取消,如果这个进程当前状态是可以取消的,那么至于什么时候取消这个线程取决于 cancelability 的类型。
    一个线程的 cancelability 类型取决于 pthread_setcanceltype, 这个类型,可能是异步的(asynchronous), 也可能是推迟的(deferred)。 异步取消,通常是可以在任何时候取消(一般都是调用之后立刻取消这个线程,但是系统不保证)。推迟取消意味着,他们会等到执行到下一个取消点的函数时候取消(有一串取消点函数)。

  6. exit
    void exit(int status);这个函数的执行会导致所有的普通进程终结,并且返回给父进程 status & 0377。
    所有的函数退出时候用atexit注册,然后以相反的顺序调用on_exit(认为是以栈的形式),在atexit 和 on_exit的过程中,可能会发现其他的函数需要exit ,然后会放在栈顶,(姑且这么理解),然后每次以弹栈的方式调用,(意味着中途发现需要退出的,会先执行),如果其中一个函数,没有返回(比如调用了_exit(2), 或者是被信号杀死),那么没有退出函数被调用,然后剩下需要退出的函数也直接被抛弃了(自生自灭),如果一个函数多次使用atexit 或者 on_exit,那么它会被调用多次。
    !!!在退出程序的时候,会刷新所有的标准io流,然后再关闭。所以假如在一个程序中打开一个文件,但是没有关闭,在执行exit 的时候,会刷新这个io 流,并且关闭这个文件。并且打开这个文件时候,创建的临时文件,会被删除。

  7. pthread_yield
    int pthread_yield(void ) 调用这个函数,线程会被挂起,优先级会放在最后。(协同式调度)
    在进程中是sched_yield

实验代码:

#include "head.h"

void *print(void *arg) {
    printf ("In Thread!\n");
}
int main() {
    pthread_t pthread;
    pthread_create(&pthread, NULL, print, NULL);
    return 0;
}

直接运行这个代码,发现,运行之后没有出结果,这是因为主线程执行完,直接退出,导致所有线程都关闭。解决这个问题可以在创建线程之后sleep 一下,或者使用pthread_join 去等待线程退出。

#include "head.h"
void *print(void *arg) {
    printf ("In Thread!\n");
}
int main() {
    pthread_t pthread;
    pthread_create(&pthread, NULL, print, NULL);
    pthread_join(pthread, NULL);
    return 0;
}

向函数中添加参数

// 传入一个参数
#include "head.h"

void *print(void *arg) {

    printf ("In Thread!\n");
    printf ("Someone is %d years old\n", *(int *)arg);
}

int main() {
    pthread_t pthread;
    int age = 18;
    pthread_create(&pthread, NULL, print, &age);
    pthread_join(pthread, NULL);
    return 0;
}
// 传入多个参数
#include "head.h"

struct MyArg {
    char name[20];
    int age;
};

void *print(void *arg) {

    printf ("In Thread!\n");
    printf ("%s is %d years old\n", (*(struct MyArg *)arg).name, (*(struct MyArg*)arg).age);
}

int main() {
    pthread_t pthread;
    int age = 18;
    char name[20] = "Monster";
    struct MyArg myinfo;
    myinfo.age = age;
    strcpy(myinfo.name, name);
    pthread_create(&pthread, NULL, print, &myinfo);
    pthread_join(pthread, NULL);
    return 0;
}

这里要记住一点,在多进程并发的时候,传入的age 中的数值往往可能是会变动的,但是传入的是地址,可能传入时候,age 的值是正确的,但是使用的时候,age 已经改变了,我们可以在线程函数之内用一个变量去承接,但函数有风险,也可以动态申请。

线程池

创建线程的目的是解决一个确定性的工作
对于单进程处理方式是,来一个任务,处理一个任务。来一个任务,处理一个任务。
如果两个任务同时来的时候,就不能同时处理了。
所以我们引入了多进程概念,去复制一个自己,去处理。但是这样的开销太大,不值得
考虑到多线程,每次需要的时候,创建一个线程,做完任务,消除。
但是创建和销毁都是需要时间,来的任务,不能达成最快的响应。
线程池:创建多个线程,处理完任务不销毁,让这个线程等待任务,这样会有更高的并发。

线程池布局:
一个线程区,一个等待区。
等待区放置的是等待处理的任务,线程区放置的是将要处理任务的线程
线程会从等待区中拿出任务,然后处理,回到线程区。
但是线程拿取任务采用的是竞争的方式,所以在竞争任务的时候会发生竞争。所以在这个时候需要引入锁

实现线程池:

  1. 一个任务队列(循环队列)有push 操作和 pop 操作 (有锁)
  2. pthread_create 创建n个线程
  3. 线程只做do_work(要处理的任务)
    1. whle (1)
    2. pop (有锁)
    3. 做任务
    实现一个循环打印的线程池
#ifndef _THREAD_POOL_H
#define _THREAD_POOL_H

struct task_queue {
    int size;//多大
    int total; // 有多少个人
    int head;
    int tail;
    char **data;// 存放数据
    pthread_mutex_t mutex;
    pthread_cond_t cond;// 唤醒线程,告诉线程任务来了
};

void task_queue_init(struct task_queue *taskQueue, int size);
void task_queue_push(struct task_queue *taskQueue, char *str);
char* task_queue_pop(struct task_queue *taskQueue);

#endif
#include "head.h"
#include "thread_pool.h"

void task_queue_init(struct task_queue *taskQueue, int size) {
    taskQueue->size = size;
    taskQueue->total = 0;
    taskQueue->head = taskQueue->tail = 0;
    pthread_mutex_init(&taskQueue->mutex, NULL);
    pthread_cond_init(&taskQueue->cond, NULL);
    taskQueue->data = calloc(size, sizeof(void *));// 每一个座位放一个void 指针
    return ;
}

void task_queue_push(struct task_queue *taskQueue, char *str) {
    pthread_mutex_lock(&taskQueue->mutex);
    if (taskQueue->total == taskQueue->size) {
        pthread_mutex_unlock(&taskQueue->mutex);
        printf("task queue is full!\n");
        return ;
    }
    printf("<push> : %s\n", str);
    taskQueue->data[taskQueue->tail] = str;
    taskQueue->total++;
    if (++taskQueue->tail == taskQueue->size) {
        printf("task queue reach end!\n");
        taskQueue->tail = 0;
    }// 满了
    pthread_cond_signal(&taskQueue->cond);
    pthread_mutex_unlock(&taskQueue->mutex);
    return ;
}

char *task_queue_pop(struct task_queue *taskQueue) {
    pthread_mutex_lock(&taskQueue->mutex);
    while (taskQueue->total == 0) {
        printf("task queue is empty!\n");
        pthread_cond_wait(&taskQueue->cond, &taskQueue->mutex);
    }// 用循环是有可能被叫醒,但是发现没人。

    char *str = taskQueue->data[taskQueue->head];
    printf("<pop> : %s\n", str);
    taskQueue->total--;
    if (++taskQueue->head == taskQueue->size) {
        printf("taskQueue reach end!\n");
        taskQueue->head = 0;
    }
    pthread_mutex_unlock(&taskQueue->mutex);

    return str;
}
#include "head.h"
#include "thread_pool.h"

#define THREAD 5
#define QUEUE 50

void *do_work(void *arg) {
    pthread_detach(pthread_self());// 分离自己,别人无法join 自己
    struct task_queue *taskQueue = (struct task_queue *)arg;
    while (1) {
        char *str = task_queue_pop(taskQueue);
        printf("<%lu> : %s !\n", pthread_self(), str);
    }
}


int main() {
    pthread_t tid[THREAD];
    struct task_queue taskQueue;
    task_queue_init(&taskQueue, QUEUE);
    char buff[QUEUE][1024] = {0};

    for (int i = 0; i < THREAD; i++) {
        pthread_create(&tid[i], NULL, do_work, (void *)&taskQueue);
    }
    int sub = 0;
    while (1) {
        FILE *fp = fopen("./test.c", "r");
        if (fp == NULL) {
            perror("fopen");
            exit(1);
        }
        while (fgets(buff[sub++], 1024, fp) != NULL) {
            task_queue_push(&taskQueue, buff[sub]);
            if (sub == QUEUE) {
                sub = 0;
            }
            if (taskQueue.total == taskQueue.size) {// 当任务队列满了,等待有空余位置再进入
                while (1) {
                    if (taskQueue.total < taskQueue.size) {
                        break;
                    }
                    usleep(10000);
                }
            }
        }
        fclose(fp);
    }
    return 0;
}

循环打印文件中的变量,但是出现的问题是执行到最后总会发生段错误。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值