linux下多线程
首先我们在linux下学习的线程并不是真正的线程,而是用进程模拟的线程,所以一个进程也被叫做线程组,线程是一个进程内部的执行序列,一条执行流。进程是资源竞争的基本单位,线程是程序执行的最小单位,不过线程虽然是进程模拟的,可是线程也有类似于线程id等自己的东西。进程与线程相比安全性更高一点,不过线程由于是轻量级进程所以更加轻便简洁。
进程ID与线程ID
我们在创建线程的时候,为每一个线程都创建了task_struct,这样的话每一个线程的task_struct都有一个进程ID,其实这个不是进程ID而是线程ID,而这个线程组的task_struct的进程ID与首线程一样,同时也是我们看到的用户层的进程ID或进程组ID。
查看进程内所有线程信息: ps-eLf其中有一列叫LWP 这个直译过来是轻量级进程,也就是线程pid NLWP就是进程中几个线程用户态展现出来pid这一列其实是tgid线程没有父子之分,所有线程都是平级的,非要有区别就是主线程与其他线程之分。
线程的优缺点
所有东西都是相对的,线程与进程没有好坏之分,只有使用场景的不同,接下来对比进程与线程。
线程优点:
一个进程中可能会有多个线程,而这些线程共享同一个虚拟地址空间
1.因此有时会也说线程是运行在进程中的,
2.因为共享地址空间,线程间通信变得极为方便。
3.创建或销毁一个线程相较于进程来说成本更低(不需额外创建虚拟地址空间…)
4.线程的调度切换相较于进程较低线程
缺点:
1.一位内线程间的数据访问变得更加简单,因此数据安全问题更加突出
2.一些系统调用和异常都是针对整个进程的,因此一个线程崩溃了整个进程都会崩溃。
线程控制
系统并没有提供线程的调用接口,所以有大佬在posix库中自己实现了一套线程控制函数,包括线程创建,分离,等待等。所以编译的时候要链接库 -lpthread。
线程的创建
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
创建一个线程,成功返回0,否则返回-1.
thread是线程id
attr是设置属性,一般置NULL
start_routine是线程要完成的动作
arg是start_rotine所需要的参数
pthread_create接口创建了一个用户线程,并且通过第一个参数返回了一个用户线程id,这个id非常大,其实他是一个地址,是一个指向线程自己的线程地址空间在这个进程虚拟地址空间中的位置。
每一个线程都需要有自己的栈区,否则如果所有线程共用一个栈的话,会引起调用栈混乱,并且因为cpu是以pcb来调度的,因此线程是cpu调度的基本单位,所以每个线程也都应该有自己的上下文数据来保存cpu调度切换的数据(而这些都是在线程地址空间中,每一个线程都有自己的线程地址空间,他们相对来说独立)
线程的退出
线程有三种退出方式
1.在线程函数中return退出,不过不适用于主线程,会导致进程退出
2.调用pthread_exit退出这个线程,切记不能使用exit函数直接退出,这样会导致直接退出进程
3.调用pthread_cancel让同一线程组的线程退出
void pthread_exit(void *value_ptr);
value_ptr是一个空类型的指针.保存退出时返回的值,不要指向一个局部变量
int pthread_cancel(pthread_t thread);
thread是线程ID,成功返回0,失败返回错误码
线程退出很大一个问题是退出后临近资源的处理,当我们退出一个线程但是资源没有回收的时候,如果其他线程也要获取这个临界资源,那么就会无限的等下去,linux为我们提供了两个函数解决这个问题
#define pthread_cleanup_push(routine,arg)
{ struct _pthread_cleanup_buffer _buffer;
_pthread_cleanup_push (&_buffer, (routine), (arg));
#define pthread_cleanup_pop(execute)
_pthread_cleanup_pop (&_buffer, (execute)); }
这里提供线程创建与退出的代码
5 #include <unistd.h>
6 #include <stdlib.h>
7 #include <pthread.h>
8
9 void *thr_start(void *arg)
10 {
11 while(1) {
12 printf("child pthread!!\n");
13 sleep(1);
14 }
15 return NULL;
16 }
17 int main()
18 {
19 pthread_t tid;
20 int ret = -1;
21
22 ret = pthread_create(&tid, NULL, thr_start, NULL);
23 if (ret != 0) {
24 printf("pthread_create error\n");
25 return -1;
26 }
27 pthread_cancel(tid);
28 while(1) {
29 printf("main thread!!!\n");
30 sleep(1);
31 }
32 return 0;
33 }
线程等待与分离
线程退出的时候有时候会形成僵尸线程,导致没办法清理资源,导致其他线程不能复用资源,所以我们需要等待线程,由一个线程等待另一个线程退出然后回收资源。
线程有一个joinable状态,如果线程处于这个状态那么表示线程处于被等待状态,当我们创建一个线程的时候,线程默认处于joinable状态。
int pthread_join(pthread_t thread, void **retval);
thread:用于指定等待哪个线程
retval:用于接收线程的返回值
成功返回0,失败返回错误码
如果线程已经退出,那么将直接返回,否则阻塞等待。
如果我们不关心一个线程的退出状态,我们可以将它设置为分离状态,分离状态是和joinable状态相对的状态,表示线程退出后系统回收资源。而设置这个分离状态的过程被叫做线程分离。
线程的datach属性与joinable属性相对应,也相冲突,两者不会同时存在如果一个线程属性是datach属性,那么调用pthread_join将直接报错,所以我们说只有一个线程处于joinable才可被等待
int pthread_datach(pthread_t thread)
用这个函数设置一个线程的分离属性,成功返回0,失败返回错误码
线程安全
因为进程中的线程共享了进程的虚拟空间,因此线程间通信将变得简单,但是缺点也随之而来,这个缺点就是,缺乏数据的访问控制更容易造成数据混乱
我们把能造成数据混乱的情况总结了两个比较经典的模型,他们都是描述多个进程/线程之间在数据访问时候所应该保持的关系,不至于造成混乱
一般用线程的同步与互斥来实现线程安全
同步:对临界资源操作的时序性,一般用条件变量实现
互斥:对同一时间临界资源操纵的唯一性,一般用互斥锁实现
线程安全的经典模型:生产消费者模型
生产者与生产者关系:互斥,都在抢操作一个资源,如果要实现安全访问要保证互斥生产者与消费者关系:同步+互斥:只有生产出来才能消费,讲究时序制约
消费者与消费者关系:互斥
互斥锁
我们一般用互斥锁来实现线程间的互斥,当一个线程要操作临界资源的时候,先获取互斥锁,互斥锁获取后其他线程就不能再次获取,当操作完毕后对锁进行释放。从而来实现线程间的互斥。
互斥锁的初始化有两种方式,一种是赋值初始化,这一种不用自己释放,还有一种是函数初始化,这一种要自己释放互斥锁。
pthread_mutex_t mutex = PYHREAD_MUTEX_INITALIZER 这个是赋值初始化
int pthread_mutex_init(pthread_mutex_t *mutex,const ptrread_mutexatter_t *attr)
互斥锁的初始化函数成功返回0,失败返回错误码
mutex是互斥锁变量,attr是互斥锁属性,不关心置NULL
加锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);阻塞加锁,如果获取不到锁就阻塞等待锁被解开 int pthread_mutex_trylock(pthread_mutex_t *mutex);非阻塞加锁,如果获取不到锁就立刻报错返回 int pthread_mutex_timedlock(pthread_mutex_t *mutex,struct timespec *t);限时阻塞加锁,如果获取不到锁就等待指定时间,如果这段时间内一直获取不到锁就报错返 回,否则加锁。
解锁:
int pthread_mutex_unlock(pthread_mutex_t *mutex);加锁后在任意有可能推出的地方都要进行解锁,否则有可能卡死
使用互斥锁也是有风险的,因为多了一次操作,所以有可能产生死锁情况,死锁要满足以下条件:
1.互斥条件—只有一个线程可以获取
2.不可剥夺条件—一个线程获取了锁,其他进程不能释放
3.请求与保持条件—拿了第一个锁之后又去获取第二个锁,没有获取到锁二不释放锁一
4.环路等待条件—线程一拿了锁一去请求锁
对于死锁情况又大佬提出了著名的银行家算法来解决死锁情况
银行家算法简单来说就是有一个银行家给一些客户发不同额度的信用卡,满足客户不多于额度的提款要求,其实银行家每一次只用将大的额度订单挂起,先处理最小额度的订单,等待小额度的订单还款后在满足大额度订单即可,不过当银行家手中资源不能满足最小额度的时候还是会产生死锁情况,这里推荐一篇关于银行家算法的详细博客银行家算法
销毁互斥锁:pthread_mutex_destroy(&mutex) 仅用于函数释放
条件变量
条件变量是线程为了实现线程同步而使用,条件变量一般和互斥锁一起使用实现线程间同步与互斥
与互斥锁一样,条件变量有两种初始化方式,一种是赋值初始化,一种是函数初始化,这一种需要自己释放条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond,const ptrread_mutexatter_t *attr)
成功返回0,失败返回错误码
cond是操作的条件变量,attr是状态选项,不关心置NULL
有一种情况是当消费者先获取到了锁,但是生产者还没来得及生产东西,这个时候消费者会形成死锁,消费者要通过函数将锁子释放,先让生产者生产出来东西再获取锁进行消费
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
这个函数就是为了避免上述情况,消费者获取到锁后先看有没有资源,没有资源将锁子释放
成功返回0,失败返回错误码,cond是条件变量,mutex是互斥锁
当生产者生产结束后要通过条件变量来唤醒消费者
int pthread_cond_broadcast(pthread_cond_t *cond)唤醒所有等待在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond)唤醒第一个等待变量的线程
不得不说这段代码导致markdown崩了很多次,还让作者重新了百分之九十的博客,最后无奈在markdown里边完成了整段代码,谨以此段文字献祭2018年9月28日早上被我消耗的三小时
#include <stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<stdlib.h>
#include<pthread.h>
#include<string.h>
pthread_cond_t cond;
pthread_mutex_t mutex;
int basket = 0;
void *sale(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
if(basket == 0)
{
printf("sale!\n");
basket = 1;
pthread_cond_tsignal(&cond);
}
pthread_mutex_unlock(&mutex);
}
reutrn NULL;
}
void *buy(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
if(basket == 0)
{
pthread_cond_wait(&cond,&mutex);
}
printf("nuy success!\n");
basket = 0;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t tid1,tid2;
int ret;
pthread_cond_init(&cond,NULL);
pthread_mutex_init(&mutex,NULL);
ret = pthread_create(&tid1,NULL,sale,NULL);
if(ret != 0)
{
perror("creat error!\n");
return -1;
}
ret = pthread_create(&tid2,NULL,buy,NULL);
if(ret != 0 )
{
perror("creat error!\n");
return -1;
}
pthread_join(&tid1,NULL);
pthread_join(&tid2,NULL);
pthread_mutex_destory(&mutex);
pthread_cond_destory(&cond);
return 0;
}
以上就是关于线程基础的全部内容,不全待补充!