目录
[1. 了解线程概念,理解线程与进程区别与联系。](#1. 了解线程概念,理解线程与进程区别与联系。)
[2. 学会线程控制,线程创建,线程终止,线程等待。](#2. 学会线程控制,线程创建,线程终止,线程等待。)
[3. 了解线程分离与线程安全概念。](#3. 了解线程分离与线程安全概念。)
[2 Linux线程互斥](#2 Linux线程互斥)
[5生产者消费者模型(CP问题 ---consumer producter)](#5生产者消费者模型(CP问题 ---consumer producter))
[4. 学会使用互斥量,条件变量,posix信号量,以及读写锁。](#4. 学会使用互斥量,条件变量,posix信号量,以及读写锁。)
[1 信号量的初始化](#1 信号量的初始化)
[2 信号量的销毁编辑](#2 信号量的销毁编辑)
[3 信号量的P操作编辑](#3 信号量的P操作编辑)
[4 信号量的V操作编辑](#4 信号量的V操作编辑)
1. 了解线程概念,理解线程与进程区别与联系。
线程是一种特殊的进程
线程:线程是进程内的一个执行分支,线程的执行粒度要比进程更细
其实线程也是一个子进程,当然他也有自己的task_struct,但是他没有自己独立的地址空间和页表,他和父进程共用一个地址空间和页表,正文代码部分分一部分给这个进程,这个进程被称为线程
1 重新定义线程和进程
什么叫做线程?
线程是操作系统调度的基本单位
重新理解进程?
内核观点:进程是承担分配系统资源的实体(进程=内核数据结构task_struct+代码和数据--->(这是我们之前的理解))
所以进程是包含线程的,线程是我们进程内部的执行流资源
如何理解以前的进程?
操作系统以进程位单位,给我们分配资源,我们当前进程内部只有一个执行流(执行流其实也是资源)
对于这个线程,按照我们之前的理解,要先描述再组织,要创建他的进程控制块(struct tcb //struct ctrl block),Windows是这样做的,但是Linux中,服用了进程数据结构和管理算法,所以就用struct task_struct 去模拟线程
所以Linux没有真正意义上的线程(没有自己的进程控制块),而是用"进程"模拟的线程
所以linux操作系统是比较简洁的,他的稳定性比Windows强的不是一点
cpu执行的时候部分进程和线程,这个进程和线程的概念是操作系统层面上的,Linux中的执行流称为轻量级进程
可以把进程比作--家庭,把线程比作--家庭成员
下面这张图就是cpu中得到的虚拟地址如何转化为物理地址(读进cpu内部的地址是虚拟地址)32位机器的地址大小为4字节,32个比特位,cpu将这32个比特位分为10-10-12 三个部分,
然后通过一级页表和二级页表,二级页表存储的就是物理内存的地址,物理内存是分页的,每一页大小一般都是4kb,2^12大小就是4096,正好就可以进行偏移,能够访问每一页的任何地方
起始地址+类型 = 起始地址+偏移量 (x86的特点)
线程分配资源的本质,就是分配地址空间范围
2 线程为什么比进程更轻量化?
a,创建和释放更加轻量化(生死)
b,切换更加轻量化(运行)
线程切换的时候,cpu中缓存的catch数据不会改变(因为线程公用这些catch),而进程之间的切换时肯定要加载新的catch数据(即线程切换不需要热缓存)
因为内核当中没有线程的概念,所以不会给我们提供线程的系统调用,只会给我们提供轻量级进程的系统调用
但是我们用户需要线程的接口0---->pthread线程库(应用层) 这是轻量级进程接口进行封装,为用户提供直接线程的接口
几乎所有的linux平台,都是自带这个库的
Linux中编写线程代码需要使用第三方pthread库
3 线程的优缺点
4 进程VS线程的线程
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
线程ID
一组寄存器
栈
errno
信号屏蔽字
调度优先级
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中 都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id
进程和线程的关系如下图:
2. 学会线程控制,线程创建,线程终止,线程等待。
1 线程创建
因为要链接库,所以编译时要用 -lpthread 这个选项就可以了
ldd +你生成的可执行程序 ,就可以看到这个可执行程序链接的动态库
ps -aL : 显示所有轻量级进程(线程)
LWD: light weight process 轻量级进程的ID,当一个轻量级进程的PID == LWD ,那这个线程就是主线程,所以线程之间,PID相同,LWD不同
注意:任何一个线程被kill -9 掉之后,所有相关的线程都挂,因为这个信号是发给进程的,所有的线程都归属于进程的一部分
这个传递的东西也可以传递对象,返回也可以返回对象(都是用对象的指针),接收过来的参数式void* 将这个指针强转为对象的指针就可以了
功能:创建一个新的线程 原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void*), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数(返回值void* ,参数为void* args)
arg:传给线程启动函数的参数
返回值:
成功返回0;失败返回错误码
指针的大小时跟平台有关的,linux下,大小是8字节
2 线程等待
当然线程也有回收机制,主线程也要对创建的线程去等待
这个等待是阻塞的
这个二级指针式输出型参数,通过这个来获得进程执行完成函数的返回值(类型式void*)
为啥要用这个二级指针?
只有这个函数里面可以访问到函数退出的时候的返回值,通过你传入相应的类型指针void**,就可以把这个返回值带出来
3 线程终止
而且在线程执行函数当中不能用exit()终止线程,这个是用来终止进程的
通过pthread_exit()可以终止线程
主线程也可以去提前终止目标线程
C++ 语言本身也支持多线程,引入<thread>就可以使用了(在linux环境下,g++编译的时候仍然要加 -lpthread,重这个就可以看出来,c++11里面的多线程就是封装原生线程库)
我们刚刚讲的是原生的线程库
4 获取线程id
获取当前线程自己的tid
5 线程ID及进程地址空间布局
pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是 一回事。
前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要 一个数值来唯一表示该线程。
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于 NPTL线程库(原生线程库)的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
线程库是加载到内存的(动态库)
每一个线程的库级别的tcb(线程控制块,用数组组织起来)的起始地址,叫做线程的tid(就是地址空间的一块地址,也是我们创建线程获得的)
这个就是线程的tcb线程控制块,包含很多属性
所以除了主线程,所有的其他线程的独立栈,都在共享区,具体来讲是在pthread库中,tid指向的是用户的tcb中!
注意:这个栈结构式必须的,是(独立)调用的关键
Linux中的线程是用户级线程,内核级线程是在内核中实现线程的概念(tcb)
每一个线程都有自己的栈结构,线程线程之间,没有密码,线程栈上面的数据也是可以被其他线程访问的
全局变量是被所有的线程同时看到并访问,如果线程想要一个私有的全局变量呢?
把全局变量定义前面加一个__thread ,每一个线程访问的这个全局变量就会每个线程各自私有一份,但是他们的值是同步的(一样的,哪个线程改变这个变量,所有线程各自私有的变量都会变化)
而且这个__thread只能用于定义内置类型,不能用来修饰自定义结构体
这个存储方法叫做线程局部存储
3. 了解线程分离与线程安全概念。
1分离线程
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资 源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程 资源。
2 Linux线程互斥
共享数据的数据不一致问题是由多线程并发访问造成的
1 创建互斥锁
因为修改一个变量的步骤 : tickets--
1 先将变量tickets读取到cpu中的寄存器中
2 cpu内部进行--操作
3 将计算结果写回到内存中
解决方法---对共享数据的任何访问,保证任何时候只有一个执行流访问!---->互斥
通过加锁!mutex(互斥锁)
2 使用互斥锁
加锁的本质:以时间换取安全
加锁的表现:线程对于临界区的代码时串行执行
加锁的原则:尽量要保证临界区代码,越少越好
如果定义是全局的或者是静态的锁,那么就可以不用初始化,只要附一个初始值就可以了
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); 参数:
cond:要初始化的条件变量
attr:NULL
而且线程对于锁的竞争能力可能不同(高频调度的时候)
纯互斥环境,如果锁的分配不够合理,容易导致其他线程的饥饿问题(这个问题真一定存在的)
让所有的线程获取锁,按照一定的顺序 ----按照一定的顺序获取资源--->同步
有一个问题-->锁本身就是共享资源
所以,申请锁和释放锁本身就被设计成原子性操作
设计成原子的方法:通过把锁这个资源交换到线程的上下文中,因为线程的栈是私有的,所以就变成了原子的了
3可重入VS线程安全
概念
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们 称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重 入函数,否则,是不可重入函数。
死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资 源而处于的一种永久等待状态。
简单来说就是有两个锁,一个进程申请不释放自己申请过的锁而去申请另外一个进程持有锁,另外一个进程也是同样的样子,这就造成了死锁
条件(必要)
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
解决死锁问题:理念就是破坏四个必要条件就可以了
同步问题就是保证数据安全的情况下,让我们的线程访问资源具有一定大的顺序性
4条件变量
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情 况就需要用到条件变量。
1条件变量函数 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL
条件变量的使用必须依赖锁
2条件变量的使用
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待(其实就是一个队列,线程在上面排队哦)
mutex:互斥量,后面详细解释
1调用的时候,自动释放锁
2 因为唤醒二返回的时候,重新持有锁
3唤醒条件变量下等待的线程
唤醒一个线程
唤醒所有线程
注意:判断临界资源的时候,要把判断放在加锁之后
因为判断资源是否是就绪状态也是访问临界资源,所以这样
5生产者消费者模型(CP问题 ---consumer producter)
生产者-消费者问题 (也称有限缓冲问题 ,Bounded Buffer Problem)是操作系统与并发编程中的一个经典同步问题,用于描述多个线程或进程如何安全、高效地共享一个固定大小的缓冲区。
那么生产者和消费者就有并发访问问题
生产者vs生产者:互斥
生产者vs消费者:互斥(为了安全),同步
消费者vs消费者:互斥
321原则
三种关系
两种角色:生产者和消费者
一个交易场所:特定结构的内存空间
BlockingQueue 在多线程编程中阻塞队列(Blocking Queue)是一种常 用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会 被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取 出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
生产和消费模型也是高效的(可以并发访问数据)
生产者:1 获取数据 2 生产数据到队列
消费者: 1 消费数据 2 加工处理数据
1生产消费模型代码示例
BlockQueue.hpp
cpp
#pragma once
#include <iostream>
using namespace std;
#include <queue>
#include <pthread.h>
template <class T>
class BlockQueue
{
static const int defaultnum = 20;
public:
BlockQueue(int maxcap = defaultnum)
: maxcap_(maxcap)
{
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&c_cond_, nullptr);
pthread_cond_init(&p_cond_, nullptr);
}
T pop()
{
pthread_mutex_lock(&mutex_);
while (q_.size() == 0)
{
pthread_cond_wait(&c_cond_, &mutex_); // 没有数据的时候就等待
}
T out = q_.front();
q_.pop();
pthread_cond_signal(&p_cond_);
pthread_mutex_unlock(&mutex_);
return out;
}
void push(const T &in)
{
pthread_mutex_lock(&mutex_);
while (q_.size() == maxcap_)
{
pthread_cond_wait(&p_cond_, &mutex_); // 没有数据的时候就等待
}
q_.push(in);
pthread_cond_signal(&c_cond_);
pthread_mutex_unlock(&mutex_);
}
~BlockQueue()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&c_cond_);
pthread_cond_destroy(&p_cond_);
}
private:
queue<T> q_;
int maxcap_; // 队列的极值
pthread_mutex_t mutex_;
pthread_cond_t c_cond_; // 消费和生产用不同的条件变量
pthread_cond_t p_cond_;
};
cpp
#include "BlockQueue.hpp"
#include <unistd.h>
void *Consumer(void *args)
{
BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
while (true)
{
// 消费
int t = bq->pop();
cout << "消费者获得: " << t << endl;
}
}
void *Productor(void *args)
{
BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
int i = 0;
while (true)
{
bq->push(i);
cout << "生产者生产: " << i << endl;
i++;
sleep(1);
}
}
int main()
{
// BlockQueue 内部可以传递任务和其他东西
BlockQueue<int> *bd = new BlockQueue<int>();
pthread_t p, s;
pthread_create(&p, nullptr, Productor, bd);
pthread_create(&s, nullptr, Consumer, bd);
pthread_join(p, nullptr);
pthread_join(s, nullptr);
delete bd;
return 0;
}
4. 学会使用互斥量,条件变量,posix信号量,以及读写锁。
临界资源可以分为多份,份数可以通过信号量来控制,多份之间互不干扰
信号量的本质是一把计数器,用来描述资源数目的,把资源是否就绪放在了临界区外
基于环形队列的生产消费模型
在程序中用的是数组来模拟环形队列
生产者与消费者之间的互斥和并发运行由信号量来维持了
消费者与消费者,生产者与生产者之间是互斥的关系,那么这两个之间就要用互斥锁来保持互斥关系,即用两把锁即可
1 信号量的初始化
2 信号量的销毁
3 信号量的P操作
获取信号量(信号量减少)
4 信号量的V操作
释放信号量(信号量增加)
5线程池
池化技术:以空间换时间
线程池其实就是一个生产消费者模型,主线程发布任务,这个线程池自动去调用线程去执行任务
线程池代码
ThreadPool.hpp
cpp
#pragma once
#include <iostream>
using namespace std;
#include <vector>
#include <queue>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include <cstdbool>
struct ThreadInfo
{
pthread_t tid;
string name;
};
static const int defaultnum = 5;
template <class T>
class ThreadPool
{
public:
void Lock()
{
pthread_mutex_lock(&mutex_);
}
void Unlock()
{
pthread_mutex_unlock(&mutex_);
}
void Wakeup()
{
pthread_cond_signal(&cond_);
}
void ThreadSleep()
{
pthread_cond_wait(&cond_, &mutex_);
}
bool IsQueueEmpty()
{
return tasks_.empty();
}
T Pop()
{
T t = tasks_.front();
tasks_.pop();
return t;
}
public:
ThreadPool(int num = defaultnum)
: threads_(num)
{
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
static void *HanderTask(void *args) // 静态成员函数没有this指针,就可以对应上了
{
ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);
while (true)
{
tp->Lock();
if (tp->IsQueueEmpty())
{
tp->ThreadSleep();
}
T t = tp->Pop();
tp->Unlock();
t();
cout << "result: " << t.GetResult() << endl;
}
}
void Start()
{
int num = threads_.size();
for (int i = 0; i < num; i++)
{
threads_[i].name = "threads-" + to_string(i);
pthread_create(&(threads_[i].tid), nullptr, HanderTask, this);
}
}
void Push(const T &t)
{
Lock();
tasks_.push(t);
Wakeup();
Unlock();
}
~ThreadPool()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
private:
vector<ThreadInfo> threads_;
queue<T> tasks_;
pthread_mutex_t mutex_;
pthread_cond_t cond_;
};













这个就是线程的tcb线程控制块,包含很多属性











