> 作者:დ旧言~
> 座右铭:松树千年终是朽,槿花一日自为荣。
> 目标:理解【Linux】线程安全——补充|互斥、锁|同步、条件变量。
> 毒鸡汤:有些事情,总是不明白,所以我不会坚持。早安!
> 专栏选自:Linux初阶
> 望小伙伴们点赞👍收藏✨加关注哟💕💕
🌟前言
每建造一个建筑物时,我们都需要考虑这个建筑物的安全问题,为每一人的安全做个保证,在Linux下的线程其实也有安全隐患,那我们该如何维护线程的安全呢?本篇博客带大家了解Linux下的线程安全。
⭐主体
学习【Linux】线程安全——补充|互斥、锁|同步、条件变量咱们按照下面的图解:
🌙 知识补充
💫 线程的ID
概念:
pthread_create创建一个线程,产生一个线程ID存放在第一个参数之中,该线程ID与内核中的LWP并不是一回事。pthread_create函数第一个参数指向一块虚拟内存单元,该内存单元的地址就是新创建线程ID,这个ID是线程库的范畴,而内核中LWP是进程调度的范畴,轻量级进程是OS调度的最小单位,需要一个数值来表示该唯一线程。
Linux并不提供真正的线程,只提供了LWP,但是程序员用户不管LWP,只要线程。所以OS在OS与应用程序之间设计了一个原生线程库,pthread库,系统保存LWP,原生线程库可能存在多个线程,别人可以同时在用。OS只需要对内核执行流LWP进行管理,而提供用户使用的线程接口等其他数据则需要线程库自己来管理。所以线程库需要对线程管理“先描述,在组织”。
线程库实际上就是一个动态库:
进程运行时动态库加载到内存,然后通过页表映射到进程地址空间的共享区,这时候进程的所有线程都是能看到这个动态库的:
每个线程都有自己独立的栈:主线程采用的栈是进程地址空间中原生的栈,其他线程采用的是共享区中的栈,每个线程都有自己的struct pthread,包含了对应线程的属性,每个线程也有自己的线程局部存储(添加__thread可以将一个内置类型设置为线程局部存储),包含对应的线程被切换时的上下文。每一个新的线程在共享区都有一块区域对其描述,所以我们要找到一个用户级线程只需要找到该线程内存块的起始地址就可以获取到该线程的信息了:
线程函数起始是在库内部对线程属性进行操作,最后将要执行的代码交给对应的内核级LWP去执行。所以线程数据的管理本质在共享区。线程ID本质就是进程地址空间共享区上的一个虚拟地址:
#include <stdio.h> #include <bits/pthreadtypes.h> #include <pthread.h> #include <unistd.h> void* start_routine(void*args) { while(true) { printf("new thread tid:%p\n",pthread_self()); sleep(2); } } int main() { pthread_t tid; pthread_create(&tid,nullptr,start_routine,nullptr); while(true) { printf("main thread tid:%p\n",pthread_self()); } return 0; }
💫 局部存储验证
给一个全局变量g_val,让一个线程进行++,其他线程会受到影响:
#include <iostream> #include <pthread.h> #include <unistd.h> using namespace std; int g_val = 100; void* start_routine(void*args) { string name = static_cast<const char*>(args); while(true) { cout<<name<<" running ... "<<"g_val: "<<g_val<<"&g_val: "<<&g_val<<endl; sleep(1); ++g_val; } } int main() { pthread_t tid; pthread_create(&tid,nullptr,start_routine,(void*)"thread 1"); while(true) { printf("main thread g_val: %d &g_val: 0x%x\n",g_val,&g_val); sleep(1); } pthread_join(tid,nullptr); return 0; }
此时在给全局变量g_val加上__thread:
从输出结果上看,g_val此时就不再是共享了,每个线程独有。添加__thread
可以将一个内置类型设置为线程局部存储。每个线程都有一份,介于全局变量和局部变量之间线程特有属性。
💫 Thread.hpp——线程的封装
我们如果想要跟C++一样使用,创建使用线程时,直接构造对象设置回调函数,对线程原生接口可以进行简单的封装:
mythread.cc:
#include <iostream> #include <pthread.h> #include <cassert> #include <cstring> #include <functional> class Thread; //上下文 class Context { public: Thread *this_; void *args_; public: Context():this_(nullptr),args_(nullptr) {} ~Context() {} }; class Thread { public: typedef std::function<void*(void*)> func_t; const int num = 1024; public: Thread(func_t func,void*args,int number):func_(func),args_(args) { char buffer[num]; snprintf(buffer,sizeof(buffer),"thread-%d",number); name_=buffer; Context*ctx = new Context(); ctx->this_ = this; ctx->args_=args_; int n = pthread_create(&tid_,nullptr,start_routine,ctx); assert(n==0); (void)n; } static void*start_routine(void*args) { Context*ctx = static_cast<Context*>(args); void*ret = ctx->this_->run(ctx->args_); delete ctx; return ret; } void join() { int n = pthread_join(tid_,nullptr); assert(n==0); (void)n; } void*run(void*args) { return func_(args); } ~Thread() {} private: std::string name_; pthread_t tid_; func_t func_; void* args_; };
main.cc:
#include <iostream> #include <pthread.h> #include <unistd.h> #include <memory> #include "mythread.cc" using namespace std; void* thread_run(void* args) { std::string name = static_cast<const char *>(args); while(true) { cout << name << endl; sleep(1); } } int main() { std::unique_ptr<Thread> thread1(new Thread(thread_run,(void*)"hellothread",1)); std::unique_ptr<Thread> thread2(new Thread(thread_run,(void*)"COUTthread",2)); std::unique_ptr<Thread> thread3(new Thread(thread_run,(void*)"PRINTthread",3)); //thread1->start(); //thread2->start(); //thread3->start(); thread1->join(); thread2->join(); thread3->join(); return 0; }
🌙 线程安全问题
线程安全:
在多个执行流中对同一个临界资源进行操作访问, 而不会造成数据二义性,也就是说, 在拥有共享数据的多条线程并行执行的程序中, 通过同步和互斥机制保证各个线程都可以正常且正确的执行, 不会出现数据污染等意外情况, 也就是保证了线程安全。
下面模拟抢票的过程,多个线程对共享资源tickets做–的过程:
#include<iostream> #include<cstdio> #include<cstring> #include<unistd.h> #include<pthread.h> using namespace std; int ticket = 30;//票数 void* route(void *arg){ while(ticket > 0){ usleep(100); --ticket; printf("第%d位黄牛抢到票, 还剩%d张\n", *(int*)arg, ticket); } return NULL; } int main(){ pthread_t tid[5]; int n[5] = {1,2,3,4,5}; for(int i = 0; i < 5; ++i){ int ret = pthread_create(&tid[i], NULL, route, n + i); if(ret){ fprintf(stderr, "pthread_create:%s\n", strerror(ret)); } } for(int i = 0; i < 5; ++i){ pthread_join(tid[i], NULL); } return 0; }
此时结果出现了负数,在现实生活中,抢票怎么可能出现负数
结果出现负数:
这是因为, 当多个线程同时对一个临界资源访问时, 就有很大的可能会出现数据二义性的问题, 就比如上面的例子, 当还有最后一张票时, 第一个黄牛的线程判断ticket > 0 进入循环抢票, 但此时如果ticket还没有减一, 这个线程就被挂起等待, 操作系统给CPU调度了其他的黄牛线程, 此时其他线程判断ticket > 0, 也就入了抢票循环, 这就出问题了, 同一张票, 本该抢完就结束的, 但这样一来就会出现上面例子问题发生, 此时线程是不安全的. 总结一下就是
- while判断后, 可以并发的切换到其他线程
- usleep模拟一段业务过程, 在这段过程中, 可能会并发多个线程也运行到这段代码
- --ticket本身就不是一个原子操作如果调试来看的话, --ticket一个语句要对应三条汇编指令
而解决这种问题的办法就是加锁!(后面详细介绍这个内容)
【Linux】线程安全——补充|互斥、锁|同步、条件变量(下) https://developer.aliyun.com/article/1565760