Linux基础内容(25)—— 线程控制和线程结构_哈里沃克的博客-CSDN博客https://blog.csdn.net/m0_63488627/article/details/131372872?spm=1001.2014.3001.5501
目录
1.线程互斥
1.问题引入
2.问题原因
3.安全问题
互斥
加锁
加锁后的特点
如何理解锁
原子性的理解
加锁原则
4.互斥锁的原理
5.锁的封装
2.线程安全与可重入函数
1.重入
1.不可重入
2.可重入
2.线程安全问题情况
1.线程不安全
2.线程安全
3.线程安全与可重入函数关系
4.死锁
1.死锁的必要条件
2.破坏死锁的条件
1.线程互斥
1.问题引入
我们设计一个类似的购票程序,其思路就是有10000个ticket,我们生成几个线程进行抢票。如果购票到0,说明此时没有票。但是如果执行下面的逻辑进行抢票,那么会出现抢到负值。这就是当前线程的问题。
int tickets = 10000; void *get_tickets(void *args) { std::string username = static_cast<const char *>(args); while (true) { if (tickets > 0) { usleep(1234); // 模拟真实抢票花费时间 std::cout << username << "正在抢票:" << tickets << std::endl; tickets--; } else break; } return nullptr; }
当然这个设计有一些前提:
为了出现我们想要的问题,就需要尽可能的让多个线程交叉执行,多个线程交叉执行本质:就是让调度器尽可能的频繁发生线程调度与切换,线程一般在什么时候发生切换呢?时间片到了,来了更高优先级的线程,线程等待的时候。线程是在什么时候检测上面的问题呢?从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以,就直接发生线程切换
2.问题原因
1.其实当ticket为1时,多个进程能同时进入函数进行判断。这是因为当进行判断时,cpu会读取内存中的数据,随后再判断。
2.cpu如果只有一个,那么其实多个线程进行判断后,就不能进入到判断内部,因为线程和cpu处理一一对应。
3.那么我们能知道真正错误的原因是:当一个线程usleep后,线程的上下文就会被存储挂起。那么多个线程进入后,执行usleep语句进行挂起后,这些线程都认为自己存储的ticket为1。随这usleep时间到了,那么此时多线程的执行语句仍然在函数中,那么将内存当前的ticket取出,随后对ticket进行-1。那么以此往复,就出现了负数的ticket。
3.安全问题
线程的共享变量进行++/--操作时,其实只是C语言层面只有一行指令,但是对标CPU其实至少有三条:1.将内存的数据放入cpu 2.cpu中的数据进行逻辑算数运算 3.将运算得到的数据返回给内存 。那么上面的问题产生就是因为2和3操作之间,线程切换了,cpu的资源就被挂起。也就是说一旦一个线程挂起,另一个线程执行当前的共享资源。随这另一个线程完成,挂起的线程苏醒后操作就会出现所谓的安全问题。
互斥
1.多个执行流进行安全访问的共享资源为临界资源
2.我们把多个执行流访问临界资源的代码为临界区
3.想要多个线程串行访问共享资源的操作就是互斥
4.如果一个资源进行访问,要么做就做完,要么不做,该性质被称为原子性
加锁
当前的问题解决为加锁
pthread_mutex_init:局部锁的创建
如果锁位全局或者静态的,初始化直接pthread_mutex_t的变量初始化为PTHREAD_MUTEX_INITALIZER
如果是局部的,需要最后进行pthread_mutex_destory
pthread_mutex_lock:加锁
pthread_mutex_unlock:解锁
int tickets = 10000; pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; void *get_tickets(void *args) { std::string username = static_cast<const char *>(args); while (true) { pthread_mutex_lock(&lock); if (tickets > 0) { usleep(1234); // 模拟真实抢票花费时间 std::cout << username << "正在抢票:" << tickets << std::endl; tickets--; pthread_mutex_unlock(&lock); } else { pthread_mutex_unlock(&lock); break; } } return nullptr; }
只不过当前的执行流并不是轮换进行的。
加锁后的特点
1.加锁和解锁的过程是多个线程串行执行的,以至于代码的执行速度变低
2.锁只规定互斥访问,并没有规定让谁优先执行,所以锁直接就是竞争关系
如何理解锁
1.锁需要被所有的线程看见才能使用,那么锁的使用也就意味着锁是共享资源
2.锁是用于保护线程安全保护全局资源的,那么锁也理所应当要被保证安全,加锁也必须是安全的
3.加锁的过程是原子性的,要加锁则要么一定成功,要么一定失败
4.如果线程在申请加锁时暂时失败,那么我们的执行流会被阻塞,直到锁被释放,并且该线程得到锁,随后继续进行执行流的工作。谁持有锁,谁进入临界区
原子性的理解
1.原子性就是只有两种执行的结果,要么做就做完,要么不做
2.其实有一种很直观的原子性代码,就是站在cpu角度的编译代码只有一行的,轻易体现出要做就做完的原子性原则
3.相互竞争锁的线程来说,当前一个线程申请锁成功,那么也就意味着其他竞争的线程只能被挂起等待处在阻塞状态;当然申请锁成功的线程也可以随时被cpu切换走,不过此时的线程会将锁一起带走被切换,那么对应竞争的线程而言,依然没有锁,依然阻塞等待,直到申请成功的线程释放锁。对于其他竞争的线程而言,其实只有两态:有锁/没有锁。那么对于其他的线程而言其实也是原子性的。
加锁原则
1.加锁的粒度要低,优化时间性能
2.加锁必须一视同仁,而不是一部分加锁一部分不加针对同一份公共资源
4.互斥锁的原理
1.加锁的本质其实是原子性的
2.在编译中,存在一条swap或者exchange语句,只需要这一条就可以将cpu的寄存器里的值与内存中的值相互转换
3.寄存器只有一套,被所有执行流共享的。cpu的执行流在cpu的寄存器中的内容是私有的,称为运行时的上下文
4.加锁:线程进入时,线程会将自己的锁状态填入cpu的寄存器,随后要加锁时,直接swap或者exchange一条汇编语句交换,此时如果内存中有锁,那么交换后,线程就将锁纳入了线程的上下文。如果此时断开,线程也会将锁一起带走挂起,那么其他线程进行相同的载入和拿取时判断直到锁并没有申请成功,自然线程不会进行下面的操作,使得原子性得到体现
5.解锁:就是将锁归还到内存,这样就使得其他的线程能调度锁了
5.锁的封装
// RAII风格的加锁 class Mutex { public: Mutex(pthread_mutex_t *lock_p) :lock_p_(lock_p) {} void lock() { if(lock_p_) pthread_mutex_lock(lock_p_); } void unlock() { if(lock_p_) pthread_mutex_unlock(lock_p_); } ~Mutex() {} private: pthread_mutex_t *lock_p_; }; class LockGuard { public: LockGuard(pthread_mutex_t *mutex) :mutex_(mutex) { mutex_.lock(); //构造函数中进行加锁 } ~LockGuard() { mutex_.unlock(); //析构函数后自动解锁 } private: Mutex mutex_; };
void *get_tickets(void *args) { ThreadData *td = static_cast<ThreadData *>(args); while (true) { LockGuard lockguard(td->mutex_p_); { if (tickets > 0) { usleep(1234); // 模拟真实抢票花费时间 std::cout << td->threadname_ << "正在抢票:" << tickets << std::endl; tickets--; //pthread_mutex_unlock(td->mutex_p_); } else { //pthread_mutex_unlock(td->mutex_p_); break; } } // 抢票完了还有其他工作 usleep(1000); // 线程生成订单 } return nullptr; }
2.线程安全与可重入函数
1.重入
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数
1.不可重入
1.调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
2.调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
3.可重入函数体内使用了静态的数据结构2.可重入
1.不使用全局变量或静态变量
2.不使用用malloc或者new开辟出的空间
3.不调用不可重入函数
4.不返回静态或全局数据,所有数据都有函数的调用者提供
5.使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
2.线程安全问题情况
1.线程不安全
1.不保护共享变量的函数
2.函数状态随着被调用,状态发生变化的函数
3.返回指向静态变量指针的函数
4.调用线程不安全函数的函数2.线程安全
1.每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
2.类或者接口对于线程来说都是原子操作
3.多个线程之间的切换不会导致该接口的执行结果存在二义性
3.线程安全与可重入函数关系1.是否为可重入函数针对函数,线程是否安全针对线程,其实二者并没有直接的关系。线程安全一定是必须保证的,而可重入函数需要根据需求进行设计
2.函数是可重入的,那就是线程安全的;函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题;如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
3.可重入函数是线程安全函数的一种
4.线程安全不一定是可重入的,而可重入函数则一定是线程安全的
5.如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的
4.死锁
1.在线程之间存在多把锁的情况下,不同的线程申请成功不同的锁,一旦互相之间需要申请对方的锁。那么此时就会出现线程互相等待对方的锁,造成双方都挂起,进而存在死锁的问题。
2.一个线程申请成功一个锁,此后忘记自己存在锁,随后又申请同样的锁,那么此时自己等待自己释放锁的奇怪现象出现导致单一线程的单一锁出现死锁问题。
1.死锁的必要条件
1.互斥 -- 锁的基本条件
2.请求与保持 -- 线程自己需要锁但是不会释放线程自己的锁
3.不剥夺 -- 需要线程自愿释放自己的锁
4.环路等待条件 -- 线程互现申请成功锁并且需要对方的锁
2.破坏死锁的条件
1.互斥不允许破坏
2.破坏请求与保持,只需要在申请另一个锁失败时,自动释放原先自己拥有的锁
3.破坏不剥夺,根据优先级来剥夺优先级低的线程的锁
4.破坏环路等待,防止环路的语句