线程是Linux最为重要的内容,熟练掌握线程是系统编程的最重要的一环。
目录
线程的概念
在传统操作系统中PCB是一个进程,但由于Linux下的线程是以进程的pcb模拟的,所以Linux下的pcb实际是一个个线程。因为这些pcb相较于传统的pcb更加轻量化,所以Linux下的线程也叫轻量化的进程。一个进程中至少有一个线程(进程产生一定会有程序要运行)所以进程中至少有一个PCB。CPU调度的是PCB,线程即是CPU调度的基本单位。而进程是一个线程组,是资源分配的基本单位。
线程的特点
由于进程的线程运行在同一个虚拟地址空间:共享进程代码段和数据段
- 共享的资源:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前⼯作目录
- ⽤户id和组id
- 独立的资源
- 独立的数据
- 独立的寄存器
- 独立的errno
- 独立的信号屏蔽字
- 线程id
多进程和多线程对比
- 线程的优点:源于(使用同一个虚拟地址空间)
- 线程的销毁和创建成本更低-----》进程创建页表等
- 线程间通信更加方便----》同一个进程
- 线程的调度成本低----》进程调度需要切换页表,线程只用切换pcb
- 线程的执行粒度更加细致-----》细致到处理函数
- 线程的缺点:
- 缺乏控制,因此编码需要注意的问题很多
- 稳定性低/健壮性差
线程的控制
-
线程创建
操作系统没有提供直接创建线程的系统调用接口,所以大佬们封装一套线程控制接口----封装了线程库供用户使用
我们创建的线程是一个用户态线程-----
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
thread:返回线程ID
attr:设置线程的属性,attr通常为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数,没有参数就给空就完事儿了
返回值:成功返回0;失败返回错误码pthread_t pthread_self(void);
返回调用线程的线程id
-
pid、tgid、tid
运行上述程序。在另一个终端上运行 ps -eLf |head -1 && ps -eLf |grep Create |grep -v grep(Create是我的执行程序名字)x下面只是显示了一部分。ps是显示进程信息
UID:启动这些进程的用户。
PID:进程的进程ID。
PPID:父进程的进程号
LWP:线程号,也可以说是轻量级进程号
C:进程生命周期中的CPU利用率。
NLWP:有多少个线程
STIME:进程启动时的系统时间。
TTY:进程启动时的终端设备。
TIME:运行进程需要的累计CPU时间。
CMD:启动的程序名称。
上图可以看到PID是相同的,说明只有一个进程,但是LWP有两个,说明是两个线程,NLWP是线程的个数。
现在来说说三个id的历史。
- pid
因为线程是进程模拟实现的,所以以后的pid就是线程id,当然可以理解位轻量级进程id,
- tgid
那进程的id就要用另一个名称,就是tgid。为什么是tgid,因为现在的进程是线程组,所以tgid就是thread group id。
- tid
在代码中我们无法获取这个LWP,但是有一个tid是可以获取的,它的类型是pthread_t ,tid是一个地址,但也唯一标识一个线程,所以也是线程id,以后我们在代码中通过这个tid来操作线程。后面对线程控制都是用的tid这个线程id。
- 接口
getpid()-----返回进程,也就是线程组的id,这个id是tgid
gettid()------返回线程id,这个id是pid。这个接口一般不使用
pthread_self(void)-----返回调用线程的id,这个id是tid
-
线程终止
void pthread_exit(void *value_ptr);
退出调用线程,并且返回一个value_ptr
exit(0); exit退出的是整个进程(所有线程都会退出)
这里有个注意的地方,就是return
在普通线程中return 是退出线程,在main中return直接退出进程
线程打印一次之后,就退出了。由于我不需要返回值去干嘛,所以返回值置空。
int pthread_cancel(pthread_t thread);
thread:线程ID
返回值:成功返回0;失败返回错误码
pthread_exit()--------退出调用线程
pthread_cancle()----取消指定线程
线程还是执行一次后退出,注意如果在线程中退出自己,那么程序会卡住,因为这是非法操作。
-
线程等待
等待指定线程退出,线程退出未处理也会形成僵尸进程。(前面没有等待,所以也是僵尸线程、线程是不会被显示,因为我们查看进程信息的时候主要显示的就是主线程的信息,所以这里能看到)
一个线程被创建,默认有一个属性joinable,处于这个属性的线程必须等待,因为该类线程退出后,不会自动回收资源,会造成内存泄漏
pthread_join(pthread_t thread, void **value_ptr);
thread:线程ID
value_ptr:它指向⼀个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
-
线程分离
为了解决上面的问题,这里我们将将线程的join able属性修改为detach属性,处于这个属性的线程,退出后将自动回收资源,但是不能被等待,否则报错,但是不会崩溃
int pthread_detach(pthread_t thread)
线程分离可以在线程任意时间分离,包括分离自己
可以直接在入口函数中直接分离。
线程安全
- 发生:多个执行流对数据的竞争操作,这是不安全的。对比前面的重入函数是一个道理。
- 解决------ 同步和互斥
- 加粗的接口是常用的
同步:对临界资源的时序可控;例:用户对缓冲区输入数据,CPU从缓冲区拿数据。有则拿,无则无操作。
互斥:对临界资源的唯一访问;例:缓冲区只允许有一个读或者写操作。
-
互斥的实现:互斥锁
互斥锁变量:pthread_mutex_t mutex
由于线程创建之后,谁先运行不一定,所以创建线程之前初始化。操作步骤如下:
- 1、初始化
静态初始化:pthread_mutex_t mutex = READ_MUTEX_INITIALIZER:直接初始化
动态初始化:int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
mutex:要初始化的互斥量
attr:通常NULL
- 2、加锁/解锁
加锁:int pthread_mutex_lock(pthread_mutex_t *mutex);能加则加,加不了,阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex);尝试枷锁,加不上,报错退出
解锁:int pthread_mutex_unlock(pthread_mutex_t *mutex);有加锁必解锁
返回值:成功返回0,失败返回错误号
- 3、销毁
锁的销毁:int pthread_mutex_destroy(pthread_mutex_t *mutex)
注意:
使⽤PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
不要销毁⼀个已经加锁的互斥量
已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁
产生死锁必要条件 :(必要条件)前提两个或两个以上的锁
- 互斥条件:有互斥才有互斥锁,我操作时别人不能操作
- 不可剥夺条件:我的锁别人不能解
- 请求与保持条件:拥有自己的锁,请求别的锁,未得到别的锁,不解自己的锁
- 环路等待条件:A锁,请求自己的锁,自己请求A锁
死锁的预防:破坏必要条件
死锁的避免:检测(银行家算法)
-
同步的实现:条件变量
条件变量:pthread_cond_t 条件满足则唤醒,条件不满足则等待
- 初始化:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
cond:要初始化的条件变量
attr:通常NULL
- 等待:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
cond:要在这个条件变量上等待
mutex:互斥量
这里必须说一下为什么要加互斥锁。因为这个wait的内部并不是原子操作,而是先进行了该线程互斥量的解锁,然后线程被挂起等待,当有其它线程唤醒这个线程的时候,他就被唤醒然后进行互斥量加锁。pthread_cond_wait(cv, mutex) 实际做的事情就是下面蓝色框内几个事情。后面我们对生产者消费者模型还原,就知道了。
- 唤醒:
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
- 销毁:
int pthread_cond_destroy(pthread_cond_t *cond)
-
生产者消费者模型
先来了解一下生产者消费者模型吧。
#include<iostream>
#include<queue>
#include<pthread.h>
using namespace std;
class BolckQueue
{
public:
//构造函数
BolckQueue()
:capacity_(10)
{
pthread_mutex_init(&mutex_,NULL);
pthread_cond_init(&product_,NULL);
pthread_cond_init(&customer_,NULL);
}
//析构函数
~BolckQueue()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&product_);
pthread_cond_destroy(&customer_);
}
//队列的接口
void Push(int val)
{
QueueBlock();//先对队列进行加锁
while(QueueIsFull())//如果队列是满的就让生产者等待
{
ProductWait();
//现在就来说说这个等待
//如果队列是满的,生产者线程就对临界资源解锁,让消费者取临界资源取数据
//等临界资源有空间了,消费者线程对生产者线程进行唤醒
//生产者线程就对临界资源加锁,上面就是wait操作,然后跳出循环,开始递送数据
}
queue_.push(val);
QueueUnblock();//队列解锁
CustomerWake();//对消费者唤醒
}
int Pop()
{
QueueBlock();//队列加锁
while(QueueIsEmpty())
{
CustomerWait();//如果队列为空,消费者等待
}
int val = queue_.front();
queue_.pop();
QueueUnblock();
ProductWake();//消费者拿出后,唤醒生产者
return val;
}
//成员私有接口
private:
void QueueBlock()
{
pthread_mutex_lock(&mutex_);
}
void QueueUnblock()
{
pthread_mutex_unlock(&mutex_);
}
bool QueueIsFull()
{
return queue_.size() == capacity_;
}
bool QueueIsEmpty()
{
return queue_.empty();
}
void ProductWait()
{
pthread_cond_wait(&product_,&mutex_);
}
void ProductWake()
{
pthread_cond_signal(&product_);
}
void CustomerWait()
{
pthread_cond_wait(&customer_,&mutex_);
}
void CustomerWake()
{
pthread_cond_signal(&customer_);
}
//队列成员变量
queue<int> queue_;
int capacity_;//队列的容量
pthread_cond_t product_;//生产者条件变量
pthread_cond_t customer_;//消费者条件变量
pthread_mutex_t mutex_;//互斥量
};
void *customer(void* arg)
{
BolckQueue* c_q = (BolckQueue*)arg;
while(1)
{
int data;
data = c_q->Pop();
cout << "取出的数据是:" << data << endl;
}
return NULL;
}
void *productor(void* arg)
{
BolckQueue* p_q = (BolckQueue*)arg;
int data = 0;
while(1)
{
p_q->Push(data++);
cout << "输入数据:" << data << endl;
}
return NULL;
}
#define NUM 4
int main()
{
int time = 0;
BolckQueue q_test;
pthread_t c_tid[NUM];
pthread_t p_tid[NUM];
while(time < NUM)
{
if(pthread_create(&p_tid[time],NULL,productor,(void*)&q_test) != 0 )
{
cout << "create error" << endl;
return -1;
}
++time;
}
time = 0;
while(time < NUM)
{
if(pthread_create(&c_tid[time],NULL,customer,(void*)&q_test) != 0 )
{
cout << "create error" << endl;
return -1;
}
++time;
}
time = 0;
while(time < NUM)
{
pthread_join(c_tid[time],NULL);//每个线程我们都需要等待,防止僵尸线程
++time;
}
time = 0;
while(time < NUM)
{
pthread_join(p_tid[time],NULL);
++time;
}
return 0;
}
-
posix标准信号量
- 概念
信号量的本质是具有等待队列的计数器,主要用于是按进程/线程的同步,当然也可以实现互斥(互斥时信号量只能是0和1)
- 信号量原理
- 初始一个资源计数
- 当获取资源时,先判断资源数
- 若 >0 ,表示有资源,计数 -1,直接返回,获取资源
- 若<=0,表四没有资源,则阻塞等待
- 当资源使用完毕,计数+1,唤醒等待队列上的进程/线程
- 接口介绍
初始化:int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:信号量
pshared: 0表⽰示线程间共享,非零表⽰示进程间共享
value:信号量初始值
销毁信号量:int sem_destroy(sem_t *sem);
sem:信号量
等待信号量: int sem_wait(sem_t *sem);
会将信号量的值减1,若小于0则等待,大于0就使用。
唤醒信号量:int sem_post(sem_t *sem);
表⽰示资源使⽤用完毕。将信号量值加1。
-
信号量实现生产者消费者
下面根据信号量的同步和互斥功能来实现生产者和消费者模型
#include<iostream>
#include<vector>
#include<pthread.h>
#include<unistd.h>
#include<semaphore.h>
using namespace std;
class RingQueue
{
public:
//构造函数
RingQueue()
:capacity_(10)
,queue_(10)
,cus_step_(0)
,pro_step_(0)
{
sem_init(&free_space_,0,capacity_);
sem_init(&data_space_,0,0);
sem_init(&mutex_,0,1);//初始化1,1表示有资源,0表示没有资源。就实现了互斥
}
~RingQueue()
{
sem_destroy(&free_space_);
sem_destroy(&data_space_);
sem_destroy(&mutex_);
}
void Push(int val)
{
ProductorWait();//是否有空间进行生产
QueueBlock();//队列加锁
queue_[pro_step_] = val;//添加数据
pro_step_ = (pro_step_ + 1) % capacity_;
QueueUnblock();
CustomerWake();
}
int Pop()
{
CustomerWait();
QueueBlock();//队列加锁
int data = queue_[cus_step_];
cus_step_ = (cus_step_ + 1) %capacity_;
QueueUnblock();
ProductorWake();
return data;
}
private:
//私有成员接口
void QueueBlock()
{
//pthread_mutex_lock(&mutex_);
sem_wait(&mutex_);
}
void QueueUnblock()
{
//pthread_mutex_unlock(&mutex_);
sem_post(&mutex_);
}
void CustomerWait()
{
sem_wait(&data_space_);
}
void CustomerWake()
{
sem_post(&data_space_);
}
void ProductorWait()
{
sem_wait(&free_space_);
}
void ProductorWake()
{
sem_post(&free_space_);
}
//成员变量
vector<int> queue_;
int capacity_;
int cus_step_;
int pro_step_;
sem_t mutex_;
sem_t free_space_;
sem_t data_space_;
};
void* customer(void* arg)
{
RingQueue* c_q = (RingQueue*)arg;
while(1)
{
sleep(1);
int data = c_q->Pop();
cout << "取出的数据是:" << data << endl;
}
return NULL;
}
void* productor(void* arg)
{
RingQueue* p_q = (RingQueue*)arg;
int data = 0;
while(1)
{
sleep(1);
p_q->Push(data++);
cout << "+++++输入的数据是:" << data << endl;
}
return NULL;
}
#define NUM 5
int main()
{
int time = 0;
RingQueue q_test;
pthread_t c_tid[NUM];
pthread_t p_tid[NUM];
while(time < NUM)
{
if(pthread_create(&c_tid[time],NULL,customer,(void*)&q_test) != 0)
{
cout << "create error" << endl;
}
++time;
}
time = 0;
while(time < NUM)
{
if(pthread_create(&p_tid[time],NULL,productor,(void*)&q_test) != 0)
{
cout << "create error" << endl;
}
++time;
}
time = 0;
while(time < NUM)
{
pthread_join(c_tid[time],NULL);
++time;
}
time = 0;
while(time < NUM)
{
pthread_join(p_tid[time],NULL);
++time;
}
return 0;
}
线程池
- 概念
线程池就是一堆线程(一定数量),这里会有一个任务队列,当有任务的时候,会有线程去给任务队列添加任务,处理任务时,线程从任务队列取任务。简单理解就是一堆线程,一个生产者消费者模型。
- 作用
在双十一的时候,淘宝一瞬间会有很多的请求链接,分分钟百万上下,这个时候如果服务器直接创建一百万个线程,服务器可能就凉了。
- 避免大量线程被创建和销毁
- 避免显示一时间创建太多,资源耗尽,程序崩溃
- 解耦合
- 支持忙闲不均,支持并发
- 实现
- 最大数量限制----线程数量
- 当前线程池的线程数量
- 线程安全的任务队列
- 消费者生产者
#include<iostream>
#include<queue>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<semaphore.h>
using namespace std;
typedef bool (*mysolution)(int data);//定义一个函数指针,
//返回值是布尔,参数类型是T
class Task
{
public:
Task()
{}
void SetTask(int& data,mysolution func)
{
data_ = data;
func_ = func;
}
~Task()
{}
//处理函数
bool Run()
{
func_(data_);
return true;
}
private:
int data_;//要处理的数据
mysolution func_;//要处理的函数
};
#define THRMAX 5 //线程的最大数,队列容量也进行10个容量
class Threadpool
{
public:
//队成员进行初始化
Threadpool(int thrMax = THRMAX)
:thrMax_(5)
,thrcur_(5)
,capacity_(10)
{
quiet_flag = false;
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond_con,NULL);
pthread_cond_init(&cond_pro,NULL);
}
~Threadpool()
{
pthread_cond_destroy(&cond_con);
pthread_cond_destroy(&cond_pro);
pthread_mutex_destroy(&mutex);
}
//线程入口函数,传入的是线程池类的对象地址
static void* ThrStart(void* arg)//这里之所以静态,是由于类成员函数有一个this指针
{ //而线程创建里的函数只能有一个参数
Threadpool* tp = (Threadpool*)arg;//获取对象
while(1)
{
tp->QueueBlock();//对任务队列进行加锁
while(tp->QueueIsEmpty())
{
tp->CustomerWait();//如果队列没有任务,消费者线程进行等待
}
Task task;
tp->Pop(task);
tp->ProductorWake();//唤醒生产者线程
tp->QueueUnblock();//任务队列进行解锁
task.Run();//处理数据,这个处理必须在解锁之后,不然一直处理数据,
//别的线程无法再获取数据
}
return NULL;
}
//线程初始化,线程初始化不应该放在构造里
//1、启动太慢
//2、未知错误
bool ThreadInit()
{
pthread_t tid;
for(int i = 0; i < 5; ++i)
{//这里不需要数组,因为在后面不会用到tid所以,后面分离之后就不需要了
int res = pthread_create(&tid,NULL,ThrStart,(void*)this);
if(res != 0)
{
cout << "CREATE ERROR" << endl;
return false;
}
pthread_detach(tid);//分离之后不用等待
}
return true;
}
//向任务队列添加数据
bool PushTask(Task& task)
{
QueueBlock();//对任务队列进行加锁
if(quiet_flag == true)
{
QueueUnblock();
return false;
}
while(QueueIsFull())
{
ProductorWait();//生产者线程判断队列是否满了
}
Push(task);//添加任务
CustomerWake();
QueueUnblock();
return true;
}
void ThreadQuiet()
{
QueueBlock();
quiet_flag = true;
QueueUnblock();
while(thrcur_ > 0)
{
CustomerWakeAll();
usleep(1000);
}
}
//成员接口
private:
void QueueBlock()
{
pthread_mutex_lock(&mutex);
}
void QueueUnblock()
{
pthread_mutex_unlock(&mutex);
}
void CustomerWait()
{
if(quiet_flag == true)
{
--thrcur_;
pthread_mutex_unlock(&mutex);
cout << pthread_self() << "已经退出"<<endl;
pthread_exit(NULL);
}
pthread_cond_wait(&cond_con,&mutex);
}
void CustomerWake()
{
pthread_cond_signal(&cond_con);
}
void CustomerWakeAll()
{
pthread_cond_broadcast(&cond_con);
}
void ProductorWait()
{
pthread_cond_wait(&cond_pro,&mutex);
}
void ProductorWake()
{
pthread_cond_signal(&cond_pro);
}
bool QueueIsEmpty()
{
return queue_.empty();
}
bool QueueIsFull()
{
return (queue_.size() == capacity_);
}
void Push(Task& task)
{
queue_.push(task);
}
void Pop(Task& task)
{
task = queue_.front();
queue_.pop();
}
//成员变量,用信号量来实现线程安全
private:
int thrMax_;//最大线程数量
int thrcur_;//当前线程数量
int capacity_;//队列容量
queue<Task> queue_;//任务队列
bool quiet_flag;//线程退出的标志
pthread_mutex_t mutex;//互斥量
pthread_cond_t cond_con;//信号量实现同步
pthread_cond_t cond_pro;
};
bool TaskHandler(int data)
{
cout << "处理线程是:" << pthread_self() << endl;
cout << "处理数据是:" << data << endl;
sleep(1);
return true;
}
#define TASKNUM 10
int main()
{
Threadpool testpool;//创建一个线程池对象
testpool.ThreadInit();//线程池初始化,10个线程,一个安全的任务队列
Task task[TASKNUM];//创建10个任务对象
for(int i = 0; i < TASKNUM; ++i)
{
task[i].SetTask(i,TaskHandler);
testpool.PushTask(task[i]);
}
testpool.ThreadQuiet();
return 0;
}