1, 线程
线程(thread)也是并发的一种形式,线程是比进程更小的活动单位,一个进程中可以有多个线程,线程是进程内部的一个执行分支。
一个进程刚开始时只有一个线程(称之为主线程),后续的代码中可以创建新的线程,可以指定新线程去执行某个函数,这个函数称之为线程函数。
进程内部的多个线程共享该进程内部的所有数据,所以线程间通信相比进程简便很多,如:直接使用全局变量就行
2,线程相关函数
2.1 创建一个新线程
pthread_t类型变量用来表示一个线程的id,线程id具有唯一性void *(*start_routine) (void *) start_routine 是函数指针,指向一个返回值为void*,参数为void*的函数(这样的函数,称之为线程函数)pthread_create - create a new threadSYNOPSIS#include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);thread:pthread_t类型变量的地址,该变量用来保存新线程的idattr:用来指定线程的属性,一般为NULL,表示采取默认属性,如果不想采用默认属性,后续会有函数来修改线程属性。start_routine:线程函数的地址。新线程创建成功后就去执行该函数arg:线程函数的参数Compile and link with -pthread.
2.2 线程结束
(1) 正常结束
该执行的指令都执行完了(线程函数执行完了)
(2) 进程结束了,进程内的所有线程都结束
(3) 在线程执行的任意时刻,调用 pthread_exit 函数,该线程就会退出/结束
#include <pthread.h>void pthread_exit(void *retval);retval:线程 退出码/返回值 的地址
(4) 被别人干掉
#include <pthread.h>int pthread_cancel(pthread_t thread);thread:需要被干掉的那个线程的id线程有一个属性,可以决定是否能被别人干掉,可以用一个函数来设置#include <pthread.h>int pthread_setcancelstate(int state, int *oldstate);state: PTHREAD_CANCEL_ENABL3E 可以被干掉(默认属性)PTHREAD_CANCEL_DISABLE 不能被干掉oldstate:用来保存改变之前的属性,如果不关心改变之前的属性,就为NULL
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
int x = 0;
void func1()//普通函数
{int i;for(i=0;i<100;i++){printf("主线程i=%d\n",i);usleep(100);}
}void* func2(void *arg)//线程函数
{int i;for(i=0;i<100;i++){printf("-----------新线程i=%d\n",i);usleep(100);}
}void* func3(void *arg)//线程函数
{int i;printf("参数:%s\n",(char *)arg);for(i=0;i<100;i++){printf("-----------新线程i=%d\n",i);usleep(100);}
}void* func4(void *arg)//线程函数
{x += 100;int i;printf("参数:%d\n",*((int *)arg));for(i=0;i<100;i++){printf("-----------新线程i=%d\n",i);usleep(100);}}int main()
{//func1();//func1是普通函数,直接调用,并没有并发,先执行完func函数再往下执行pthread_t tid;//用来保存新线程的id//pthread_create(&tid,NULL,func2,NULL);//创建新线程执行 func2函数,并且把参数NULL传递给func2//char buf[] = "线程参数测试";//pthread_create(&tid,NULL,func3,(void*)buf);//创建新线程执行 func3函数,并且把参数buf传递给func3//传递字符串int data = 100;pthread_create(&tid,NULL,func4,(void*)&data);//创建新线程执行 func4函数,并且把参数&data传递给func4//传递整数int i;for(i=0;i<100;i++){printf("主线程i=%d\n",i);usleep(100);}printf("x=%d\n",x);return 0;//进程结束了,所有线程立马都结束//pthread_exit(NULL);//只是退出当前线程(主线程),其他线程如果没有执行完,不会结束
}
2.3 资源回收
一个线程结束了,并不代表所有资源都被释放了,有两种方式回收线程资源:自动回收和手动回收
由一个属性决定是自动回收还是需要手动回收,该属性默认是需要手动回收资源,如果需要自动回收,调用
pthread_detach函数
#include <pthread.h> int pthread_detach(pthread_t thread);thread:线程id,把这个线程设置为自动回收资源失败返回-1,成功返回0 例如:pthread_detach(pthread_self());//设置自动回收该线程的资源//等待线程结束,并手动回收资源 #include <pthread.h> int pthread_join(pthread_t thread, void **retval);thread:线程idretval:二级指针,一般是定义一个一级指针变量,把这个变量的地址作为参数传入,成功后,改变了保存了 退出线程的 退出码。如果不需要保存退出码,该参数为NULL失败返回-1,成功返回0二者选一即可
多线程并发也会有和多进程并发一样的问题:访问共享资源时被打断,而造成不可预知的后果
所以多线程并发时,也需要PV操作,可以用之前学过的信号量,但是有更好的方法:线程互斥锁
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void * func1(void *arg)
{//pthread_detach(pthread_self());//设置自动回收该线程的资源int i;for(i=0;i<100;i++){printf("-----------新线程i=%d\n",i);usleep(100);} int *p = (int*)malloc(4);*p = 200;//退出码pthread_exit((void*)p);
}int main()
{pthread_t tid;pthread_create(&tid,NULL,func1,NULL);int i;for(i=0;i<100;i++){printf("主线程i=%d\n",i);usleep(100);}int *p;int r = pthread_join(tid,(void*)&p);//等待tid线程结束并 手动回收tid线程的资源if(-1==r){perror("pthread_join失败");}printf("线程退出码:%d\n",*p);free(p);return 0;//进程结束了,所有线程立马都结束
}
3,线程互斥锁
pthread_mutex_t类的变量就是线程互斥锁
3.1 初始化线程互斥锁
#include <pthread.h> int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr); mutex:线程互斥锁的地址attr:互斥锁的属性,一般为NULL,表示默认属性(如:初始化之后为解锁状态)失败返回-1,成功返回0
3.2 P操作
#include <pthread.h> int pthread_mutex_lock(pthread_mutex_t *mutex);//如果是死锁状态,一直等int pthread_mutex_trylock(pthread_mutex_t *mutex);//尝试获取锁资源int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);//限时等待
3.3 V操作
int pthread_mutex_unlock(pthread_mutex_t *mutex);
3.4 销毁互斥锁
#include <pthread.h>int pthread_mutex_destroy(pthread_mutex_t *mutex);
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <string.h>
#include <semaphore.h>pthread_mutex_t mutex;//线程互斥锁//多线程并发时,也需要PV操作,可以用之前学过的信号量,但是有更好的方法:线程互斥锁
int data = 0;//共享资源//信号量
void *func1(void *arg)
{sem_t *s = (sem_t *)arg;int i;for(i=0;i<1000000;i++){sem_wait(s);data++;sem_post(s);}
}//线程互斥锁
void * func2(void *arg)
{int i;for(i=0;i<1000000;i++){pthread_mutex_lock(&mutex);data++;pthread_mutex_unlock(&mutex);}
}int main()
{pthread_mutex_init(&mutex,NULL);pthread_t tid1,tid2;pthread_create(&tid1,NULL,func2,NULL);pthread_create(&tid2,NULL,func2,NULL);pthread_join(tid1,NULL);pthread_join(tid2,NULL);printf("data=%d\n",data);pthread_mutex_destroy(&mutex);return 0;
}
练习:
模拟一个外卖店,有如下要求:
该店菜品有 辣椒炒肉,剁椒鱼头,水煮肉片,麻婆豆腐,红烧肉,糖醋排骨,空心菜,大白菜
每隔时间t1有顾客下单,下单的间隔时间t1随机生成,用 sleep/usleep模拟,下单菜品也是随机生成
两个外卖小哥接单,假设送单时间为t2,也是随机生成,用 sleep/usleep模拟
先下单的一定会被先接单,且同一个外卖小哥只能送完一单才能接下一单
该外卖店最多只接100单,完成则下班
如果累计有20单未被接单,则暂停下单
下单和接单信息打印输出
顾客 -》一个线程
每个外卖小哥 -》 一个线程
void * func1(void *arg)//顾客线程 {while(1)//一直下单,间隔时间为 t1{sleep(t1);下单 -> 下单信息保存到队列 入队} }void *func2(void *arg)//外卖小哥线程 {while(1)//一直接单,送单时间为 t2{接单 -> 出队模拟送单 -> sleep(t2)} }
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include "queue.h"
char * name[8] = {"辣椒炒肉","剁椒鱼头","水煮肉片","麻婆豆腐","红烧肉","糖醋排骨","空心菜","大白菜"};
int count = 0;//记录下单的总数量
pthread_mutex_t mutex;
pthread_cond_t cond;
pthread_cond_t cond2;
//模拟顾客下单void * func1(void *arg)
{Queue * l = (Queue*)arg;int t1;//顾客下单的间隔时间int index;//顾客下单菜品的下标while(1){t1 = rand() % 10 + 10;//间隔时间假设为 [1,3]秒钟sleep(t1);pthread_mutex_lock(&mutex);if(count >= 100){pthread_mutex_unlock(&mutex);break; }else{if(l->num < 20){//下单,入队index = rand()%8;push(l,index);count++;printf("顾客下单了,这是第%d单,菜品是:%s\n",count,name[index]); pthread_cond_signal(&cond);} else{//订单>=20,暂停下单pthread_cond_wait(&cond2,&mutex);}}pthread_mutex_unlock(&mutex);}
}//模拟外卖小哥接单
void* func2(void *arg)
{Queue * l = (Queue*)arg;int t2;//小哥送单的时间int index;while(1){//接单pthread_mutex_lock(&mutex);if(is_empty(l) == 0){index = get_front(l);//获取订单pop(l);printf("外卖小哥接单,菜品名:%s\n",name[index]);pthread_cond_signal(&cond2);}else{if(count >= 100){pthread_mutex_unlock(&mutex);break;}pthread_cond_wait(&cond,&mutex);printf("一直在循环判断,但是条件一直不成立,浪费CPU\n");}pthread_mutex_unlock(&mutex);t2 = rand() % 1 + 1;//送单时间假设为 [1,3]秒钟sleep(t2);}}int main()
{Queue * l = init_queue();pthread_mutex_init(&mutex,NULL);pthread_cond_init(&cond,NULL);pthread_cond_init(&cond2,NULL);pthread_t tid1,tid2,tid3,tid4;pthread_create(&tid1,NULL,func1,(void*)l);pthread_create(&tid2,NULL,func1,(void*)l);pthread_create(&tid3,NULL,func2,(void*)l);pthread_create(&tid4,NULL,func2,(void*)l);pthread_join(tid1,NULL);pthread_join(tid2,NULL);pthread_join(tid3,NULL);pthread_join(tid4,NULL);pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);pthread_cond_destroy(&cond2);destroy_queue(l);return 0;
}
4,线程条件变量
前面的例子中,外卖小哥使订单减少,顾客使订单增加,是一个典型的 消费者-生产者 模型。
如果数据“已满”(超过20单),生产者应该停止生产,如果数据“空了”,消费者停止消费。
问题是消费者怎么知道数据空了?生产者怎么知道数据已满?
常规的做法就是循环判断,一直不间断的进行判断,直到满足我的条件。
生产者的条件:数据没有满
消费者的条件:数据不为空
这种常规做法有一个缺点:浪费CPU资源,有时候一直循环但是都是不满足条件
-》线程条件变量
在条件不满足的时候,进行休眠(是指进入阻塞态,让出CPU);
当条件满足时,需要别人来唤醒我;(消费者休眠一般由生产者唤醒,生产者休眠一般由消费者唤醒)
4.1 初始化线程条件变量
pthread_cond_t 类型的变量就是线程条件变量 #include <pthread.h> int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *cattr);cond:要初始化的线程条件变量的首地址cattr:属性,一般为NULL,表示默认属性失败返回-1,同时errno被设置成功返回0
4.2 进入阻塞状态,等待条件满足
#include <pthread.h>int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);cond:线程条件变量地址mutex:线程互斥锁,在执行 pthread_cond_wait操作前,必须线对 mutex进行lock/p 操作因为在pthread_cond_wait函数内部会对 mutex进行 unlock/V 操作int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);
4.3 唤醒正在阻塞的线程
#include <pthread.h>int pthread_cond_signal(pthread_cond_t *cond);cond:条件变量的地址,唤醒阻塞在该条件变量上的任意一个线程int pthread_cond_broadcast(pthread_cond_t *cond);//broadcast 广播cond:条件变量的地址,唤醒阻塞在该条件变量上的所有线程
4.4 销毁线程条件变量
int pthread_cond_destroy(pthread_cond_t *cond);cond:条件变量的地址