目录
1. 线程概念
1.1 pid & tgid
pid:线程号(轻量级的进程号),内核当中是没有线程的概念的,称之为轻量级进程(lwp),线程的概念是c标准库当中的概念。
tgid:线程组id,对标的就是进程id。
主线程:pid = tgid,一个进程中有且只有一个主线程
工作线程:同一个线程组当中的tgid是相等的,标识的是同一个进程。pid是不同的,标识不同的线程。
1.2 线程标识符
线程标识符:线程独有空间的首地址
线程独有的空间包括:线程号、栈、errno、信号屏蔽字、一组寄存器、调度优先级
1.2 线程的优缺点
并行:多个执行流在同一时刻拿着不同的cpu进行运算,执行代码
并发:多个执行流在同一时刻有且只有一个执行流拥有cpu,进行运算,执行代码
一个进程当中线程数量越多越好吗?
如果一个进程当中的多个线程频繁的进行切换,则程序的运行效率有可能会降低,因为性能损失在了线程切换当中
进程的执行效率,一定是随着线程的数量增加,性能呈现正态分布的状况
一个线程的崩溃会引发整个进程的崩溃
多个线程在访问同一个变量的时候,可能导致程序结果的二义性
鲁棒性 == 健壮性
野指针 == 垂悬指针
1.3 多进程与多线程的区别
多进程指的是一个程序运行起来拥有多个进程。例如:守护进程,bash
多线程指的是一个进程当中有多个线程
2. 线程控制(线程创建,线程终止,线程等待,线程分离)
2.1 线程创建
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
pthread_t *thread:pthread_t(线程标识符的类型),出参
attr:线程属性(调度优先级、线程独有栈的大小),一般情况下传递NULL,采用默认属性
void *(*start_routine)(void *):线程的入口函数,线程执行代码的开始是从当前函数指针保存的函数开始运行
线程入口函数的返回值为void *,参数为void *
void *arg:给线程入口函数传递参数使用
返回值:0:创建成功
错误码:创建失败
线程标识符就是线程号的地址
top 命令能够实时显示系统资源各个进程占用状况,查看进程对cpu的使用率等,类似windows的任务管理器。
top -H -p [进程号]:显示进程中所有的线程对cpu的使用
-H: 设置线程模式
-p: 显示指定PID的进程
(1) 传递临时变量
以上代码还有一个问题就是内存的非法访问
这个 i 是个临时变量(栈上),出了 for 循环这个作用域就没了。上面的入口函数还一直访问这个 i
对于上面的问题的解决方法:在堆上开辟
(2) 传递结构体
(3) 传递类
main()函数是主线程函数,在main()函数中创建的线程属于子线程
主线程总是能优先获得cpu的执行权,且主线程执行完成后,程序就退出了,子线程就再也没有执行的机会,即主线程结束,子线程也会被迫结束。
(4) 传递this指针
this 是 C++ 中的一个关键字,也是一个 const 指针,它指向当前对象,通过它可以访问当前对象的所有成员。
this 只能用在类的内部,通过 this 可以访问类的所有成员,包括 private、protected、public 属性。
(5) 总结线程入口函数的参数传递
结论1:线程入口函数的参数不要传递临时变量
结论2:传递堆上开辟的空间,在线程结束之前由线程进行释放
结论3:线程入口函数的参数不仅仅可以传递内置类型,也可以传递自定义类型。自定义类型包括类的实例化对象、类的this指针、结构体指针
2.2 线程终止
线程的入口函数的return返回,当前线程就退出了
线程调用pthread_exit函数,谁调用谁退出
线程调用pthread_cancel函数,退出指定的线程,也可退出自己
void pthread_exit(void *retval)
retval:线程退出时返回的内容
void pthread_cancel(pthread_t thread);
退出thread线程
获取自己的线程标识符:pthread_t pthread_self(void);
谁调用返回谁的线程标识符
以上三种退出方式有一个通病就是:默认创建线程的时候,线程的属性是joinable属性。
joinable会导致线程在退出的时候,需要别人来回收自己的退出资源(换句话说,线程退出了,线程在共享区当中的空间还没有释放)。
2.3 线程等待:阻塞等待的方式
int pthread_join(pthread_t thread, void** retval);
thread:线程标识符,等待哪一个线程退出
void**:接收线程退出的退出信息
入口函数return返回:void*
pthread_exit返回:接收的是pthread_exit的参数
pthread_cancel:PTHREAD_CANCELED(常数)
只会打印工作线程的东西,没有打印主线程的东西。说明pthread_join在等待工作线程退出,而工作线程while(1)一直没有退出,所以阻塞等待
2.4 线程设置分离属性
一个线程如果被设置为分离属性,则该线程在退出之后,不需要其他执行流回收线程的资源,而是由操作系统进行回收。
int pthread_detach(pthread_t thread);
给thread线程设置分离属性
3. 线程安全
线程不安全的现象:互斥,同步
线程不安全的现象代码(抢票)
导致两个人抢到同一张票,这是不太可能的
线程切换
3.1 怎样描述线程不安全的现象(程序二义性)?
假设有一个cpu,两个线程,线程A和线程B,线程A和线程B都想要对全局变量i(i=100)进行减减操作;假设线程A先运行,但是线程A将i的值读到寄存器之后,就被线程切换了(线程A的程序计数器和上下文信息已经保留了下次将要执行的信息);线程A切换到线程B,线程B正常完减减操作,这时内存当中i的值被修改为99;当线程A被切换回来,因为线程A的程序计数器和上下文信息已经保留了下次将要执行的信息,所以也是从100-1得到99,保存到内存当中。所以,两个线程都执行了减减操作,内存结果应该是98,但是内存结果只体现了一次。
3.2 互斥锁
保证多个执行流在访问临界资源的时候,是原子操作的
临界资源:多个线程都能访问到的资源,就是临界资源。在同一时刻,只能有一个线程区访问临界资源
临界区:访问临界资源的代码区域被称之为临界区
原子操作:要么没有开始,要么结束了。非黑即白
互斥锁的原理:
互斥锁的底层是一个互斥量,互斥量的本质是计数器,计数器的取值只能为0或者1;
0:代表不能获取互斥锁
1:代表可以获取互斥锁
互斥锁初始化接口:
动态初始化,必须销毁互斥锁,否则造成内存泄露
int pthread_mutex_init(pthreat_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
pthread_mutex_t:互斥锁类型
mutex:传递一个互斥锁变量的地址给该参数
attr:一般传递NULLL,采用默认属性
销毁接口:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
静态初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
PTHREAD_MUTEX_INITIALIZER是一个宏,是typedef的一个结构体
加锁接口:
阻塞加锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);
非阻塞加锁接口:
int_pthread_mutex_trylock(pthread_mutex_t *mutex)
需要搭配循环来使用,并且判断返回值
带有超时时间的加锁接口:
int pthread_mutex_timelock(pthread_mutex_t *restrict mutex,const struct timespec *restrict abs_timeout);
需要搭配循环来使用,并且判断返回值
解锁:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
先创建线程还是先初始化互斥锁?
先初始化互斥锁,再创建线程
加锁的位置?
访问(读取、修改)临界资源之前,进行加锁
并不能确定哪个线程调用了互斥锁
需要gdb调试
让进程先运行起来再进行调试
调试运行时的进程: gdb attach [pid]
gdb -p [pid]
bt:查看主线程的调用堆栈
bt apply all bt:查看所有工作线程的调用堆栈
t [线程编号]:进入到某个线程,线程编号是指Thread 2 中的2,然后再bt 就进入到2号线程的调用堆栈
f [堆栈号]:进入到某个具体的堆栈
f 5:跳转到5号堆栈,但是不能调试,对于c标准库和thread库,因为它们是release版本,不能调试,只能进入到自己的堆栈,f 3:跳转到3号堆栈
p [锁名称]:可以打印锁,并显示互斥锁变量的成员函数,其中有一个_owern=线程号,说明该锁被该线程拿走了
解决:在break之前,把锁释放掉
结论:在所有可能导致线程退出的地方进行解锁
4 死锁
4.1 死锁的现象
现象1:如果执行流加锁完毕之后,不进行解锁,则会造成死锁现象
现象2:线程A获取了1锁,线程B获取了2锁,同时线程A还想获取2锁,线程B还想获取1锁。线程A就拿着1锁,阻塞在了等待获取2锁的代码上,线程B就拿着2锁,阻塞在了等待1锁的代码上,双方都造成阻塞,这就是死锁现象
4.2 死锁的必要条件
互斥条件:一个执行流获取了互斥锁之后,其他执行流不能再获取该锁
不可剥夺:A执行流拿着互斥锁,其他执行流不能释放
循环等待(环路等待):多个执行流各自拿着对方想要的锁,并且各个执行流还请求对方的锁
请求与保持:吃着碗里的,看着锅里的,一个线程保持拿着一个锁,还去请求另外一把锁
4.3 死锁的解决
破坏死锁的四个必要条件(循环等待和请求与保持)
加锁顺序一致(线程A和线程B都是先加1锁,再加2锁)
避免锁未释放的场景(线程退出之前一定要把锁释放)
资源一次性分配
5. 条件变量
条件变量:本质是一个PCB等待队列,该等待队列当中存在线程或者进程的PCB
初始化动态接口:
动态初始化:
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
pthread_cond_t:条件变量的类型
cond:接受一个“条件变量”定义的变量的地址
attr:条件变量的属性,一般传递NULL,采用默认属性
销毁:
int pthread_cond_destroy(pthread_cond_t *cond);
cond:接受一个“条件变量”定义的变量的地址
静态初始化:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
等待接口:
Int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
cond:条件变量
mutex:互斥锁
作用:谁调用该接口,就将谁放入到PCB等待队列当中
- 为什么需要互斥锁?
在pthread_cond_wait函数的内部,需要释放互斥锁。释放之后,其他线程就可以正常加锁操作了。
例子:吃面的人发现碗里没有面,则需要将自己放到PCB等待队列,调用了pthread_cond_wait函数之后,需要将拿到的互斥锁释放掉,做面的人就可以访问碗这个临界资源了,就可以做面。 - 该函数的内部实现逻辑是怎样的?
在pthread_cond_wait函数内部,先放到PCB等待队列当中,再释放互斥锁
唤醒接口:
int pthread_cond_signal(pthread_cond_t *cond);
作用:唤醒PCB等待队列当中至少一个线程
int pthread_cond_broadcast(pthread_cond_t *cond);
作用:唤醒PCB等待队列当中的所有线程
被唤醒之后:抢锁
抢到了:pthread_cond_wait函数就调用返回了
没抢到:执行流就阻塞在了pehread_cond_wait函数内部的抢锁逻辑当中,直到抢锁成功,函数才会放回。
pthread_cond_wait() 用于阻塞当前线程,等待别的线程使用pthread_cond_signal()或pthread_cond_broadcast来唤醒它。 pthread_cond_wait() 必须与pthread_mutex配套使用。pthread_cond_wait()函数一进入wait状态就会自动释放锁。
当其他线程通过pthread_cond_signal()或pthread_cond_broadcast,把该线程唤醒,使pthread_cond_wait()通过(返回)时,该线程又自动获得该mutex。
pthread_cond_signal函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。
对于只有一个吃面和一个做面的来说(两个线程),这个代码可以运行
这个代码有一个缺点:就是如果有多个吃面和多个做面的人(大于2个线程),可能会出错
现在有2个吃面的线程,2个做面的线程
出现的不是0 1 0 1的错误
解决:
但是还会出现一些问题:
6. 生产者与消费者模型
6.1 123规则
1个线程安全的队列:保证操作(访问,删除,添加)队列的元素是线程安全
互斥:互斥锁
同步:条件变量
2种角色的线程
消费者:从队列当中获取元素,进行处理
生产者:将数据放入到队列当中
3种关系:
消费者与消费者互斥
生产者与生产者互斥
消费者与生产者保证互斥+同步
6.2 生产者消费者模型代码
STL queues 是不是线程安全的? 不是
STL当中的容器都不是线程安全的
#include <stdio.h>
#include <unistd.h>
#include <iostream>
#include <queue>
#define THREAD_NUM 2
//线程安全的队列
class RingQueue
{
public:
RingQueue()
{
capacity_ = 10;
pthread_mutex_init(&lock_, NULL);
pthread_cond_init(&cons_, NULL);
pthread_cond_init(&prod_, NULL);
}
~RingQueue()
{
pthread_mutex_destroy(&lock_);
pthread_cond_destroy(&cons_);
pthread_cond_destroy(&prod_);
}
//生产者线程调用
void Push(int data)
{
pthread_mutex_lock(&lock_);
while(que_.size() >= capacity_)
{
pthread_cond_wait(&prod_, &lock_);
}
que_.push(data);
pthread_mutex_unlock(&lock_);
pthread_cond_signal(&cons_);
}
//消费者线程使用
void Pop(int* data)
{
pthread_mutex_lock(&lock_);
while(que_.empty())
{
pthread_cond_wait(&cons_, &lock_);
}
*data = que_.front();
que_.pop();
W> printf("i am consumer %p, i consume %d\n", pthread_self(), *data);
pthread_mutex_unlock(&lock_);
pthread_cond_signal(&prod_);
}
private:
std::queue<int> que_;
size_t capacity_;
pthread_mutex_t lock_;
pthread_cond_t cons_;
pthread_cond_t prod_;
};
void* ConsumeStart(void* arg)
{
RingQueue* rq = (RingQueue*)arg;
while(1)
{
int data;
rq->Pop(&data);
}
return NULL;
}
int g_data = 1;
pthread_mutex_t g_lock;
void* ProductStart(void* arg)
{
RingQueue* rq = (RingQueue*)arg;
while(1)
{
rq->Push(g_data);
//lock
pthread_mutex_lock(&g_lock);
g_data++;
//unlock
pthread_mutex_unlock(&g_lock);
sleep(1);
}
return NULL;
}
int main()
{
pthread_mutex_init(&g_lock, NULL);
RingQueue* rq = new RingQueue();
if(rq == NULL)
{
return 0;
}
pthread_t cons[THREAD_NUM], prod[THREAD_NUM], prod1[THREAD_NUM];
for(int i = 0; i < THREAD_NUM; i++)
{
int ret = pthread_create(&cons[i], NULL, ConsumeStart, (void*)rq);
if(ret < 0)
{
perror("pthread_create");
return 0;
}
ret = pthread_create(&prod[i], NULL, ProductStart, (void*)rq);
if(ret < 0)
{
perror("pthread_create");
return 0;
}
ret = pthread_create(&prod1[i], NULL, ProductStart, (void*)rq);
if(ret < 0)
{
perror("pthread_create");
return 0;
}
}
for(int i = 0; i < THREAD_NUM; i++)
{
pthread_join(cons[i], NULL);
pthread_join(prod[i], NULL);
pthread_join(prod1[i], NULL);
}
pthread_mutex_destroy(&g_lock);
return 0;
}
7. 信号量(互斥+同步)
信号量(POSIX标准的信号量)
本质:资源计数器 + PCB等待队列
当多个线程想要获取信号量的时候,都会对信号量当中的资源计数器进行减1操作
互斥:
初始化信号量的资源计数器为1,表示当前只有1个资源
意味着只有1个线程在同一时刻可以获取到信号量
同步:
资源计数器在初始化的时候,就不必刻意的是1了
资源计数器:
如果大于0:表示还有多少资源可以被使用
如果等于0:表示没有资源可以被使用
如果小于0:表示有多少线程在进行等待资源
#include <mutex>
#include <condition_variable>
using namespace std;
class Semaphore
{
public:
Semaphore(long count = 0) : count(count) {}
//V操作,唤醒
void signal()
{
unique_lock<mutex> unique(mt);
++count;
if (count <= 0)
cond.notify_one();
}
//P操作,阻塞
void wait()
{
unique_lock<mutex> unique(mt);
--count;
if (count < 0)
cond.wait(unique);
}
private:
mutex mt;
condition_variable cond;
long count;
};
初始化信号量:
Int sem_init(sem_t *sem, int pshared, unsigned int value);
sem_t:信号量的类型
sem:传入待要初始化的信号量
pthread:表示信号量到底是用于进程间还是线程间
0:线程间
非0:进程间
value:表示初始化的资源数量
销毁接口:
Int sem_destroy(sem_t *sem);
sem:传入待要初始化的信号量
等待接口:调用等待接口后,会对信号量当中的资源计数器进行减1操作
int sem_wait(sem_t *sem); //阻塞接口
sem:传入信号量
int sem trvwait(sem_t *sem); //非阻塞接口
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); //带有超时间的接口
发布接口:调用发布接口后,会对信号量当中的资源计数器进行加1操作
int sem_post(sem_t *sem);
信号量实现生产者消费者模型
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <vector>
#define THREAD_NUM 2
//循环读写
class RingQueue
{
public:
RingQueue()
:vec_(4)
{
capacity_ = 4;
sem_init(&lock_, 0, 1); //互斥初识化为1
sem_init(&read_, 0, 0); //读初识化为0
sem_init(&write_, 0, 4); //写初识化4个(大于等于0)
w_pos_ = 0;
r_pos_ = 0;
}
~RingQueue()
{
sem_destroy(&lock_);
sem_destroy(&read_);
sem_destroy(&write_);
}
void Push(int data)
{
sem_wait(&write_);
sem_wait(&lock_);
vec_[w_pos_] = data;
w_pos_ = (w_pos_ + 1) % capacity_;
sem_post(&lock_);
sem_post(&read_);
}
void Pop(int* data)
{
sem_wait(&read_);
sem_wait(&lock_);
*data = vec_[r_pos_];
r_pos_ = (r_pos_ + 1) % capacity_;
sem_post(&lock_);
sem_post(&write_);
}
private:
std::vector<int> vec_;
size_t capacity_;
// 互斥
sem_t lock_;
//同步
sem_t read_;
sem_t write_;
int w_pos_;
int r_pos_;
};
void* ReadStart(void* arg)
{
RingQueue* rq = (RingQueue*)arg;
while(1)
{
int data;
rq->Pop(&data);
W> printf("i am ReadStart %p, i consume %d\n", pthread_self(), data);
}
return NULL;
}
void* WriteStart(void* arg)
{
RingQueue* rq = (RingQueue*)arg;
int data = 1;
while(1)
{
rq->Push(data++);
sleep(1);
}
return NULL;
}
int main()
{
RingQueue* rq = new RingQueue();
if(rq == NULL)
{
return 0;
}
pthread_t re[THREAD_NUM], wr[THREAD_NUM];
for(int i = 0; i < THREAD_NUM; i++)
{
int ret = pthread_create(&re[i], NULL, ReadStart, (void*)rq);
if(ret < 0)
{
perror("pthread_create");
return 0;
}
ret = pthread_create(&wr[i], NULL, WriteStart, (void*)rq);
if(ret < 0)
{
perror("pthread_create");
return 0;
}
}
for(int i = 0; i < THREAD_NUM; i++)
{
pthread_join(re[i], NULL);
pthread_join(wr[i], NULL);
}
return 0;
}
8. 线程池
线程池:本质上是1个线程安全的队列 + 一大堆线程
定义类型:待要处理的数据 + 如何处理(处理方法)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <iostream>
#include <queue>
using namespace std;
#define THREAD_NUM 5
//定义线程安全队列的类型
typedef void (*HANDLER)(int);
class QueType
{
public:
QueType()
{
}
QueType(int data, HANDLER handler)
{
data_ = data;
handler_ = handler;
}
void DealData()
{
handler_(data_);
}
private:
//TODO data
int data_;
//deal function
HANDLER handler_;
};
class ThreadPool
{
public:
ThreadPool()
{
capacity_ = 10;
pthread_mutex_init(&que_lock_, NULL);
pthread_cond_init(&cons_cond_, NULL);
pthread_cond_init(&prod_cond_, NULL);
thread_num_ = THREAD_NUM;
is_quit_ = false;
}
~ThreadPool()
{
pthread_mutex_destroy(&que_lock_);
pthread_cond_destroy(&cons_cond_);
pthread_cond_destroy(&prod_cond_);
}
int Start()
{
//1.create thread
for(int i = 0; i < THREAD_NUM; i++)
{
int ret = pthread_create(&tid_[i], NULL, ThreadPollStart, (void*)this);
if(ret < 0)
{
perror("pthread_create");
thread_num_--;
continue;
}
}
return thread_num_;
}
static void* ThreadPollStart(void* arg)
{
pthread_detach(pthread_self());
ThreadPool* tp = (ThreadPool*)arg;
while(1)
{
pthread_mutex_lock(&tp->que_lock_);
while(tp->que_.empty())
{
if(tp->is_quit_)
{
//pos
tp->thread_num_--;
pthread_mutex_unlock(&tp->que_lock_);
pthread_exit(NULL);
}
pthread_cond_wait(&tp->cons_cond_, &tp->que_lock_);
}
QueType qt;
tp->Pop(&qt);
pthread_mutex_unlock(&tp->que_lock_);
pthread_cond_signal(&tp->prod_cond_);
qt.DealData();
}
return NULL;
}
void Push(QueType& qt)
{
pthread_mutex_lock(&que_lock_);
while(que_.size() >= capacity_)
{
pthread_cond_wait(&prod_cond_, &que_lock_);
}
que_.push(qt);
pthread_mutex_unlock(&que_lock_);
pthread_cond_signal(&cons_cond_);
}
void Pop(QueType* qt)
{
*qt = que_.front();
que_.pop();
}
void exitThreadPool()
{
is_quit_ = true;
while(thread_num_)
{
pthread_cond_signal(&cons_cond_);
}
}
private:
queue<QueType> que_;
size_t capacity_;
pthread_mutex_t que_lock_;
pthread_cond_t cons_cond_;
pthread_cond_t prod_cond_;
pthread_t tid_[THREAD_NUM];
int thread_num_;
bool is_quit_;
};
void DeadFunc(int data)
{
printf("data : %d\n", data);
}
int main()
{
ThreadPool* tp = new ThreadPool();
if(tp == NULL)
{
exit(1);
}
if(tp->Start() <= 0)
{
exit(2);
}
for(int i = 0; i < 100; i++)
{
QueType qt(i, DeadFunc);
tp->Push(qt);
}
tp->exitThreadPool();
delete tp;
return 0;
}
9. 总结
- 定义一个成员变量
1.1 初始化
1.2 要不要释放
1.3 正常业务逻辑
1.4 当前是否有线程安全问题存在
多个线程是否能够访问到
1.5 加锁保护 - 加锁考虑的问题
2.1 什么时候加锁
2.2 什么时候解锁
在临界区代码有可能导致线程退出的地方需要考虑是否需要解锁 - 编写代码的时候,动态开辟的内存,什么时候释放