本篇是参考B站UP爱编程的大丙的视频和笔记的学习笔记。
进程是系统分配资源和调度的基本单位,操作系统为不同进程分配相互独立的内存空间。属于同一进程的所有线程共用同一地址空间。不同进程不同任意访问其他进程的内存和资源。
线程是比进程更小的基本单位,它能减少程序在并发执行时所付出的时空开销,提高程序并发执行的程度,使OS具有更好的并发性。
进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。 线程是作为调度和分派的基本单位。
这段总结复制自CSDN余百里的博客。
主要从五方面对线程与进程进行比较:
1.调度的基本单位
进程是作为进行资源分配和调度的基本单位,独立单位。实际运行中,每次调度进程都要进行上下文切换,开销加大。 线程是作为调度和分派的基本单位,也是能够独立运行的。当线程切换时,仅需要保存和设置少量寄存器的内容,开销远低于进程。 同一进程中,线程的切换不会引起进程的切换;从一个进程的线程切换到另一个进程的线程是,必定会引起进程的切换。
2.并发性
进程之间可以并发执行,而且在一个进程中的多个线程间也可以并发执行,设置还允许在一个进程中的所有线程都能并发执行。 不同进程中的线程也能并发执行。 如此,是的OS具有更好的并发性,能够更加有效地提高系统资源的利用率和系统的吞吐量。
3.拥有资源
进程可以拥有资源,并作为系统中拥有资源的一个基本单位。 线程本身并不拥有系统资源,仅有一点点必不可少、能保证独立运行的资源。 线程除了拥有自己的少量资源外,还允许多个线程共享该进程所拥有的资源。表现为:属于同一进程的所有线程都具有相同的地址空间。这些线程还可以访问进程所拥有的资源,如已打开的文件、定时器、信号量机构等内存空间和它申请到的I/O设备。
4.独立性
同一进程中的不同线程之间的独立性比不同进程之间的独立性低。 原因:为防止进程之间彼此干扰和破坏,每个进程都拥有一个独立的地址空间和其他资源,除了共享全局变量外,不允许其他进程的访问。而同一进程中的不同线程往往是为了提高并发性以及进行相互间合作创建的,他们共享进程的内存地址空间和资源。
5.系统开销
创建或撤销进程时,系统都要为之分配和回收进程控制块,分配或回收其他资源,为此付出的开销明显大于线程创建或撤销时所付出的开销。 进程切换时,涉及到进程上下文的切换,而线程的切换代价也远低于进程切换代价。
在处理多任务程序的时候使用多线程比使用多进程更有优势。
举一个浏览器的例子:一个采用了线程思想设计的浏览器。现在用这个浏览器打清华大学的百度百科的主页,可以看到在浏览器上首先出现的是网页中的文字部分,过了一段时间一些小的图片被显示出来,而那些更大的图片和动画则需要再过一段时间才能被渲染出来。这样实现的好处是用户很快就能看到一些文字信息,然后再是图片等信息,而不是等待较长一段时间以后看到网页的全部信息,显然这种逐步显示的方式其用户交互性要友好得多。
在这个浏览器实现的过程中,共启动了四个线程,分别是获取数据的线程、显示文本的线程、解压图片的线程和渲染图片的线程。
如果没有线程,即只有一个进程来完成上述工作,效果又会是什么样子?如果只有一个进程,执行的代码必然是首先将页面布局、文本信息、图片对象等内容全部下载下来,然后再逐个解码渲染,最后再将所有的信息全部整理好输出到屏幕上,结果就是用户面对空白屏幕等待了较长一段时间,然后所有网页信息全部被显示出来。
笔记参考:
线程同步 | 爱编程的大丙 (subingwen.cn)
读写锁在linux下有三种状态:读锁、写锁、不加锁。写独占,读共享,写锁的优先级高。
使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的。
使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的。
读写锁是一把锁。
读写锁是“写模式加锁”时, 解锁前,所有尝试对该锁进行加锁(不管是读锁还是写锁)的线程都会被阻塞;--> 写独占
读写锁是“读模式加锁”时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。--> 读共享
读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求,优先满足写模式锁。--> 写锁优先级高
读写锁非常适合于对数据结构读的次数远大于写的情况。因为读锁是共享的,可以提高并发性。
#include
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,
void*(start_routine),void *arg);
参数:
thread: 传出参数,是无符号长整形数,线程创建成功,会将线程 ID 写入到这个指针指向的内存中
attr: 线程的属性,一般情况下使用默认属性即可,写 NULL
start_routine: 函数指针,创建出的子线程的处理动作,也就是该函数在子线程中执行。
arg: 作为实参传递到 start_routine 指针指向的函数内部
返回值:线程创建成功返回 0,创建失败返回对应的错误号
读锁是共享的,为啥要用读锁呢?
加锁是为了保证程序的原子性。加读锁可以保证读的时候没有其他线程写。比如一个long型参数的写操作并不是原子性的。如果不加读写锁,那读到的数很可能是写操作的中间状态,比如刚写完前32位的中间状态就读。
#include
#include
#include
#include
#include
#include
#include //创建进程的库文件
#define MAX 50
//全局变量
int number;
pthread_rwlock_t rwlock; //读写锁变量
void* read_num(void* arg)
{
for(int i = 0;i
#include //头文件
pthread_cond_t cond; //条件变量类型
//初始化
#include
pthread_cond_t cond;
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr); //cond是条件变量地址,attr是条件变量属性,一般使用默认属性,指定位NULL
// 销毁释放资源
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);
通过函数原型可以看出,该函数在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁主要功能是进行线程同步,让线程顺序进入临界区,避免出现数共享资源的数据混乱。该函数会对这个互斥锁做以下几件事情:
在阻塞线程时候,如果线程已经对互斥锁 mutex 上锁,那么会将这把锁打开,这样做是为了避免死锁
当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个 mutex 互斥锁锁上,继续向下访问临界区
用到生产者消费者模型中就是,当生产者生产的任务列表为空时,表示已经没有可消费的任务了,这时候会让抢到时间片的消费者线程阻塞。
#include
#include
#include
#include
#include
#include
#include //创建进程的库文件
//用生产者消费者模型测试条件对量加互斥锁
//定义一个链表节点
struct Node{
int number;
struct Node* next;
};
//定义条件变量,控制消费者线程
pthread_cond_t cond;
//互斥锁变量
pthread_mutex_t mutex;
//定义指向头结点的指针
struct Node* head = NULL;
//生产者线程
void *producer(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex); //互斥锁上锁
//创建一个链表的新节点
struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
pnew->number = rand() % 100;
pnew->next = head;
head = pnew; //指针前移
printf("+++product,number = %d,tid = %ld\n",pnew->number,pthread_self());
pthread_mutex_unlock(&mutex);
//生产了任务,通知消费者消费
pthread_cond_broadcast(&cond);
sleep(rand()%3);
}
return NULL;
}
//消费者线程
void* consumer(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex); //互斥锁上锁
//条件变量,任务队列中没有任务可以消费了
while(head == NULL)
{
//不能用if,会有bug,会出现段错误
//如果用if判断head为空后会阻塞当前线程,当生产者生产一个任务后消费者线程被唤醒,这时是直接从当前语句往下执行,不会再判断head是否等于空。这时如果唤醒了多个消费者,同时连续多个消费者都抢到了时间片,他们都再不会判断head是否为空。这样会造成消费者访问了空任务(这里是访问了空指针),出现段错误
//当生产队列为空时,消费者没有任务可以消费。阻塞当前线程,阻塞之前如果线程已经对互斥锁上锁,将这把锁打开,避免死锁。
pthread_cond_wait(&cond,&mutex);
//当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个 mutex 互斥锁锁上,继续向下访问临界区
}
struct Node* pnode = head;
printf("---consumer:number:%d,tid = %ld\n",pnode->number,pthread_self());
head = head->next;
free(pnode);
pthread_mutex_unlock(&mutex); //互斥锁解锁
sleep(rand()%3);
}
return NULL;
}
int main()
{
//初始化条件变量
pthread_cond_init(&cond,NULL);
pthread_mutex_init(&mutex,NULL); //初始化互斥锁
//创建生产者消费者线程
pthread_t pro[5],cum[5];
for(int i = 0;i <5;++i)
{
pthread_create(&pro[i],NULL,producer,NULL);
}
for(int i = 0;i <5;++i)
{
pthread_create(&cum[i],NULL,consumer,NULL);
}
//释放资源
for(int i = 0;i <5;++i)
{
pthread_join(pro[i],NULL);
}
for(int i = 0;i <5;++i)
{
pthread_join(cum[i],NULL);
}
//销毁条件变量
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex); //销毁互斥锁变量
}
#include
#include
#include
#include
#include
#include
#include //创建进程的库文件
#include //信号量的头文件
//用信号量来模拟生产者消费者模型
//链表的节点
struct Node
{
int number;
struct Node* next;
};
//生产者信号量
sem_t psem;
//消费者信号量
sem_t csem;
//加互斥锁变量
pthread_mutex_t mutex;
//创建链表头节点
struct Node* head = NULL;
void* producer(void* arg)
{
while(1)
{
sem_wait(&psem);
pthread_mutex_lock(&mutex);
struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
pnew->number = rand()%100;
pnew->next = head;
head = pnew;
printf("+++product,number = %d,tid = %ld\n",pnew->number,pthread_self());
pthread_mutex_unlock(&mutex);
//生产完毕,给消费者加信号灯
sem_post(&csem);
sleep(rand()%3);
}
return NULL;
}
void* consumer(void* arg)
{
while(1)
{
sem_wait(&csem);
pthread_mutex_lock(&mutex);
struct Node* pnode = head;
printf("---consumer:number:%d,tid = %ld\n",pnode->number,pthread_self());
head = head->next;
free(pnode);
pthread_mutex_unlock(&mutex);
//消费完毕,给生产者加信号灯
sem_post(&psem);
sleep(rand()%3);
}
return NULL;
}
int main()
{
//创建一个有初始值的链表
// for(int i = 0;i <3;i++)
// {
// struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
// pnew->number = rand()%100;
// pnew->next = head;
// head = pnew;
// }
//初始化信号量
//生产者和消费者拥有的信号灯的总和为4
sem_init(&psem,0,4); //生产者一共有4个信号灯
sem_init(&csem,0,0); //消费者一共有0个信号灯
//因为定义的是空链表,相当于任务位列中没有任务,所以消费者为0
//如果初始链表不是空链表,则消费者可以不为空
pthread_mutex_init(&mutex,NULL);
//创建生产者消费者线程
int ppthr = 5; //生产者线程数量
int cpthr = 5; //消费者线程数量
pthread_t p[ppthr],c[cpthr];
for(int i = 0;i
笔记来自:手写线程池 - C 语言版 | 爱编程的大丙 (subingwen.cn)
线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。如果某个线程在托管代码中空闲(如正在等待某个事件), 则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。
线程池的组成主要分为 3 个部分,这三部分配合工作就可以得到一个完整的线程池:
任务队列,存储需要处理的任务,由工作的线程来处理这些任务
通过线程池提供的 API 函数,将一个待处理的任务添加到任务队列,或者从任务队列中删除
已处理的任务会被从任务队列中删除
线程池的使用者,也就是调用线程池函数往任务队列中添加任务的线程就是生产者线程
工作的线程(任务队列任务的消费者) ,N个
线程池中维护了一定数量的工作线程,他们的作用是是不停的读任务队列,从里边取出任务并处理
工作的线程相当于是任务队列的消费者角色,
如果任务队列为空,工作的线程将会被阻塞 (使用条件变量 / 信号量阻塞)
如果阻塞之后有了新的任务,由生产者将阻塞解除,工作线程开始工作
管理者线程(不处理任务队列中的任务),1个
它的任务是周期性的对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测
当任务过多的时候,可以适当的创建一些新的工作线程
当任务过少的时候,可以适当的销毁一些工作的线程
#pragma once
#include
#include
//using定义别名,using 新的类型 = 旧的类型。
using callback = void (*)(void*); //定义callback是指向参数是void*,返回值是void的函数指针,(*)前面是返回值,后面是参数列表
//任务类,接收要处理的任务
struct Task
{
Task():function(nullptr),arg(nullptr)
{
}
Task(callback f,void* arg1) :function(f), arg(arg1)
{
}
callback function;
void* arg;
};
//任务队列
class TaskQueue
{
public:
TaskQueue();
~TaskQueue();
//添加任务
void AddTask(Task task);
void AddTask(callback func, void* arg);
//取出任务
Task TakeTask();
//当前队列中任务的数量
int TaskNum()
{
return m_taskQ.size();
}
private:
pthread_mutex_t m_mutex;
std::queue m_taskQ;
};
#include "TaskQueue.h"
TaskQueue::TaskQueue()
{
//初始化互斥锁
pthread_mutex_init(&m_mutex,NULL);
}
TaskQueue::~TaskQueue()
{
//销毁互斥锁
pthread_mutex_destroy(&m_mutex);
}
void TaskQueue::AddTask(Task task)
{
pthread_mutex_lock(&m_mutex);
m_taskQ.push(task);
pthread_mutex_unlock(&m_mutex);
}
void TaskQueue::AddTask(callback func, void* arg)
{
pthread_mutex_lock(&m_mutex);
Task t(func, arg);
m_taskQ.push(t);
pthread_mutex_unlock(&m_mutex);
}
Task TaskQueue::TakeTask()
{
Task t;
pthread_mutex_lock(&m_mutex);
if (!m_taskQ.empty())
{
t = m_taskQ.front();
m_taskQ.pop();
}
pthread_mutex_unlock(&m_mutex);
return t;
}
#pragma once
#include"TaskQueue.h"
#include
class ThreadPool
{
public:
//创建线程池并初始化
ThreadPool(int min, int max);
//销毁线程池
~ThreadPool();
//给线程池添加任务
void addTask(Task task);
//获取线程池中工作的线程的个数
int getBusyNum();
//获取线程池中活着的线程个数
int getAliveNum();
private:
//工作的线程(消费者线程)任务函数
static void* worker(void* arg); //静态成员函数智能访问类的静态成员变量
//管理者线程任务函数
static void* manager(void* arg);
//单个线程退出
void threadExit();
private:
//任务队列
TaskQueue* m_taskQ;
pthread_t m_managerID; //管理者线程ID
pthread_t* m_threadIDS; //工作的线程ID数组
int m_minNum; //最小线程数量
int m_maxNum; //最大线程数量
int m_busyNum; //忙的线程个数
int m_liveNum; //存活的线程个数
int m_exitNum; //要销毁的线程个数
pthread_mutex_t m_mutexPool; //锁整个的线程池
pthread_cond_t m_notEmpty; //条件变量,任务队列是不是空了
bool m_shutdown = false; //是不是要销毁线程池,销毁为1,不销毁为0
};
#include "ThreadPool.h"
#include
#include
#include
ThreadPool::ThreadPool(int min, int max)
:m_minNum(min),m_maxNum(max),m_busyNum(0),m_liveNum(min)
{
do
{
//实例化任务队列
m_taskQ = new TaskQueue;
if (m_taskQ == nullptr)
{
std::cout << "malloc taskQ fail..." << std::endl;
break;
}
//根据线程的最大上限给线程数组分配内存
m_threadIDS = new pthread_t[max];
if (m_threadIDS == nullptr)
{
std::cout << "malloc thread_t[] fail..." << std::endl;
break;
}
//初始化
memset(m_threadIDS, 0, sizeof(pthread_t) * max);
//初始化互斥锁,条件变量
if (pthread_mutex_init(&m_mutexPool, NULL) != 0 ||
pthread_cond_init(&m_notEmpty, NULL) != 0)
{
std::cout << "init mutex or condition fail..." << std::endl;
break;
}
m_shutdown = false;
//根据最小线程个数,创建线程
for (int i = 0; i < min; ++i)
{
pthread_create(&m_threadIDS[i], NULL, worker, this); //类的普通成员函数,只有在对象实例化之后才有地址,可以定义为静态成员函数
std::cout << "创建子线程,ID:" << std::to_string(m_threadIDS[i]) << std::endl;
}
//创建管理者线程,1个
pthread_create(&m_managerID, NULL, manager, this); //this指针指向当前被实例化的对象,把当前对象传递给manager
} while (0);
//释放资源
//if (m_threadIDS) delete[]m_threadIDS;
//if (m_taskQ) delete m_taskQ;
}
ThreadPool::~ThreadPool()
{
//关闭线程池
m_shutdown = true;
//阻塞回收管理者线程
pthread_join(m_managerID,NULL);
//唤醒阻塞的消费者线程
for(int i = 0;i AddTask(task);
//唤醒阻塞在条件变量的消费者线程
pthread_cond_signal(&m_notEmpty);
}
//获取线程池中工作的线程个数
int ThreadPool::getBusyNum()
{
pthread_mutex_lock(&m_mutexPool);
int busyNum = m_busyNum;
pthread_mutex_unlock(&m_mutexPool);
return busyNum;
}
//获取线程池中活着的线程个数
int ThreadPool::getAliveNum()
{
pthread_mutex_lock(&m_mutexPool);
int aliveNum = m_liveNum;
pthread_mutex_unlock(&m_mutexPool);
return aliveNum;
}
void* ThreadPool::worker(void* arg)
{
ThreadPool* pool = static_cast(arg);
//判断任务队列是否为空,如果为空工作线程阻塞
while (true)
{
//访问任务队列(共享资源)加锁
pthread_mutex_lock(&pool->m_mutexPool);
//判断任务队列是否为空,如果为空工作线程阻塞
while (pool->m_taskQ->TaskNum() == 0 && !pool->m_shutdown)
{
std::cout << "thread " << std::to_string(pthread_self()) << " waiting..." << std::endl;
//阻塞线程
pthread_cond_wait(&pool->m_notEmpty, &pool->m_mutexPool);
//解除阻塞之后,判断是否要销毁线程,跟管理者线程有关系
if (pool->m_exitNum > 0)
{
pool->m_exitNum--;
if (pool->m_liveNum > pool->m_minNum)
{
pool->m_liveNum--;
pthread_mutex_unlock(&pool->m_mutexPool);
pool->threadExit();
}
}
}
//判断线程池是否被关闭了
if (pool->m_shutdown)
{
//如果线程池被关闭了,要把互斥锁打开,避免死锁
pthread_mutex_unlock(&pool->m_mutexPool);
pool->threadExit(); //退出当前线程
}
//从任务队列中取出一个任务
Task task = pool->m_taskQ->TakeTask();
//工作的线程+1
pool->m_busyNum++;
//线程池解锁
pthread_mutex_unlock(&pool->m_mutexPool);
//执行任务,人物的执行跟线程池和任务队列没关系
std::cout << "thread " << std::to_string(pthread_self()) << " start working..." << std::endl; //to_string能把一个整形数转换成字符串类型
task.function(task.arg); //任务函数执行过程中,会一直阻塞在这
delete task.arg;
task.arg = nullptr;
//任务处理结束
std::cout << "thread " << std::to_string(pthread_self()) << " end working..." << std::endl;
//由于线程池中的m_busyNum会被多个线程访问,所以在修改之前要加锁
pthread_mutex_lock(&pool->m_mutexPool);
pool->m_busyNum--;
pthread_mutex_unlock(&pool->m_mutexPool);
}
return nullptr;
}
//管理者线程:主要工作是管理线程池中的线程数,增加或销毁线程
void* ThreadPool::manager(void* arg)
{
ThreadPool* pool = static_cast(arg);
while(!pool->m_shutdown) //要用pool->的原因:非静态数据成员的引用必须与特定的对象相对
{
//每隔一段时间检测一次
sleep(5);
// std::cout<<"管理者线程开始运行"<m_mutexPool);
int queueSize = pool->m_taskQ->TaskNum();
int liveNum = pool->m_liveNum;
int busyNum = pool->m_busyNum;
pthread_mutex_unlock(&pool->m_mutexPool);
//如果当前任务数>存活的线程数&&存活的线程数<最大线程数,则添加线程
const int NUMBER = 2; //一次要添加的线程个数
if(queueSize>liveNum&&liveNumm_maxNum)
{
//对线程池进行操作要加锁
pthread_mutex_lock(&pool->m_mutexPool);
int num = 0;
for(int i = 0;i m_maxNum&&numm_liveNumm_maxNum;++i)
{
if(pool->m_threadIDS[i] == 0)
{
pthread_create(&pool->m_threadIDS[i],NULL,worker,pool);
++num;
++pool->m_liveNum;
}
}
pthread_mutex_unlock(&pool->m_mutexPool);
}
//如果当前忙线程数*2<存活的线程数目&&存活的线程数>最小线程数,销毁多余线程
if(busyNum*2pool->m_minNum)
{
pthread_mutex_lock(&pool->m_mutexPool);
pool->m_exitNum = NUMBER;
pthread_mutex_unlock(&pool->m_mutexPool);
//让工作的线程自杀
for(int i = 0;i < NUMBER;++i)
{
pthread_cond_signal(&pool->m_notEmpty); //唤醒条件变量的阻塞,跳转到worker函数看是否要销毁线程
}
}
}
return nullptr;
}
//退出当前线程
void ThreadPool::threadExit()
{
pthread_t tid = pthread_self();
for(int i = 0;i
//测试程序
#include"ThreadPool.h"
#include
#include
void taskFunc(void* arg)
{
int num = *(int*)arg;
std::cout<<"thread "<< pthread_self()<<" is working,num = "<< num<
运行过程分析:
运行开始,先进入main函数,创建线程池对象,调用ThreadPool的构造函数,创建三个子线程,子线程等待。创建管理者线程,管理者线程睡眠。
继续执行main函数,进入for循环,往任务队列添加任务,然后唤醒阻塞在条件变量的消费者线程,消费者线程加锁,从任务队列中取出一个任务,任务队列任务数-1,线程解锁,执行任务。执行任务与线程池无关。
在步骤2中,主线程和子线程谁抢到时间片等系统资源谁执行,在这期间,“睡醒”的管理者也加入抢时间片的队列,谁抢到谁执行。
最后,调用ThreadPool析构函数,回收资源。