以下是阿鲤对Linux下线程的总结,希望对大家有所帮助;若有误请慷慨指出。
1:线程概念
2:线程控制
2:线程安全
4:死锁
注:以下的代码实现均为centos7环境;
一:线程概念
1:线程的介绍:
在传统操作系统中对程序的描述方式分为pcb和tcb即进程和线程;而在Linux下其进程和线程均是通过pcb进行描述的;pcb:是一个文件描述信息,其使用虚拟地址空间对内存进行访问;在这里就有了进程和线程的区别。一个进程拥有一个虚拟地址空间,而多个线程拥有一个虚拟地址空间;可以这么理解,多个线程(一个线程组)构成一个进程;所以说Linux下没有真正的线程。Linux下的线程是轻量级进程。图解如下:
所以进程是系统资源分配的基本单位,而线程是cpu调度的基本单位
2:线程的独有与共享:
独有:栈(函数调用),寄存器(程序信息),信号屏蔽字(pcb中的阻塞信号),errno(错误信息),标识符(存储在共享区,在创建中使用tid返回首地址);
共享:虚拟地址空间(代码段,数据段),文件描述符表(可以减少文件打开次数),信号处理方式(信号是针对进程的),工作路径,用户ID,组ID
3:多进程与多线程多任务处理比较
多线程特点:
1:线程间通信很方便(包括进程间通信+全局变量等);
2:线程的创建与销毁成本低;
3:线程间的调度成本低;
4:指向粒度更加细腻
多进程特点:
1:具有独立性,因此更加的稳定,健壮;
共同特点:
1:并行压缩cpu处理/IO等待时间
所以对主功能程序安全稳定性要求高的最好使用多进程,剩下的使用多线程。
二:线程控制
注:Linux下线程控制的接口都是库函数(操作系统没有向用户提供一个轻量级进程接口,因此大佬们就对进程控制的接口进行了封装,从而封装出线程控制库函数)
1:线程创建
接口:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
*thread:用于获取线程ID,通过这个ID可以找到线程描述信息,进而访问pcb(线程的首地址,存储在虚拟地址的共享区);
attr:线程熟悉,通常NULL;
start_routine:线程入口函数,创建的线程就是为了运行这个函数,函数运行完毕,线程退出;
arg:通过线程入口函数,传给线程的参数(通信)
return:0-成功, 非0值-失败
eg:
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
void *func(void *arg)
{
while(1)
{
printf("The main thread passed me a parameter:%d\n",*((int*)(arg)));
sleep(1);
}
return NULL;
}
int main()
{
int tmp = 888;
pthread_t tid;
int ret = pthread_create(&tid, NULL, func, (void*)&tmp);
if(ret != 0)
{
printf("thread create error: %d\n", ret);
return -1;
}
while(1)
{
printf("I am main thread\n");
sleep(1);
}
return 0;
}
注意:因为是库函数,所以需要使用-l进行库的链接;
线程查看:
如上图使用ps -efL | head -n 1 && ps -efL | grep create是查看线程的命令(其中L选型为轻量级进程);其中LWP是线程ID,PID是进程ID;所以我们可以看出来进程ID是主线程ID。
2:线程终止
终止方式
普通线程入口函数中的return(mian函数中的return退出的是进程);
void pthread_exit(void *retval);退出一个线程,谁调用谁退出 retval-线程返回值;
int pthread_cancel(pthread_t thread);取消一个指定的线程;tid:指定的线程id;
注意:
线程退出也不会完全释放资源,需要被其他线程等待;
使用int pthread_cancel(pthread_t thread);取消自己是一个违规操作;
主线程退出,其他线程正常运行,这也不是主流做法
主线程退出,并不影响整个进程的运行,只有所有线程退出,进程才会退出。
3:线程等待
等待一个线程的退出,获取退出线程的返回值,回收这个线程所占用的资源
接口:
int pthread_join(pthread_t thread, void **retval);
thread:等待线程的id
retval:用于获取线程退出的返回值
return:0-成功, 非0值-失败
eg:
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
void *func(void *arg)
{
sleep(3);
char *buf = "BelongAl";
pthread_exit(buf);
return NULL;
}
int main()
{
pthread_t tid;
int tmp;
int ret = pthread_create(&tid, NULL, func, (void*)&tmp);
if(ret != 0)
{
printf("thread create error: %d\n", ret);
return -1;
}
void *retval = NULL;
pthread_join(tid, &retval);
printf("%s\n",(char*)retval);
return 0;
}
注:不是所有的线程都能被等待,一个线程被创建,默认情况下有一个属性joinable;处于joinable属性的线程退出后,不会被自动释放,需要等待;
4:线程分离
将线程的属性从joinable设置为detach;处于detach属性的线程退出后会自动释放资源,不需要被等待。
接口:
int pthread_detach(pthread_t thread);
thread:指定线程id
return:0-成功, 非0值-失败
注:
等待一个被分离的线程,则pthread_detach会返回错误:这不是一个joinable线程(因为在获取返回值时将获取不到,detach属性的线程退出后已经自动的释放了资源)。
线程的分离可以在任意地方,可以在线程入口函数中让线程分离自己,也可以让创建线程在创建之后直接分离。
三:线程安全
1:概念:在多个执行流中对同一个临界资源进行操作访问,而不会造成数据二义;
2:方法
互斥:通过保证同一时间只有一个执行流可以对临界资源进行访问,来保证数据访问的安全性。
同步:通过一些条件判断来实现多个执行流对临界资源访问的合理性(有资源则访问,无资源则等待直到有资源被唤醒)。
3:互斥的实现:互斥锁
1:第一个线程访问时,判断可以访问,因此将状态置为不可访问(计数器置为0),然后去访问资源
2:其他线程访问的时候,发现不可访问,就陷入等待(将线程置为可中断休眠状态)
3:第一个线程访问临界资源完毕后,将状态置为可以访问(计数器置为1),唤醒等待的线程(将线程置为运行状态),被唤醒的进程开始竞争这个资源
计数器原子性操作原理:我们可以发现,每一个线程都可以对计数器进行修改,那么只有计数器修改操作是原子性的,才可以保证线程安全;那么计数器是怎样实现原子性呢?请看下图
互斥锁操作接口:
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr):锁的初始化
int pthread_mutex_lock(pthread_mutex_t *mutex);加锁(阻塞)
int pthread_mutex_trylock(pthread_mutex_t *mutex);尝试加锁(非阻塞)
int pthread_mutex_unlock(pthread_mutex_t *mutex);解锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);销毁锁
注:pthread_mutex_t:互斥锁变量类型;attr:互斥锁属性
eg:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
int tickets = 100;//表示有100张票
pthread_mutex_t mutex;
void *thr_tout(void *arg)
{
while(1)
{
pthread_mutex_lock(&mutex);//阻塞加锁
if(tickets > 0)
{
printf("tout:%p - get a ticket:%d\n",pthread_self(), tickets);
tickets--;
pthread_mutex_unlock(&mutex);//解锁
}
else
{
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);//线程退出
}
}
return NULL;
}
int main()
{
int i = 0, ret;
pthread_t tid[4];
//互斥锁初始化
//pthread_mutex_init() 或 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
pthread_mutex_init(&mutex, NULL);
for(; i < 4; i++)
{
ret = pthread_create(&tid[i], NULL, thr_tout, NULL);
if(ret != 0)
{
printf("thread cerat error\n");
return -1;
}
}
for(i = 0; i < 4; i++)//线程等待
{
pthread_join(tid[i], NULL);
}
pthread_mutex_destroy(&mutex);
return 0;
}
4:同步的实现:条件变量
有资源的时候可以获取,没有资源的时候则需要让线程等待,等待被唤醒(其他线程产生一个资源的时候)
条件变量:向用户提供了两个接口,使一个线程等待接口和唤醒一个线程接口+等待队列;条件变量只是提供了等待与唤醒的功能,但是什么时候等待,什么时候唤醒,需要用户自己来做判断
操作接口:
pthread_cond_t :条件变量类型
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);//初始化
cond:条件变量
attr:条件变量属性
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//初始化
int pthread_cond_destroy(pthread_cond_t *cond);//销毁
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);//使当前执行流等待,加入等待队列
int pthread_cond_signal(pthread_cond_t *cond);//唤醒至少一个等待队列中的执行流
int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒所有等待队列中的执行流
eg:吃面做面模型
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
//定义两个条件变量是防止在唤醒时唤醒错误
pthread_cond_t gourment_cond;//定义一个美食家变量
pthread_cond_t chief_cond;//定义一个厨师变量
int have_delicacy = 0;//刚开始并没有美食
pthread_mutex_t mutex;//定义一个互斥锁
void *gourment(void *arg)//美食家函数
{
while(1)
{
pthread_mutex_lock(&mutex);//加锁
while(have_delicacy == 0)/*如果没有美食则需要等待,但是在等待前需要先解锁//循环的原因为 pthread_cond_signal函数是唤醒至少一个,为了防止争抢混乱所采用的。例如在厨师做了美食之后,唤醒了一个或多个美食家若不采用循环,只有一个美食家会进入而其他美食家会陷入锁的阻塞,在这个美食家吃完之后会解锁然后再唤醒厨师,但是再唤醒厨师之前,那些阻塞再锁前的美食家可能会先行加锁但是因为不是循环等待,则会导致休眠之后解锁直接去吃美食,导致逻辑混乱 */
{
pthread_cond_wait(&gourment_cond, &mutex);//等待函数包括:解锁-》休眠-》加锁
}
printf("really delicious\n");
have_delicacy--;
pthread_mutex_unlock(&mutex); //解锁
pthread_cond_signal(&chief_cond); //唤醒厨师
}
return NULL;
}
void *chief(void *arg)//厨师长函数
{
while(1)
{
pthread_mutex_lock(&mutex);//加锁
while(have_delicacy == 1)//如过做好了没有人吃则陷入等待
{
pthread_cond_wait(&chief_cond, &mutex);
}
printf("I made a bowl of Buddha jumping over the wall\n");
have_delicacy++;
//做出没事之后唤醒等待的美食家
pthread_mutex_unlock(&mutex);//解锁
pthread_cond_signal(&gourment_cond);//唤醒美食家
}
return NULL;
}
int main()
{
pthread_t gourment_tid, chief_tid;
int ret;
pthread_cond_init(&chief_cond, NULL);//初始化厨师条件变量
pthread_cond_init(&gourment_cond, NULL);//初始化美食家条件变量
pthread_mutex_init(&mutex,NULL);//初始化互斥锁
int i = 0;
for(i = 0; i < 4; i++)
{
ret = pthread_create(&gourment_tid, NULL, gourment, NULL);//创建美食家线程
if(ret != 0)
{
printf("pthread create error\n");
return -1;
}
}
for(i = 0; i < 4; i++)
{
ret = pthread_create(&chief_tid, NULL, chief, NULL);//创建厨师线程
if(ret != 0)
{
printf("pthread create error\n");
return -1;
}
}
pthread_join(gourment_tid, NULL);
pthread_join(chief_tid, NULL);
pthread_cond_destroy(&chief_cond);//销毁厨师条件变量
pthread_cond_destroy(&gourment_cond);//销毁美食家条件变量
pthread_mutex_destroy(&mutex);//销毁互斥锁
return 0;
}
4:同步的实现:信号量(POSIX标准)(也可实现互斥)
本质:计数器+等待队列+向外提供的使执行流阻塞/唤醒的功能接口。通过对资源进行计数,统计当前资源数量,通过自身的计数,就可以进行条件判断,是否能够进行操作,若不能获取资源,则阻塞当前执行流。在程序初始化阶段,根据实际资源数量初始化信号量计数器,在每次获取资源之前先获取信号量(先去判断计数器是否小于0,若大于0,则计数-1,直接返回,获取数据,否则阻塞当前执行流;其他执行流生产一个资源后,先判断计数器是否<0,若小于0,则唤醒一个执行流,然后计数器+1)
接口:
sem_t sem : 信号量类型
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:信号量ID
pshared:这个参数决定了当前信号量适用于进程间还是线程间;0-线程间/非0-进程间
value:实际资源数量,用于初始化信号量计数器初值
int sem_wait(sem_t *sem);//阻塞等待
int sem_trywait(sem_t *sem);//尝试等待
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);//等待一段时间
int sem_post(sem_t *sem);//唤醒操作
int sem_destroy(sem_t *sem);//销毁
eg:生产者与消费者模型
#include<iostream>
#include<semaphore.h>
#include<vector>
#define MAX_QUEUE 5
class ringQueue
{
std::vector<int> m_array;
int m_capacity;
int m_pos_write;//写指针
int m_pos_read;//读指针
sem_t m_consumer_sem;//数据资源计数器,消费者计数器
sem_t m_product_sem;//空闲空间计数器,生产者计数器
sem_t m_sem_lock;//锁
public:
ringQueue(int capacity = MAX_QUEUE):
m_capacity(capacity),
m_pos_read(0),
m_pos_write(0),
m_array(MAX_QUEUE)
{
sem_init(&m_consumer_sem, 0, 0);//数据资源初始化
sem_init(&m_product_sem, 0, MAX_QUEUE);//空闲时间初始化
sem_init(&m_sem_lock, 0, 1);//锁的初始化
}
~ringQueue()
{
sem_destroy(&m_product_sem);
sem_destroy(&m_consumer_sem);
sem_destroy(&m_sem_lock);
}
bool push(int &data)
{
//没有空间空间则直接阻塞,并且空闲空间计数-1;
sem_wait(&m_product_sem);
sem_wait(&m_sem_lock);//加锁,保护入队操作
m_array[m_pos_write++] = data;
if(m_pos_write == m_capacity)
{
m_pos_write = 0;
}
sem_post(&m_sem_lock);//解锁,
sem_post(&m_consumer_sem);//资源计数器+1,唤醒消费者
return true;
}
bool pop(int *data)
{
//通过资源计数器判断是否否能获取资源,资源计数器-1
sem_wait(&m_consumer_sem);
sem_wait(&m_sem_lock);//加锁
*data = m_array[m_pos_read++];
if(m_pos_read == m_capacity)
{
m_pos_read = 0;
}
sem_post(&m_sem_lock);//解锁
sem_post(&m_product_sem);//空闲计数器+1,唤醒生产者
return true;
}
};
void *product(void* arg)
{
ringQueue *rq = (ringQueue*)arg;
int i = 0;
while(1)
{
rq->push(i);
std::cout << "productor: " << pthread_self() << "put data: " << i++ << std::endl;
}
return NULL;
}
void *consumer(void *arg)
{
int data;
ringQueue *rq = (ringQueue*)arg;
while(1)
{
rq->pop(&data);
std::cout << "consumer: " << pthread_self() << "get data: " << data << std::endl;
}
return NULL;
}
#define MAX_THR 4
int main()
{
int ret, i;
pthread_t ptid[MAX_THR], ctid[MAX_THR];
ringQueue rq;
for(i = 0; i < MAX_THR; i++)
{
ret = pthread_create(&ptid[i], NULL, product, (void*)&rq);
if(ret != 0)
{
std::cout << "thread create error" << std::endl;
return -1;
}
}
for(i = 0; i < MAX_THR; i++)
{
ret = pthread_create(&ctid[i], NULL, consumer, (void*)&rq);
if(ret != 0)
{
std::cout << "thread create error" << std::endl;
return -1;
}
}
for(i = 0; i < MAX_THR; i++)
{
pthread_join(ptid[i], NULL);
pthread_join(ctid[i], NULL);
}
return 0;
}
四:死锁
1:概念:多个执行流在对多个锁资源进行争抢操作,但是因为推进不当,而导致互相等待,流程无法继续推进的情况。
2:死锁产生的四个必要条件
1,互斥条件:一个锁只有一个人能加,我加了锁,别人就不能加了
2,不可剥夺条件:我加的锁,别人不能替我释放
3,请求与保持条件:我加了A锁,然后去请求B锁,但是请求不到B锁,我也不释放A锁
4,环路等待条件:eg:有甲乙两人,AB两锁;甲持有A锁,乙持有B锁,;甲请求B锁,乙请求A锁;在满足条件三的情况下,形成了环路等待
3:死锁的预防
即破坏产生死锁的四个必要条件
1:锁资源按序一次性分配
2:加锁的时候可以使用非阻塞加锁,若无法加锁,则将手中的其他锁释放掉
4:死锁的避免
1:银行家算法:定义三张表:现在有多少钱(现在都有那些锁);现在那些人已经借了钱(当前那些执行流已经获取了锁);当前还有那些人需要借多少钱(当前那些执行流想要那些锁);
若给一个执行流分配指定的锁有可能会造成环路等待(非安全状态),则不予分配,并且回溯释放当前执行流已有的资源。
2:死锁监测算法