网站建设中网站需求分析报告功能,建设网站选择主机时费用最昂贵的方案是,衡水网站建设的地方,中国品牌网是什么网站线程与进程 多级页表 线程控制 线程互斥 线程同步 生产者消费者模型 常见概念
下面选取32位系统举例。
一.线程与进程
上图是曾经我们认为进程所占用的资源的集合。
1.1 线程概念
线程是一个执行分支#xff0c;执行粒度比进程细#xff0c;调度成本比进程低线程是cpu… 线程与进程 多级页表 线程控制 线程互斥 线程同步 生产者消费者模型 常见概念
下面选取32位系统举例。
一.线程与进程
上图是曾经我们认为进程所占用的资源的集合。
1.1 线程概念
线程是一个执行分支执行粒度比进程细调度成本比进程低线程是cpu调度的基本单位进程是分配资源的基本单位线程是进程内部的一个执行流
通常我们创建线程是为了执行程序的一部分代码所以执行粒度一定比进程更细我们知道进程内核数据结构代码和数据。引入线程之后这个概念就应该修正为进程许多内核数据结构代码和数据。这些数据结构指向同一个程序地址空间如图在操作系统的概念中这样的数据结构叫做线程控制块TCB但由于创建新的数据结构还要设计新的调度逻辑所以Linux在内核中并没有设计这样的数据结构而是复用进程的数据结构PCB。故此在Linux中并没有真正的线程而是用进程模拟的线程线程在Linux中叫轻量级进程LWP。
1.2 理解线程
创建线程只需要执行一部分代码所以执行粒度比进程细当发生调度时操作系统会识别是进程间的调度还是线程间的调度如果是线程间的调度那么只需要改变部分寄存器的值即可而cache不需要重新加载代码。所以线程的调度成本是比进程小的。cpu每次调度都是以线程为单位的操作系统分配资源是以进程为单位的。线程有自己的栈线程共享进程的大部分数据但也有私有数据 **线程ID ****一组寄存器 ****栈 ****errno ****信号屏蔽字 **调度优先级
1.3 线程优点
创建一个新线程的代价要比创建一个新进程小得多与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多线程占用的资源要比进程少很多能充分利用多处理器的可并行数量在等待慢速I/O操作结束的同时程序可执行其他的计算任务计算密集型应用为了能在多处理器系统上运行将计算分解到多个线程中实现I/O密集型应用为了提高性能将I/O操作重叠。线程可以同时等待不同的I/O操作。
1.4 线程缺点
健壮性减低主线程退出全部线程退出一个线程收到信号所有线程收到信号缺乏访问控制相比与进程线程之间的通信成本是极低的但是线程之间的通信缺乏访问控制即同步机制。当一个线程正在写入还没有写完另一个线程就可能会读走数据。
二.多级页表 文件系统指出磁盘和内存的数据交换是以块4KB为单位因此内存的管理也要以4KB为单位在内存中这样的结构叫做页。 如果页表只有一张并且页表保存的是字节间的映射关系那么一个页表需要保存232行这样肯定是不行的一是查找速度慢二是内存可能不够。因此Linux中采用多级页表并且将32虚拟地址拆分成3部分使用前10位用来在页目录中使用中间10位在页表项中使用最后12位做为页内偏移地址使用。212 4KB
页目录共有1024行每个页表项也有1024行共可以映射2^20页地址这恰好是内存中页的数量页表映射出物理页号物理页号和虚拟地址中的12位页内偏移组合构成物理字节地址。 由于程序不可能使用整个内存所以页表不会一次全部创建而是创建一部分。 三.线程控制 3.1 线程库的理解
在Linux中没有真正意思上线程所以系统不会提供线程相关的接口但是有控制轻量级进程的相关接口。可是用户只认线程所以有了用户级线程库pthread这个库底层封装了轻量级进程的相关接口给上层提供了线程相关的接口。pthread库在任何一个Linux系统上都自带了因此pthread库也叫做原生线程库。由于pthread库属于第三方库所以在使用gcc/g编译时需要指定库名称。gcc xxxx -lpthread
3.2 创建线程
pthread_creatpthread_t整数输出型参数返回给用户线程id以后操作线程用线程id。attr可以定义线程的属性一般设置为空void*(start_routine)(void) 函数指针设置线程指向的函数void* arg线程执行函数的参数返回值成功返回0失败返回-1
代码示例
#includeiostream
#includepthread.h
#include unistd.husing namespace std;void* threadRoutine(void* s)
{const char* str (const char*)s;cout str endl;}
int main()
{pthread_t p1;pthread_create(p1, nullptr, threadRoutine, (void*)thread 1);sleep(3);return 0;
}通过命令ps -aL可以查看当前用户创建的线程线程不分父子分主次主线程的PIDLWP。操作系统判断是进程间切换还是线程间切换通常使用PID来区分的看两个结构体的PID是否相同相同即是线程间切换。 如果主线程退出当前进程的所有线程立即退出。如果任一线程收到信号那么所有线程都会收到信号。 3.3 线程等待
如果主线程不等待新线程那么新线程会产生类似僵尸进程的情况导致内存泄漏。
pthread_join 等待指定id的线程void** retval输出型参数已知线程执行的任务返回值为void*因此要获得这个值就需要void*的地址即void**
代码示例
#includeiostream
#includepthread.h
#include unistd.h
#include cstringusing namespace std;void* threadRoutine(void* s)
{const char* str (const char*)s;cout str endl;return (void*)1; }
int main()
{pthread_t p1;pthread_create(p1, nullptr, threadRoutine, (void*)thread 1);void *retval nullptr;int n pthread_join(p1, retval);if (n ! 0){cerr errno: errno strerror(errno) endl;}// 当前环境是64位的int是32位的如果强转为32位会报错。cout wait successful! retval: (int64_t)retval endl;return 0;
}3.4 线程分离
如果新线程不用被等待获取返回值可以在主线程中分离该线程
pthread_detach 分离指定id的线程
#includeiostream
#includepthread.h
#include unistd.h
#include cstringusing namespace std;void* threadRoutine(void* s)
{const char* str (const char*)s;cout str endl;
}
int main()
{pthread_t p1;pthread_create(p1, nullptr, threadRoutine, (void*)thread 1);pthread_detach(p1);sleep(3);return 0;
}3.5 线程退出
线程退出有三种方式
return线程正常执行完退出pthread_exit, 谁调用该函数谁退出retval为返回值。pthread_cancel让指定id的线程退出退出码为PTHREAD_CANCELED-1
3.6 获取线程id
pthread_self可以获取当前线程的id #includeiostream
#includepthread.h
#include unistd.h
#include cstringusing namespace std;void* threadRoutine(void* s)
{const char* str (const char*)s;cout str endl;}
int main()
{pthread_t p1;pthread_create(p1, nullptr, threadRoutine, (void*)thread 1);pthread_detach(p1);cout pthread_self() endl;sleep(3);return 0;
}3.7 线程id与LWP的区别
pthread库是共享库也会被映射到进程地址空间中的共享区。在pthread库中创建的所有线程会被组织成数组。而线程id就是数组中线程的地址。LWP是轻量级进程使用的被封装进struct pthread字段中。这种结构类似文件struct FILE中封装了文件描述符fd。
每一个线程都拥有自己的栈主线程的栈是进程地址空间的栈而其他线程的栈都在共享区中所有线程共享大部分数据如果创建了一个全局变量但是想让每个线程都拥有这个变量那么就需要在前面加__thread,然后每个线程都会在自己的局部存储里面创建这个全局变量。其生命周期是全局的。
#includeiostream
#includepthread.h
#include unistd.h
#include cstringusing namespace std;
__thread int _gval 0;void* threadRoutine(void* s)
{const char* str (const char*)s;cout str _gval: _gval _gval: _gval endl;
}void* threadRoutine1(void* s)
{sleep(3);const char* str (const char*)s;cout str _gval: _gval _gval: _gval endl;
}
int main()
{pthread_t p1, p2;pthread_create(p1, nullptr, threadRoutine, (void*)thread 1);pthread_create(p2, nullptr, threadRoutine1, (void*)thread 2);sleep(4);return 0;
}上面两个线程打印出来的地址一定不同。
四.线程互斥
临界资源多个执行流共享的资源就是临界资源临界区访问临界资源的代码就是临界区互斥任一时刻只有一个执行流进入临界区。原子性一个操作只有两种状态要么还没开始要么就已完成。
在多线程的场景下不同线程访问临界资源会引发线程安全的问题例如对临界资源做自增操作。
#includeiostream
#includepthread.h
#include unistd.h
#include cstringint _gval 0;void* threadRoutine(void* s)
{ int cnt 0;while (cnt 100){cnt;}
}int main()
{pthread_t p1, p2;pthread_create(p1, nullptr, threadRoutine, nullptr);pthread_create(p2, nullptr, threadRoutine1, nullptr);sleep(4); return 0;
}cnt这一条代码会被解释为三条汇编指令
如果一个线程执行到第二步的时候时间片到了切换为另一个线程然后再切换回第一个线程那么第二个线程对cnt的操作就会被覆盖。如果要保证对cnt的操作是线程安全的即没有并发访问问题就需要对该临界区加锁让临界区任何时刻只能有一个线程进入。
4.1 互斥锁接口
锁的接口定义在pthread.h头文件中。
定义
pthread_mutex_t mutex;
定义mutex变量
初始化 pthread_mutex_t* mutex 传入外部定义的锁。const pthread_mutexattr_t* attr锁的属性一般设置为nullptr
销毁 pthread_mutex_t* mutex 传入外部定义的锁 如果定义的锁是全局变量可以使用PTHREAD_MUTEX_INITIALIZER初始化不需要调用pthread_mutex_init() 和 pthread_mutex_destroy() 加锁 解锁 代码示例
#includeiostream
#includepthread.h
#include unistd.h
#include cstringint _gval 0;// 写法一
pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;void* threadRoutine(void* s)
{ int cnt 0;pthread_mutex_lock(mutex);while (cnt 100){cnt;pthread_mutex_unlock(mutex);}}int main()
{pthread_t p1, p2;pthread_create(p1, nullptr, threadRoutine, nullptr);pthread_create(p2, nullptr, threadRoutine1, nullptr);sleep(4); return 0;
}// 写法二void* threadRoutine(void* s)
{ pthread_mutex_t* mutex (pthread_mutex_t*)s;int cnt 0;pthread_mutex_lock(mutex);while (cnt 100){cnt;pthread_mutex_unlock(mutex);}}int main()
{pthread_mutex_t mutex;pthread_mutex_init(mutex, nullptr);pthread_t p1, p2;pthread_create(p1, nullptr, threadRoutine, (void*)mutex);pthread_create(p2, nullptr, threadRoutine1, (void*)mutex);pthread_mutex_destroy(mutex);sleep(4); return 0;
}4.2 锁的原理
锁也是共享资源访问共享资源就会有线程安全问题。为了防止这种套娃情况的发生加锁和解锁操作具有原子性大多数体系结构都提供了swap或exchange指令这两条指令都保证了加锁的原子性。 加锁 伪代码 lock movb $0, %al xchgb %al, mutex ;这条指令是核心将锁转移到当前线程的上下文中这也就相当于线程取得了锁资源 if al 0 then return 0 else 挂起等待 goto lock 解锁 movb $1 ,mutex 唤醒等待mutex的线程 return 0; 在上面代码中1不会增多只会在不同线程之间流转从而保证了多个线程只有一个锁资源
4.3 死锁 死锁是指在一组进程中的各个进程均占有不会释放的资源但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。br /**四个条件之一**互斥请求与保持线程请求资源的过程中不放弃持有资源不剥夺一个执行流已获得的资源在末使用完之前不能强行剥夺环路等待若干执行流之间形成一种头尾相接的循环等待资源的关系
预防死锁破坏四个条件之一
不加锁可以破坏互斥条件主动释放锁可以破坏请求与保持条件控制线程释放锁资源可以破坏不剥夺条件按照顺序申请锁可以破坏环路等待条件
五.线程同步
如果一个线程释放锁之后又立即申请锁那么可能会导致其他线程一直申请不到锁这样就会形成饥饿问题。这样明显是不合理的我们可以使用条件变量来防止发生饥饿问题。一般条件变量是配合互斥锁使用的。条件变量的接口定义在pthread.h头文件中。
5.1 条件变量
定义
pthread_cond_t
定义一个条件变量
初始化 pthread_cond_t* cond 传入外部定义的条件变量。const pthread_condxattr_t* attr条件变量的属性一般设置为nullptr
销毁 阻塞 pthread_cond_t* cond阻塞cond条件变量pthread_mutex_t *mutex当线程被阻塞时会释放锁等到线程被唤醒时会重新申请锁
唤醒 唤醒在条件变量下等待的线程有两种方式1.唤醒一个线程 2.唤醒所有线程pthread_cond_singal() 唤醒在条件变量cond下等待的一个线程pthread_cond_broadcast() 唤醒在条件变量cond下等待的所有线程
5.2 posix信号量 POSIX信号量和SystemV信号量作用相同都是用于同步操作达到无冲突的访问共享资源目的。 POSIX可以用于线程间同步。信号量是资源的一种预定机制将共享资源看作多份。信号量相关的接口在semaphore.h头文件中。定义信号量
sem_t
信号量类型可以定义信号量
初始化 pshared:0表示线程间共享非零表示进程间共享value信号量初始值资源的数量当value1时为二元信号量效果等同于互斥锁。
销毁 申请信号量资源 对信号量初始值(value)-1
归还信号量资源 对信号量初始值(value)1
六.生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯而通过阻塞队列来进行通讯所以生产者生产完数据之后不用等待消费者处理直接扔给阻塞队列消费者不找生产者要数据而是直接从阻塞队列里取阻塞队列就相当于一个缓冲区平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。如果我们想要维护好生产者消费者模型就要研究3种关系两种角色一个缓冲区
三种关系 生产者与生产者具有互斥关系消费者与消费者具有互斥关系生产者与消费者具有互斥和同步关系。 同步当缓冲区满时让消费者消费当缓冲区空时让生产者生产 两种角色 生产者消费者 一个缓冲区 基于阻塞队列基于环形队列
生产者消费者模型的优点
效率高 效率高体现在可以并发生产任务和并发处理任务 支持忙闲不均 生产者生产任务之后不必等消费者消费而是将任务放到缓冲区可以继续进行生产任务。
6.1 基于阻塞队列(BlockQueue)式的cp问题
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于当队列为空时从队列获取元素的操作将会被阻塞直到队列中被放入了元素当队列满时往队列里存放元素的操作也会被阻塞直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的线程在对阻塞队列进程操作时会被阻塞)。
主要用到互斥锁和条件变量
生产者可以生产一系列任务让消费者执行任务。下面是模拟的一个计算器任务。 #pragma once#include iostream
#include string
#include unordered_map
#include functionalclass Task
{
public:Task() default;Task(int x, int y, char op) : _x(x), _y(y), _op(1, op){ }void run(){std::unordered_mapstd::string, std::functionvoid(void) cal {{, [this]{ this-_result this-_x this-_y; }},{-, [this]{ this-_result this-_x - this-_y; }},{*, [this]{ this-_result this-_x * this-_y; }},{/, [this]{ if (this-_y 0) this-_exitStatus 2; this-_result this-_x/this-_y; }},{%, [this]{ if (this-_y 0) this-_exitStatus 1; this-_x%this-_y; }}};cal[_op]();}void formatExpress(){std::cout productor : _x _op _y ? std::endl;}void formatRes(){std::cout consumer : _x _op _y _result std::endl;}int getExitCode(){return _exitStatus;}int getRes(){return _result;}private:int _x;int _y;std::string _op;int _result;int _exitStatus;
};#pragma once
#include iostream
#include pthread.h
#include memory
#include vector
#include queue
#include unistd.husing std::cout;
using std::endl;const int defaultCapacity 5;template class T
class BlockQueue
{public:BlockQueue(int cap defaultCapacity): _cap(cap){pthread_mutex_init(_mutex, nullptr);pthread_cond_init(_consumerCond, nullptr);pthread_cond_init(_productorCond, nullptr);}bool isFull(){return _bq.size() _cap;}void push(const T in){// 生产者生产数据pthread_mutex_lock(_mutex);// 如果缓冲区满则生产者阻塞while (isFull()) pthread_cond_wait(_productorCond, _mutex); _bq.push(in);/// 可以采用一定策略唤醒消费者消费资源pthread_cond_signal(_consumerCond);pthread_mutex_unlock(_mutex);}bool isempty(){return _bq.empty();}void pop(T* out){pthread_mutex_lock(_mutex);while (isempty())pthread_cond_wait(_consumerCond, _mutex);*out _bq.front();_bq.pop();pthread_mutex_unlock(_mutex);pthread_cond_signal(_productorCond);}~BlockQueue(){pthread_mutex_destroy(_mutex);pthread_cond_destroy(_consumerCond);pthread_cond_destroy(_productorCond);}private:// 缓冲区std::queueT _bq;// 缓冲区容量int _cap;// 互斥锁pthread_mutex_t _mutex;// 条件变量当缓冲区满的时候生产者阻塞当缓冲区空的时候消费者阻塞pthread_cond_t _consumerCond;pthread_cond_t _productorCond;
};#include bQueue.hpp
#include Task.hpp
#include cstringvoid *consume(void *args)
{BlockQueueTask *bq static_castBlockQueueTask *(args);while (true){// 从缓存区拿资源Task t;bq-pop(t);// 使用资源t.run();t.formatRes();sleep(1);}return nullptr;
}void *product(void *args)
{BlockQueueTask *bq static_castBlockQueueTask *(args);const char* s -*/%;while (true){sleep(1);// 生产资源int x rand()%10; int y rand()%10; char op s[rand()%strlen(s)];Task t(x, y, op);t.formatExpress(); // 将资源放到缓冲区中bq-push(t);}return nullptr;
}int main()
{srand(time(nullptr));int n 5;BlockQueueTask *bq new BlockQueueTask(n);std::vectorpthread_t consumers(3);std::vectorpthread_t productors(4);for (auto pth : consumers){pthread_create(pth, nullptr, consume, static_castvoid *(bq));}for (auto pth : productors){pthread_create(pth, nullptr, product, static_castvoid *(bq));}for (auto tid : consumers)pthread_join(tid, nullptr);for (auto tid : productors)pthread_join(tid, nullptr);return 0;
}6.2 基于环形队列的cp问题
下面实现一种基于环形队列的生产者消费者模型采用数组模型环形队列。在阻塞队列中我们将缓冲区看作一个整体使用因此也只定义了一个互斥锁就可以保证线程安全。但在环形队列中我们将缓冲区看成n个小空间使用因此使用信号量只要消费者和生产者不指向同一个小空间它们就可以并发执行。消费者关心的是数据个数生产者关心的是剩余空间个数因此我们可以定义两个信号量当剩余空间为0时阻塞生产者当数据个数为0时阻塞消费者。生产者与生产者之间互斥关系要靠一个互斥锁消费者与消费者互斥关系也要靠一个互斥锁故需要定义两个互斥锁。不能设置成一个互斥锁因为生产者与消费者之间除非所占空间相同不然没有互斥关系。
#pragma once#include iostream
#include pthread.h
#include memory
#include vector
#include queue
#include unistd.h
#include semaphore.husing std::cout;
using std::endl;
const int N 5;template class T
class RingQueue
{
public:RingQueue(int cap N) : _cap(cap), _consumerIndex(0), _productorIndex(0), _vRingQueue(cap){pthread_mutex_init(_consumerMutex, nullptr);pthread_mutex_init(_productorMutex, nullptr);sem_init(_spaceSem, 0, _cap);sem_init( _dataSem, 0, 0);}void push(const T in){// 生产者生产数据P(_spaceSem);lock(_productorMutex);_vRingQueue[_productorIndex] in;_productorIndex % _cap;unlock(_productorMutex);V(_dataSem);}void pop(T *out){P(_dataSem);lock(_consumerMutex);*out _vRingQueue[_consumerIndex];_consumerIndex % _cap;unlock(_consumerMutex);V(_spaceSem);}~RingQueue(){pthread_mutex_destroy(_consumerMutex);pthread_mutex_destroy(_productorMutex);sem_destroy(_spaceSem);sem_destroy(_dataSem);}private:void lock(pthread_mutex_t mutex){pthread_mutex_lock(mutex);}void unlock(pthread_mutex_t mutex) { pthread_mutex_unlock(mutex);}void P(sem_t sem){ sem_wait(sem);}void V(sem_t sem){sem_post(sem);}private:std::vectorT _vRingQueue;int _cap; // 环形队列容量sem_t _spaceSem; // 生产者关系的是空间资源sem_t _dataSem; // 消费者关心的是数据资源pthread_mutex_t _consumerMutex;pthread_mutex_t _productorMutex;// 消费者访问空间int _consumerIndex;// 生产者访问空间int _productorIndex;
};七.常见概念
STL中的容器不是线程安全的智能指针是线程安全的 unique_ptr不涉及全局变量只在局部有效shared_ptr引用计数有线程安全问题但是设计者采用了CAS操作保证了线程安全 悲观锁在每次取数据时总是担心数据会被其他线程修改所以会在取数据前先加锁读锁写锁行锁等当其他线程想要访问数据时被阻塞挂起。乐观锁每次取数据时候总是乐观的认为数据不会被其他线程修改因此不上锁。但是在更新数据前会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式版本号机制和CAS操作。自旋锁非阻塞的申请锁资源。如果访问临界区的时间短可以采用自旋锁。 pthread_spinlock_t可以定义自旋锁