线程
什么是线程
pthread : p_thread, 线程是进程的分支。线程本质上是一种轻量级的进程。
进程调度的代价太高,并且内存独立,通信很麻烦,为了解决这两个问题,引入了线程。
多个线程共享的是同一片进程的空间,所以线程之间的通信,变得简单。
进程调度的时间为什么成本高?
进程之间调度有时间局部性,CPU建立缓存的过程,可能是为了运行效率的提升,进程刚运行的时候,内存会调度出去,缓存也会被清空,代价大,线程是共享的内存,线程切换不需要置换内存。原先线程建立的信息,另一个线程也是可以用的。
接口: 线程在编译的时候需要链接 -pthread
- 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 是函数的唯一参数。
线程有三种结束方式:- 自己调用pthread_exit , 或者pthread_join
pthread_join: 在 pthread_exit 的时候会返回一个值给join 函数,然后一个线程死亡,另一个线程一起死亡。 - 从start_routine 函数返回,不是子循环,等价调用pthread_exit
- 被取消 pthread_concel
- 任何线程调用exit , 或者主线程调用exit 会导致进程中所有线程都会死亡。也就意味着,如果一个线程导致内存崩溃,会导致所有线程都会死掉。
- 自己调用pthread_exit , 或者pthread_join
线程是谁生的/怎么产生的?
线程模型(线程是怎么产生的) 主流的线程模型有两种:1. 内核线程(由内核产生的), 2.用户线程(在用户空间产生的) 最早,计算机只有进程没有线程,为了制作出更好的并发,产生了线程。
pthread_t 不是一个数字类型,实现方式不同,各种模型也不一样,统一pthread_t 。所以判断线程相等,不可以用线程id 相等来判断, 可以使用pthread_equal(t1, t2)
来判断, 如果相等返回非0数,不等返回0
pthread_attr_t 结构体表明一个线程属性,可以用pthread_attr_init初始化, 如果在创建的时候,attr 为空,那么会赋予一个默认属性。(一般情况下一个默认属性就可以)
在返回之前,线程id 会保存在thread 中,用来指向线程,并且这个id 会用在该线程的其他操作之中。
新的线程会复制创建该线程的信号掩码,(待处理的信号为空,未发生的信号,不继承)
新的线程会继承创建该线程的环境。
-
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,失败返回错误码 -
pthread_self
pthread_t pthread_self()
获得现在运行的线程id -
pthread_detach
int pthread_detach(pthread_t thread);
将id 标记的线程设定为已经分离的线程.(unjoinable 状态),标记分离的线程不需要使用pthread_join 去释放资源,系统会自动的释放资源。不可以分离一个已经分离的线程。 -
pthread_exit
void pthread_exit(void *retval)
终结调用的线程,返回的值存在retval 中,如果这个线程是joinable 那么在另一个线程中调用pthread_join 时候也会返回这个值。
任何清理的处理(这里指退出线程)都是基于pthread_cleanup_push
(这里理解:清理线程有一个栈,退出的线程都放在栈中,通过pop 执行来以相反的顺序来清空),如果这个线程有任何线程特有数据(线程特有数据是啥?),那么在清理的操作已经执行之后,有一个自己销毁自己的函数以无序的顺序调用。
当一个线程退出/结束。进程共享区(互斥锁,条件变量,信号量,和文件描述符)不会被释放。
在最后一个线程结束的时候,进程结束通过调用exit 的方式,这个时候,进程共享资源被释放,然后函数atexit 被调用。 -
pthread_cancel
int pthread_cancel(pthread_t thread);
当执行这个函数,会向目标线程发出一个请求,但是目标线程是否可以结束以及什么时候结束取决于目标线程的cancelability 的状态和 类型。
一个线程的 cancelability 状态由pthread_setcancelstate
取消,(可以通过这个函数来设置可以能否取消, 默认为可以取消),如果 这个线程当前状态是不可取消的,那么这个取消的请求会在队列中等待,直到这个线程可以取消,如果这个进程当前状态是可以取消的,那么至于什么时候取消这个线程取决于 cancelability 的类型。
一个线程的 cancelability 类型取决于pthread_setcanceltype
, 这个类型,可能是异步的(asynchronous), 也可能是推迟的(deferred)。 异步取消,通常是可以在任何时候取消(一般都是调用之后立刻取消这个线程,但是系统不保证)。推迟取消意味着,他们会等到执行到下一个取消点的函数时候取消(有一串取消点函数)。 -
exit
void exit(int status);
这个函数的执行会导致所有的普通进程终结,并且返回给父进程 status & 0377。
所有的函数退出时候用atexit注册,然后以相反的顺序调用on_exit(认为是以栈的形式),在atexit 和 on_exit的过程中,可能会发现其他的函数需要exit ,然后会放在栈顶,(姑且这么理解),然后每次以弹栈的方式调用,(意味着中途发现需要退出的,会先执行),如果其中一个函数,没有返回(比如调用了_exit(2), 或者是被信号杀死),那么没有退出函数被调用,然后剩下需要退出的函数也直接被抛弃了(自生自灭),如果一个函数多次使用atexit 或者 on_exit,那么它会被调用多次。
!!!在退出程序的时候,会刷新所有的标准io流,然后再关闭。所以假如在一个程序中打开一个文件,但是没有关闭,在执行exit 的时候,会刷新这个io 流,并且关闭这个文件。并且打开这个文件时候,创建的临时文件,会被删除。 -
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 已经改变了,我们可以在线程函数之内用一个变量去承接,但函数有风险,也可以动态申请。
线程池
创建线程的目的是解决一个确定性的工作
对于单进程处理方式是,来一个任务,处理一个任务。来一个任务,处理一个任务。
如果两个任务同时来的时候,就不能同时处理了。
所以我们引入了多进程概念,去复制一个自己,去处理。但是这样的开销太大,不值得
考虑到多线程,每次需要的时候,创建一个线程,做完任务,消除。
但是创建和销毁都是需要时间,来的任务,不能达成最快的响应。
线程池:创建多个线程,处理完任务不销毁,让这个线程等待任务,这样会有更高的并发。
线程池布局:
一个线程区,一个等待区。
等待区放置的是等待处理的任务,线程区放置的是将要处理任务的线程
线程会从等待区中拿出任务,然后处理,回到线程区。
但是线程拿取任务采用的是竞争的方式,所以在竞争任务的时候会发生竞争。所以在这个时候需要引入锁
实现线程池:
- 一个任务队列(循环队列)有push 操作和 pop 操作 (有锁)
- pthread_create 创建n个线程
- 线程只做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;
}
循环打印文件中的变量,但是出现的问题是执行到最后总会发生段错误。