龙华做网站的,建设网站一定要数据库吗,南京网站优化步骤,wordpress 手机验证码目录
线程概念
线程优点
线程缺点
线程异常
线程系统编程接口
线程创建及终止
线程等待
使用线程系统接口封装一个小型的C线程库并实现一个抢票逻辑
线程互斥
互斥量的接口
线程互斥实现原理
使用系统加锁接口封装LockGuard 实现自动化加锁
线程安全和可重入函数 …
目录
线程概念
线程优点
线程缺点
线程异常
线程系统编程接口
线程创建及终止
线程等待
使用线程系统接口封装一个小型的C线程库并实现一个抢票逻辑
线程互斥
互斥量的接口
线程互斥实现原理
使用系统加锁接口封装LockGuard 实现自动化加锁
线程安全和可重入函数
常见线程安全和可重入情况
死锁
银行家算法简介 线程概念
线程是在进程内部并且比进程更加轻量化的一种执行流。
线程是 CPU 调度的基本单位而进程是承担系统资源的基本实体
一个进程内部可以包含多个线程而在 LIinux 中没有强制性的划分进程和线程的概念线程也被叫做轻量级进程。
传统进程内部只有一个执行流而进程内部创建了线程之后内核中就有多个执行流了。
进程与线程的关系 合理的使用多线程能提高CPU执行密集型程序的执行效率和用户对IO密集型程序的使用体验 CPU密集型程序指的是需要大量计算和处理的任务涉及大量的数学运算、逻辑判断、数据处理等对CPU的计算能力要求较高IO密集型程序主要是指执行过程中需要大量的 IO 操作大型文件多线程下载涉及到大量的网络I/O操作和磁盘I/O操作需要从网络中读取文件数据并将其写入到本地磁盘中。 CPU 是通过 PCB 对进程调度的既然线程叫轻量级进程那么在 Linux 中CPU 调度线程的方式也是这样。但 进程 内核数据结构 代码和数据相比于进程线程就没有这么多资源了线程共享进程的所有数据但也有自己的数据线程ID一组寄存器栈errno信号屏蔽字调度优先级所以相比于进程它的创建更简单。
各进程之间共享的进程资源和环境有文件描述符表每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)当前工作目录用户id和组id。
线程在进程内部运行本质是在进程地址空间内运行所以对应调度所需要的寄存器少且在轮转调度的时候不需要反复更新自己的线程上下文从不需要更新缓存cache。
透过进程虚拟地址空间可以看到进程的大部分资源将进程资源合理分配给每个执行流就形成了线程执行流。 虚拟地址通过页表化为物理地址详细过程 虚拟地址的32个比特位前20个比特位是 “页表索引” 这前10个比特位形成了 2^10 - 1个页目录这 2^10 - 1个页目录中每个页目录中又存着 2^10 - 1个 “后10个比特位” 形成的数这样计算的页表一共可以映射 2 ^ 20 1048576个数而这些数页表码每个数对应一个地址标识一个IO文件的基本大小 4KB2^10 * 2^2对这 2^10 个基本IO文件编号叫页内偏移所以这 2^20 个页表码每个都对应着 2^10 个基本 IO 文件的大小4KB2^20 次方个 基本IO 文件的大小加起来也就是我们常说的 4GB 因此页表码 页内偏移 就能知道对应的文件位置 对这 1048576 个页码建立数据结构那么对页表的管理就变成了对数据结构的增删查改 有了这种页表划分逻辑进程就可以将地址空间合理的分配给线程
线程优点
创建一个新线程的代价要比创建一个新进程小得多与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多线程占用的资源要比进程少很多能充分利用多处理器的可并行数量在等待慢速I/O操作结束的同时程序可执行其他的计算任务计算密集型应用为了能在多处理器系统上运行将计算分解到多个线程中实现I/O密集型应用为了提高性能将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程缺点
性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多那么可能会有较大的性能损失这里的性能损失指的是增加了额外的同步和调度开销而可用的资源不变。
健壮性降低
编写多线程需要更全面更深入的考虑在一个多线程程序里因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
编写与调试一个多线程程序比单线程程序困难得这里的难度主要指考虑一段代码需要考虑的情况更复杂。
线程异常
单个线程如果出现除零野指针问题导致线程崩溃进程也会随着崩溃
线程是进程的执行分支线程出异常就类似进程出异常进而触发信号机制终止进程进程终止该进程内的所有线程也就随即退出。
线程系统编程接口
线程创建及终止
创建一个新的线程
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失败返回错误码
线程库NPTL提供了pthread_ self函数可以获得线程自身的ID
pthread_t pthread_self(void);
pthread_ create函数会产生一个线程ID存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。 前面的线程ID属于进程调度的范畴叫 LWP只在系统内部使用当一个进程中只有一个线程时这个线程的 LWP 就等于进程的 PID 。因为线程是轻量级进程是操作系统调度器的最小单位所以需要一个数值来唯一表示该线程。 pthread_ create 函数第一个参数指向一个虚拟内存单元该内存单元的地址即为新创建线程的线程ID属于NPTL线程库的范畴。线程库的后续操作就是根据该线程ID来操作线程的。
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
2. 线程可以调用pthread_ exit终止自己。
3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
pthread_exit 函数可以获得进程的退出信息存储在 value_ptr 指针中返回
void pthread_exit(void *value_ptr);
取消一个进程成功返回0失败返回错误码
int pthread_cancel(pthread_t thread);// thread 为要终止线程的 id
线程等待
为什么要进行线程等待
线程终止后内核资源并没有被释放只有线程被等待成功后内核资源才能被释放。等待可以得到线程的退出信息
pthread_join 接口可以等待线程结束
int pthread_join(pthread_t thread, void **value_ptr);
线程以不同的方式终止用 pthread_join 接口得到的终止状态是不同的 1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。 2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。 3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传pthread_exit的参数。 4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。 使用线程系统接口封装一个小型的C线程库并实现一个抢票逻辑
#include pthread.h
#include functional
#include string
#include iostream
template class T
using func_t std::functionvoid(T);template class T
class Thread
{
public:Thread(const std::string threadname, func_tT func, T data):_tid(0),_threadname(threadname),_isrunning(false),_func(func),_data(data){}// 因为pthread 里的 函数指针 规定只能有一个 void* 类型的参数, 如果不定义为 static(类内方法)的话, 第一个参数永远是this那么就不止一个参数了static void* Pthread_Routine(void* args) {Thread* ts static_castThread*(args);ts-_func(ts-_data);return nullptr;}bool Start(){int n pthread_create(_tid, nullptr, Pthread_Routine, this);//将 this 指针传过去, 方便一会调用函数指针使用传进来的 pthread 数据if(n 0){_isrunning true;return true;}else return false;}bool Join(){if(!_isrunning) return true;int n pthread_join(_tid, nullptr);if(n 0) //等待成功{_isrunning true;return true;}return false;}bool IsRunning(){return _isrunning;}std::string ThreadName(){return _threadname;}~Thread(){}
private:pthread_t _tid;std::string _threadname;bool _isrunning;func_tT _func;T _data;
};实现一个抢票逻辑
#include iostream
#include pthread.hpp
#include cstdio
#include unistd.h
std::string GetThreadName()
{static int Number 0;char buffer[64];snprintf(buffer, sizeof(buffer), thread-%d, Number);return buffer;
}
void Print(int i)
{std::cout i std::endl;
}
int ticket 10000;
void GetTicket(std::string name)
{while(true){if(ticket 0){usleep(100);printf(%s get a ticket %d\n, name.c_str(), ticket);ticket--; }else break;}
}
int main()
{std::string name1 GetThreadName();Threadstd::string th1(name1, GetTicket, name1);std::string name2 GetThreadName();Threadstd::string th2(name2, GetTicket, name2);std::string name3 GetThreadName();Threadstd::string th3(name3, GetTicket, name3);std::string name4 GetThreadName();Threadstd::string th4(name4, GetTicket, name4);th1.Start();th2.Start();th3.Start();th4.Start();th1.Join();th2.Join();th3.Join();th4.Join();return 0;
}
运行结果确实使用多线程并发完成了抢票但依然出现了一些问题 票竟然被抢到了 0 和负数这是这些线程并发访问公共资源的时候产生的数据不一致问题。
为什么可能无法获得争取结果if 语句判断条件为真以后代码可以并发的切换到其他线程usleep 这个模拟漫长业务的过程在这个漫长的业务过程中可能有很多个线程会进入该代码段--ticket 操作本身就不是一个原子操作需要三步原子操作这当中线程可能会被切换 要解决上述问题需要做到一下三点
代码必须要有互斥行为当代码进入临界区执行时不允许其他线程进入该临界区。如果多个线程同时要求执行临界区的代码并且临界区没有线程在执行那么只能允许一个线程进入该临界区。如果线程不在临界区中执行那么该线程不能阻止其他线程进入临界区
本质就是系统需要一把锁将公共代码保护起来Linux 将这把锁叫互斥量。
线程互斥
临界资源在多线程编程中被多个线程共享的一段代码或数据结构。这些资源在同一时间只能由一个线程访问。
临界区在进程中访问临界资源的代码。
互斥在任何时刻互斥保证有且只有一个执行流进入临界区访问临界资源对临界资源起保护作用。
原子性不会被任何调度机制打断的操作改操作只有两态要么完成要么未完成。
互斥量的接口
初始化互斥量
初始化互斥量有两种方法
方法1静态分配
pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;
方法2动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict
attr);
mutex要初始化的互斥量
attrNULL
销毁互斥量
销毁互斥量需要注意
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量已经销毁的互斥量要确保后面不会有线程再尝试加锁。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调用 pthread_ lock 时可能会遇到以下情况:
互斥量处于未锁状态该函数会将互斥量锁定同时返回成功
发起函数调用时其他线程已经锁定互斥量或者存在其他线程同时申请互斥量但没有竞争到互斥量那么pthread_ lock调用会陷入阻塞(执行流被挂起)等待互斥量解锁。
对互斥量加锁后就能改进抢票系统了。
#include iostream
#include pthread.hpp#include cstdio
#include unistd.hclass ThreadData
{
public:ThreadData(const std::string name, pthread_mutex_t* pmutex):_ThreadName(name),_mutex(pmutex){}
public:std::string _ThreadName;pthread_mutex_t* _mutex;
};
std::string GetThreadName()
{static int Number 0;char buffer[64];snprintf(buffer, sizeof(buffer), thread-%d, Number);return buffer;
}
void Print(int i)
{std::cout i std::endl;
}
int ticket 10000;
void GetTicket(ThreadData* td)
{while(true){pthread_mutex_lock(td-_mutex);if(ticket 0){usleep(10);printf(%s get a ticket %d\n,td-_ThreadName.c_str(),ticket);ticket--; pthread_mutex_unlock(td-_mutex);}else {pthread_mutex_unlock(td-_mutex);break;}}
}
int main()
{pthread_mutex_t* mutex new pthread_mutex_t;pthread_mutex_init(mutex, nullptr);std::string name1 GetThreadName();ThreadData td1(name1, mutex);ThreadThreadData* th1(name1, GetTicket, td1);std::string name2 GetThreadName();ThreadData td2(name2, mutex);ThreadThreadData* th2(name2, GetTicket, td2);std::string name3 GetThreadName();ThreadData td3(name3, mutex);ThreadThreadData* th3(name3, GetTicket, td3);std::string name4 GetThreadName();ThreadData td4(name4, mutex);ThreadThreadData* th4(name4, GetTicket, td4);th1.Start();th2.Start();th3.Start();th4.Start();th1.Join();th2.Join();th3.Join();th4.Join();return 0;
}
这样当这段公共区代码被加锁后解锁前其他的线程就不能访问这段代码了 线程互斥实现原理 其实总结为一句话当一个线程拿到开锁的钥匙之后这个钥匙就通过 swap 变为了这个线程自己的上下文就算线程被切换了钥匙就被线程用上下文带走了其他的线程依然无法打开这把锁
使用系统加锁接口封装LockGuard 实现自动化加锁
#include pthread.h
class Mutex
{public:Mutex(pthread_mutex_t* lock):_lock(lock){}void Lock(){pthread_mutex_lock(_lock);}void UnLock(){pthread_mutex_unlock(_lock);}~Mutex(){}private:pthread_mutex_t* _lock;
};class LockGuard
{public:LockGuard(pthread_mutex_t* lock):_mutex(lock){_mutex.Lock();}~LockGuard(){_mutex.UnLock();}private:Mutex _mutex;
}; 只需要在使用时定义一个 LockGuard 对象就不在用加锁和解锁了作用有点像智能指针通过对象的建立和销毁来控制加锁和解锁
线程安全和可重入函数
线程安全
多个线程并发同一段代码时不会出现不同的结果。常见对全局变量或者静态变量进行操作并且没有锁保护的情况下会出现该问题。
可重入
同一个函数被不同的执行流调用当前一个流程还没有执行完就有其他的执行流再次进入我们称之为重入。一个函数在重入的情况下运行结果不会出现任何不同或者任何问题则该函数被称为可重入函数否则是不可重入函数。
可重入与线程安全联系
函数是可重入的那就是线程安全的函数是不可重入的那就不能由多个线程使用有可能引发线程安全问题如果一个函数中有全局变量那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
可重入函数是线程安全函数的一种线程安全不一定是可重入的而可重入函数则一定是线程安全的。如果将对临界资源的访问加上锁则这个函数是线程安全的但如果这个重入函数若锁还未释放则会产生死锁因此是不可重入的。
常见线程安全和可重入情况
常见线程安全情况
每个线程对全局变量或者静态变量只有读取的权限而没有写入的权限一般来说这些线程是安全的类或者接口对于线程来说都是原子操作多个线程之间的切换不会导致该接口的执行结果存在二义性。
常见可重入的情况
不使用全局变量或静态变量不使用用malloc或者new开辟出的空间不调用不可重入函数不返回静态或全局数据所有数据都有函数的调用者提供使用本地数据或者通过制作全局数据的本地拷贝来保护全局数据
死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
死锁四个必要条件
互斥条件一个资源每次只能被一个执行流使用请求与保持条件一个执行流因请求资源而阻塞时对已获得的资源保持不放不剥夺条件:一个执行流已获得的资源在末使用完之前不能强行剥夺循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
要破坏死锁只需要破坏死锁的几个必要条件之一即可
避免死锁在写代码时加锁顺序一致尽量避免锁未释放的场景尽量资源一次性分配。 资源一次性分配One-time Allocation of Resources是指在程序运行过程中某个特定的资源只会被分配一次并在之后不会再次分配。这种分配方式意味着一旦资源被分配给了一个实体如进程或线程它将一直保持分配状态直到被释放。 还有一些可以避免死锁的算法如死锁检测算法、银行家算法
银行家算法简介
银行家算法是一种用于避免死锁的资源分配算法它是由Edsger W. Dijkstra在1965年提出的。银行家算法的目标是通过合理的资源分配来避免系统陷入死锁的状态。
银行家算法基于以下假设
每个进程在开始执行之前必须声明其最大资源需求量。系统中的资源数量是固定的。系统能够检测到每个进程的资源请求和释放情况。
算法步骤
初始化为每个进程分配它所需要的最大资源量、已分配资源量和还需要资源量。同时初始化系统的可用资源量。请求资源当一个进程需要申请一定数量的资源时首先检查系统的可用资源是否大于等于请求资源的数量如果是则进一步检查分配给该进程资源后是否仍然能够避免死锁。如果是则分配资源给该进程更新系统的可用资源量和进程的已分配和还需资源量。如果否则进程需要等待。执行任务当一个进程执行完毕后释放所有已分配资源并将这些资源回收到系统的可用资源池中。检查安全性在每次资源请求和释放之后系统需要进行安全性检查判断系统是否处于安全状态。如果系统处于安全状态则继续执行下一个进程的资源请求否则进程需要等待。
银行家算法的核心思想是每个进程在申请资源时系统需要先判断是否能够保证分配资源后系统仍然处于安全状态。如果无法保证安全性则不分配资源给进程以避免死锁的发生。
银行家算法的优点是能够有效地避免死锁的发生缺点是需要事先知道每个进程的最大资源需求量且需要保持资源分配表的实时更新。同时该算法可能导致资源利用率较低因为只有当系统处于安全状态时才会分配资源给进程。