很多东西记住之后也很容易忘记,整理的资料
进程和线程的区别
Linux线程详解(概念、原理、实现方法、优缺点)这位大佬讲的非常明白。接下来,我根据其他的资料将一些重点自己总结一下。感觉下图很好理解,图源还是来自上述链接。
进程
- 进程有独立的地址空间
- Linux为每个进程创建task_struct
- 每个进程都参与内核调度,互不影响
但是进程在切换时系统开销大,这个时候就需要用到线程。
线程
使用线程可以大大提高任务切换的效率,避免了额外的TLB & cache的刷新。因为同一进程中的线程共享相同地址空间
一个进程中的多个线程共享资源包括:
- 可执行的指令
- 静态数据
- 进程中打开的文件描述符
- 当前工作目录
- 用户ID
- 用户组ID
每个线程私有的资源包括:
- 线程ID (TID)
- PC(程序计数器)和相关寄存器
- 堆栈
- 错误号 (errno)
- 优先级
- 执行状态和属性
Linux线程库
(包含的头文件: #include <pthread.h>)
-
创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(routine)(void), void *arg) -
回收线程(三种方式)
1、pthread_join函数:
是阻塞函数,如果回收的线程没有结束,则一直等待。该函数的缺点是对于一个线程回收比较方便,对于多个线程来说,前面的线程没有结束释放,会一直阻塞,导致后面的线程无法释放。
2、pthread_detach线程分离函数:
独立使用,线程是默认线程时,创建线程时第二个参数是NULL(默认)。
3、创建线程时候设置为分离属性:
独立使用
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED); -
结束线程
void pthread_exit(void *retval) // 结束当前线程。会在结束前执行清理工作、释放资源等操作 -
取消线程
线程的取消是指在多线程环境中,一个线程(称为取消线程)请求取消另一个线程(称为目标线程)的执行。取消线程会发送取消请求给目标线程,要求其停止正在执行的任务。取消线程时需要设置取消点,还可设置取消类型。取消请求可以是线程粗略的终止方式,可能会导致资源泄漏或者程序状态不一致。
因此需要清理线程。 -
清理线程
void pthread_cleanup_push(void (*routine) (void *), void *arg)
void pthread_cleanup_pop(int execute) // 注意:这对函数必须成对使用
routine函数被执行的条件:
1、 被pthread_cancel取消掉。
2、 执行pthread_exit
3、 非0参数执行pthread_cleanup_pop()
注意:!!!!!!
1、必须成对使用,即使pthread_cleanup_pop不会被执行到也必须写上,否则编译错误。
2、pthread_cleanup_pop()被执行且参数为0,pthread_cleanup_push回调函数routine不会被执行.
3、pthread_cleanup_push 和pthread_cleanup_pop可以写多对,routine执行顺序正好相反//先进后出
4、线程内的return 可以结束线程,也可以给pthread_join返回值,但不能触发pthread_cleanup_push里面的回调函数,所以我们结束线程尽量使用pthread_exit退出线程。 -
信号量
也叫信号灯,用于同步信号,我后面会梳理相关内容。 -
互斥锁
我之前写过相关概念及用法,移步 C-线程之互斥锁、动态锁、静态锁这篇。
在实际编程中需要注意:
- 线程创建需要时间,如果主进程马上退出,那线程不能得到执行 //所以通常加 sleep(),sleep毫无疑问会导致效率变低。
- 需要在编译时候加 -lpthread,不然会报错error: ld returned 1 exit status
其他常用命令:
pthread_t pthread_self(void) 查看自己的TID
ps eLf|grep xxx 查看系统关于关键字xxx的快照
ps ef|grep xxx (和上面一行命令的区别是:少了参数L之后看不到具体的线程,因为L表示显示进程的线程信息)
线程间参数传递(难点)
- 通过地址传递参数,注意类型的转换 //注意使用sleep(1)也可以解决一些问题,但是这样效率不高,这个时候就要用到值传递。
- 值传递,这时候编译器会告警,需要程序员自己保证数据长度正确。
//定义的是int型,但是把它当做地址传。
代码展示:(注意看我解释的部分)
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
void *testThread(void *arg){
printf("This is a thread test,pid=%d,tid=%lu\n",getpid(),pthread_self());
// return NULL;
printf("This is %d thread.\n", (int)arg); //这里,我们知道该值是int型,所以直接转换成int型就行了,不用强制转换
// printf("This is %d thread.\n", *(int*)arg); // 当使用地址传递时,代码*(int*)arg将参数强转为int型后通过*取值
// pthread_exit(NULL);
while(1){
sleep(1);
}
printf("after pthread exit\n");
}
int main(){
pthread_t tid[5];
int ret;
int arg = 5;
int i;
/*该句将int型的i强制转换成了void型,testThread函数需要的是void型指针。
这里我们使用值传递,把这个值当成地址去传。该方法需要注意数据长度,程序员根据实际情况自己调整去避免数据丢失。*/
for(i=0;i<5;i++){
ret = pthread_create(&tid[i],NULL,testThread,(void *)i);
// ret = pthread_create(&tid[i],NULL,testThread,(void *)&i);
// sleep(1);
/* 对于 sleep(1) 的解释
当使用ret = pthread_create(&tid[i],NULL,testThread,(void *)&i); 我们看到传递的参数是i的地址,如果不加sleep的话,
线程创建太快了,来不及去++,导致最终程序的print的结果会是一样的。
在这里,地址传递导致效率太低了!!!!!!!!因此我们用值传递,也就是没有注释的那行代码。
*/
printf("This is main thread,tid=%lu\n",tid[i]);
}
while(1){
sleep(1);
}
}
运行上述代码编译器会警告,因为使用了值传递。我们此时可以忽略该警告,程序执行要求达到我们的目的就行了。
条件变量
应用场景:生产者消费者问题。
消费者不知道什么时候会有资源,所以需要等待生产者生产资源,为了在等待的时候不耗费CPU资源,因此我们引入条件变量。条件变量使得消费者在等待生产者生产时让线程休眠。
(下面两段话引用自https://www.cnblogs.com/yychuyu/p/13732262.html)
条件变量本身不是锁,但它也可以造成线程阻塞,通常与互斥锁配合使用,给多线程提供一个会合的场所。
如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。
函数
pthread_cond_init函数 //动态初始化条件变量
pthread_cond_destroy函数 //销毁
pthread_cond_wait函数 // 等待一个条件变量
pthread_cond_timedwait函数 // 限时等待一个条件变量
pthread_cond_signal函数 //通知一个消费线程
pthread_cond_broadcast函数 //广播通知多个消费线程
使用步骤(代码框架搭建)
步骤描述配合代码案例食用
1、初始化
类似上篇讲到的静态锁和动态锁,条件变量也是除了静态初始化,还有动态初始化。
- 静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //初始化条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //初始化互斥量
这俩通常一起使用 - 动态初始化
pthread_cond_init(&cond);
2、生产者线程
1、pthread_mutex_lock(&mutex);
2、开始产生资源
3、pthread_cond_sigal(&cond); //通知一个消费线程 (如有多个消费者,它们会争抢)
//或者
//pthread_cond_broadcast(&cond); //广播通知多个消费线程
4、pthread_mutex_unlock(&mutex);
3、消费者线程
1、pthread_mutex_lock(&mutex);
2、while (如果没有资源){ //防止惊群效应
pthread_cond_wait(&cond, &mutex);
}
3、有资源了,消费资源
4、pthread_mutex_unlock(&mutex);
在上述代码片中,我们需要注意:
第一:
步骤2非常重要,while (如果没有资源)
,加上这句话之后:
1、我们可以避免浪费出租车,也就是说当生产者生产了好几个出租车时,乘客(消费者线程)还没有到,例如当乘客到时已经生产第六辆了,那么加上while (如果没有资源)
后,乘客会把前面生产的出租车也消费掉;相反,如果没有加,乘客就会直接开始消费最新生产的出租车,浪费掉之前生产的出租车。
因此,这里我们要注意的点是:如果pthread_cond_signal或者pthread_cond_broadcast 早于 pthread_cond_wait ,则有可能会丢失信号。
2、当消费者个数多于生产者个数时,使用广播机制时避免段错误,错误会出现在下述代码的tx->next = Head处,表明指向了NULL。 pthead_cond_broadcast 信号会被多个线程收到,这叫线程的惊群效应。所以需要加上判断条件while循环。
第二:
pthread_cond_wait(&cond, &mutex)函数会自动在没有资源等待时先unlock,休眠(sleep),等资源到了,再lock。
代码案例(新手可以好好看注释喔)
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
//1、初始化
pthread_cond_t hasTaxi=PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
struct taxi{
struct taxi *next;
int num;
};
struct taxi *Head=NULL;
void *taxiarv(void *arg){
printf("taxi arrived thread\n");
pthread_detach(pthread_self()); //pthread_detach进行线程分离,三种回收线程方式之一
struct taxi *tx;
int i=1;
while(1){
tx = malloc(sizeof(struct taxi));
tx->num = i++;
printf("taxi %d comming\n",tx->num);
pthread_mutex_lock(&lock);
tx->next = Head; //Head永远指向链表的首部,最先加入的链表指向链表的末尾。
Head = tx;
pthread_cond_signal(&hasTaxi); //生产者给消费者发送信号,提醒资源到了;只能有一个消费者抢到该信号
//pthread_cond_broadcast(&hasTaxi);
pthread_mutex_unlock(&lock);
// 注意!!!在生产者函数中加sleep;但在消费者函数中不加,因为有pthread_cond_wait()函数,会自动阻塞
sleep(1); //每一秒钟,生产一辆出租车
}
pthread_exit(0); //线程无异常退出
}
void *takeTaxi(void *arg){
printf("take taxi thread\n");
pthread_detach(pthread_self());//pthread_detach进行线程分离,三种回收线程方式之一
struct taxi *tx;
while(1){
pthread_mutex_lock(&lock);
// 有车时Head!=NULL,跳过等待,直接消费;无车时,Head==NULL,执行等待直到车来
while(Head==NULL) //当不加这行时,当积攒很多车时pthread_cond_wait(&hasTaxi,&lock)进行等待
{ //等待资源到来
pthread_cond_wait(&hasTaxi,&lock); //自动阻塞,因此该函数中不用加sleep
}
tx = Head;
Head=tx->next;
printf("%d,Take taxi %d\n",(int)arg,tx->num);
free(tx);
pthread_mutex_unlock(&lock);
}
pthread_exit(0);
}
int main(){
pthread_t tid1,tid2,tid3,tid4;
pthread_create(&tid1,NULL,taxiarv,NULL); // 创建生产者线程
// sleep(5); //让生产者多生产5秒,也就是让消费者晚来5秒
pthread_create(&tid2,NULL,takeTaxi,(void*)1); //创建3个消费者的线程
pthread_create(&tid3,NULL,takeTaxi,(void*)2);
pthread_create(&tid4,NULL,takeTaxi,(void*)3);
//while(1)保证主线程不退出
while(1) {
sleep(1);
}
}
线程池
通俗的讲就是一个线程的池子,可以循环的完成任务的一组线程集合。
我们平时创建一个线程,完成某一个任务,等待线程的退出。但当需要创建大量的线程时,假设T1为创建线程时间,T2为在线程任务执行时间,T3为线程销毁时间,当 T1+T3 > T2,这时候就不划算了,使用线程池可以降低频繁创建和销毁线程所带来的开销,任务处理时间比较短的时候这个好处非常显著。
实现简单线程池思路及代码
1、创建线程池的基本结构:
任务队列链表
typedef struct Task;
线程池结构体
typedef struct ThreadPool;
2、线程池的初始化:
pool_init()
{
创建一个线程池结构
实现任务队列互斥锁和条件变量的初始化
创建n个工作线程
}
3、 线程池添加任务
pool_add_task
{
判断是否有空闲的工作线程
给任务队列添加一个节点
给工作线程发送信号newtask
}
4、实现工作线程
workThread
{
while(1){
等待newtask任务信号
从任务队列中删除节点
执行任务
}
}
5、 线程池的销毁
pool_destory
{
删除任务队列链表所有节点,释放空间
删除所有的互斥锁条件变量
删除线程池,释放空间
}
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define POOL_NUM 10
typedef struct Task{
void *(*func)(void *arg);
void *arg;
struct Task *next;
}Task;
typedef struct ThreadPool{
pthread_mutex_t taskLock;
pthread_cond_t newTask;
pthread_t tid[POOL_NUM];
Task *queue_head;
int busywork;
}ThreadPool;
ThreadPool *pool;
void *workThread(void *arg){
while(1){
pthread_mutex_lock(&pool->taskLock); //对任务队列中的头一个任务进行操作,是临界任务,防止被打扰,所以加锁
pthread_cond_wait(&pool->newTask,&pool->taskLock); //等待任务 (也就是等待pthread_cond_signal()函数释放新任务到达的信号) 等待的时候上锁
Task *ptask = pool->queue_head; //从任务队列的头中取出任务
pool->queue_head = pool->queue_head->next; // 指向头部的指针往后指 这里先往后指,真正用到了后面的空间的话后面去申请。
pthread_mutex_unlock(&pool->taskLock); //任务取完了,解锁
ptask->func(ptask->arg); //执行任务
pool->busywork--; //执行完了,busywork--
}
}
void *realwork(void *arg){
printf("Finish work %d\n",(int)arg);
}
void pool_add_task(int arg){
Task *newTask;
pthread_mutex_lock(&pool->taskLock);
while(pool->busywork>=POOL_NUM){ //pool->busywork 访问线程池的临界资源,前加锁
pthread_mutex_unlock(&pool->taskLock);
usleep(10000);
pthread_mutex_lock(&pool->taskLock);
}
pthread_mutex_unlock(&pool->taskLock);
//为新任务开辟内存空间,并将要执行的函数赋值给新任务
newTask = malloc(sizeof(Task));
newTask->func = realwork;
newTask->arg = arg;
//上锁,将任务放到线程池中
pthread_mutex_lock(&pool->taskLock);
Task *member = pool->queue_head;
if(member==NULL){
pool->queue_head = newTask;
}else{
while(member->next!=NULL){
member=member->next;
}
member->next = newTask;
}
pool->busywork++;
//通知等待的线程,任务来啦
pthread_cond_signal(&pool->newTask); //条件变量被触发,等待的线程会被唤醒
pthread_mutex_unlock(&pool->taskLock);
}
void pool_init(){
pool = malloc(sizeof(ThreadPool));
pthread_mutex_init(&pool->taskLock,NULL);
pthread_cond_init(&pool->newTask,NULL);
pool->queue_head = NULL;
pool->busywork=0;
for(int i=0;i<POOL_NUM;i++){
pthread_create(&pool->tid[i],NULL,workThread,NULL);
}
}
void pool_destory(){
Task *head;
while(pool->queue_head!=NULL){
head = pool->queue_head;
pool->queue_head = pool->queue_head->next;
free(head);
}
pthread_mutex_destroy(&pool->taskLock);
pthread_cond_destroy(&pool->newTask);
free(pool);
}
int main(){
pool_init();
sleep(20);
for(int i=1;i<=20;i++){
pool_add_task(i);
}
sleep(5);
pool_destory();
}
对于程序的理解:
首先在对线程初始化的时候就创建线程,线程的执行函数做的就是:等待任务到来,执行任务,告诉线程池任务执行了一个任务。
取任务的指针往后指。
pool_add_task函数。
为新任务开辟内存空间,并将要执行的函数赋值给新任务,将任务放到线程池中,通知等待的线程,任务来啦
以上是个人的一些学习总结和心得体会。参考了一些网络资源,文中有相应链接对应。后面还有学习心得会继续补充。新手,如有建议或写的不对的地方,欢迎讨论一下哦。欢迎交流,共同进步!