1, 线程
线程(thread)也是并发 的一种形式,线程是比进程更小的活动单位,一个进程中可以有多个线程,线程是进程内部的一个执行分支。
一个进程刚开始时只有一个线程(称之为主线程),后续的代码中可以创建新的线程,可以指定新线程去执行某个函数,这个函数称之为线程函数。
进程内部的多个线程共享该进程内部的所有数据,所以线程间通信相比进程简便很多,如:直接使用全局变量就行
2,线程相关函数
2.1 创建一个新线程
pthread_t类型变量用来表示一个线程的id,线程id具有唯一性 void *(*start_routine) (void *) start_routine 是函数指针,指向一个返回值为void*,参数为void*的函数(这样的函数,称之为线程函数) pthread_create - create a new thread SYNOPSIS #include <pthread.h> int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); thread:pthread_t类型变量的地址,该变量用来保存新线程的id attr:用来指定线程的属性,一般为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:线程id retval:二级指针,一般是定义一个一级指针变量,把这个变量的地址作为参数传入, 成功后,改变了保存了 退出线程的 退出码。如果不需要保存退出码,该参数为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:条件变量的地址