如何批量入侵WordPress站,做服装网站,常州网站建设系统,沈阳网站搜索排名目录
一、线程不安全
1.线程不安全现象
2.线程不安全程序的特质
3.线程不安全程序的原因
二、线程互斥
1.基本概念
2.锁
#xff08;1#xff09;认识锁
#xff08;2#xff09;互斥锁的使用
#xff08;3#xff09;代码的改造
3.锁的本质
#xff08;11认识锁
2互斥锁的使用
3代码的改造
3.锁的本质
1加锁对线程的影响
2锁的原理
4.封装锁
三、重入和线程安全的理解
1.正确认识重入
1认识重入
2认识可重入
2.正确认识线程安全
3.可重入与线程安全的联系
四、死锁
1.四个必要条件
2.避免死锁 一、线程不安全
1.线程不安全现象
我们都有在12306上抢票的经历吧毕竟一打开满眼的候补着实是血压高了。
那我们也编写一个简单的抢票程序设置全局变量tickets10表示一共有十张票。创建五个线程每一个线程代表一个抢票者。五个抢票者不断抢票抢到票后tickets减一并显示当前余票。
#includeiostream
#includepthread.h
#includeunistd.h
#includestdio.h
#includevector
using namespace std;#define NUM 5int tickets 10;class pthread_data
{
public:pthread_t tid;char buffer[64];
};void* start_routine(void* args)
{pthread_data* p (pthread_data*)args;string s;s p-buffer;s Remaining tickets:;while(1){if(tickets 0){sleep(1);--tickets;printf(%s%d\n, s.c_str(), tickets);}elsebreak;}pthread_exit(nullptr);
}int main()
{vectorpthread_data* vpd;for(int i 0; iNUM; i){pthread_data* pd new pthread_data;snprintf(pd-buffer, sizeof(pd-buffer), thread:%d buy ticket:,i1);pthread_create((pd-tid), nullptr, start_routine, (void*)pd);vpd.push_back(pd);}for(auto e : vpd){pthread_join(e-tid, nullptr);delete e;}return 0;
}
运行结果 按道理说票减到0线程就都应该退出了不应该出现余票为负的情况。
2.线程不安全程序的特质
线程不安全的程序一般都有这个特质多个线程交叉执行换句话说就是调度器频繁发生线程调度与切换。
虽然我们看上去所有线程都在同时执行其实线程也是同时只能执行一个单核只是因为CPU运行速度太快人是观察不到的。
对于线程切换有以下细节
线程达到时间片更高优先级线程需要执行线程等待时会发生线程切换。线程切换的检测是以内核态身份进行的访问的是地址空间的内核部分本质是操作系统在检测。线程在从内核态转为用户态时会检测是否达到线程切换条件。
CPU负责调度这些执行流在一个线程达到被切换的条件时CPU就会与该线程分离并执行另一个线程当再次轮到这个被切走的线程后才会继续执行。
如果CPU不停切换线程一个线程执行一半就接着执行下一个去了这样的交叉执行就会导致线程不安全的问题。
3.线程不安全程序的原因
首先语句的执行都需要先把变量从内存读取到寄存器然后在寄存器内进行处理最后再覆盖到内存中。根据这样的思想我们就可以试着解释上面的票数为什么会出现负数。
我们假设ticket1依旧有五个线程抢票。
第一阶段
首先内存中储存的ticket为1。第一个线程thread1开始执行判断ticket0。CPU从内存中将tickets的数值1读取到ebx寄存器内tickets确实大于0将1写回内存执行内部语句。
当线程要执行sleep时线程1会被切走。由于CPU和寄存器从只有一套所以它的上下文数据会被保存起来。
接着第二个线程thread2同样从内存中读取到tickets为1判断为真再次被切走。
当然还有thread3、thread4、thread5都会经历这样的过程。 第二阶段
我们首先要知道--tickets需要三步才能完成包括读取数据到寄存器寄存器数据减一将寄存器数据写回内存。
此时每一个线程都进入了if语句框线程thread1再次被CPU执行。此时内存中的tickets为1寄存器ebx读取数据变为1。此时执行--tickets寄存器内数据变为0最后将0写回到内存的tickets中。
thread2线程也被再次唤醒再次读取tickets为0减一得到-1再将-1写回内存中。
后面的thread3、thread4、thread5也是这样的流程最后内存中的tickets经过五次减一变成了-4这就出现了负数。 二、线程互斥
1.基本概念
临界资源多个执行流进行安全访问的共享资源。
上面的tickets就不是临界资源因为多线程对它的访问出现了问题。
临界区多个执行流中访问临界资源的代码。
上面只有部分代码属于一部分临界区对tickets进行if判断打印减一的那部分代码属于临界区。
互斥让多个线程串行访问共享资源任何时候只有一个执行流在访问共享资源。
上面的代码如果将多进程交叉并行变为串行就不会出现进程不安全的情况。让共享资源变成临界资源其实就是实现互斥。
原子性对一个资源进行访问的时候要么不做要么就做完。
在前面也说过像加加和减减--这样的操作看似只有一条代码但是它对应的汇编指令有3条也就是说这个操作不能一次完成。
现在的我们可以认为对资源进行操作如果只用一条汇编就能完成那么就说该操作具有原子性。这只是原子性表述的其中一种
2.锁
1认识锁
要想解决多线程的数据不一致问题需要做到以下几点
代码必须有互斥行为当一个线程进入临界区执行代码时不允许其他线程进入该临界区。如果有多个线程同时请求执行临界区代码并且临界区没有线程在执行代码那么只允许一个线程进入该临界区。如果线程不在临界区中执行代码那么该线程也不能阻止其他线程进入临界区。
其实做到上面三点只需要一把互斥锁你可以将锁看作一个通行证持有锁的线程才能进入临界区中执行代码其他线程不持有锁无法进入该临界区。
加锁本质就是让共享资源临界资源化多个线程串行访问共享资源从而保护共享资源的安全。
互斥锁本质上就是一个类class pthread_mutex_t可以构造对象pthread_mutex_t mutxmutx就是互斥锁对象。
2互斥锁的使用
以下是锁的一些成员函数和使用代码
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
头文件pthread.h
功能初始化互斥锁。
参数pthread_mutex_t *restrict mutex表示需要被初始化的锁的地址const pthread_mutexattr_t *restrict attr表示锁的属性一般都为nullptr。
返回值取消成功返回0取消失败返回错误码。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
头文件pthread.h
功能销毁互斥锁。
参数pthread_mutex_t *mutex表示需要被销毁的锁的地址。
返回值销毁成功返回0失败返回错误码。
pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER;
如果是全局或static修饰的锁使用上面语句初始化锁。
int pthread_mutex_lock(pthread_mutex_t *mutex);
头文件pthread.h
功能对lock到unlock的部分代码加锁仅允许线程串行。
参数pthread_mutex_t *mutex表示需要加锁的锁指针。
返回值加锁成功返回0失败返回错误码。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
头文件pthread.h
功能标识走出lock到unlock的部分代码解锁恢复并发。
参数pthread_mutex_t *mutex表示需要解锁的锁指针。
返回值加锁成功返回0失败返回错误码。
其实加锁和解锁可以圈定临界区的范围临界区内的代码只允许同一时间有一个线程执行内部代码只有该线程退出后才允许另一个线程执行该部分代码外部的代码依旧允许并行。
我打个比方的话就像只容许一个人公共厕所厕所只能进一个人必须等里面的人使用完毕后另一个人才能进去而外面的公共空间不受管制。
pthread_mutex_t lock PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(lock);
//临界区
//...
pthread_mutex_unlock(lock);
3代码的改造
对线程加锁需要做两件事让所有线程都看到同一把锁所有线程都使用同一把锁。
我们如果将锁定义在main函数内那么只有主线程能看到该锁。所以我们在Thread类中增加一个锁指针这时所有的线程就能使用同一把锁了。
#includeiostream
#includepthread.h
#includeunistd.h
#includestdio.h
#includevector
using namespace std;#define NUM 5int tickets 10;class pthread_data
{
public:pthread_t tid;char buffer[64];pthread_mutex_t* pmtx;//锁指针
};void* start_routine(void* args)
{pthread_data* p (pthread_data*)args;string s;s p-buffer;s Remaining tickets:;while(1){pthread_mutex_lock(p-pmtx);//加锁if(tickets 0){sleep(1);--tickets;pthread_mutex_unlock(p-pmtx);//解锁printf(%s%d\n, s.c_str(), tickets);//不修改临界资源可以不包含在内}else{pthread_mutex_unlock(p-pmtx);//解锁break;}}pthread_exit(nullptr);
}int main()
{vectorpthread_data* vpd;pthread_mutex_t mutx;//创建锁pthread_mutex_init(mutx, nullptr);//初始化for(int i 0; iNUM; i){pthread_data* pd new pthread_data;pd-pmtx mutx;snprintf(pd-buffer, sizeof(pd-buffer), thread%d buy ticket:,i1);pthread_create((pd-tid), nullptr, start_routine, (void*)pd);vpd.push_back(pd);}for(auto e : vpd){pthread_join(e-tid, nullptr);delete e;}return 0;
}
注意加锁和解锁的区域只需要覆盖住对临界资源进行操作的代码不要覆盖太大。
还有一定要注意解锁一定要覆盖到代码的执行路径比如抢票代码else中如果没有解锁那下面的所有执行就都加锁了会对代码运行造成巨大影响。 再次运行程序正常运行并退出只是由于串行程序的运行时间加长了。 因为正常的抢票往往是很短时间内就会有许多人访问我们将sleep1换成usleep1000缩短睡眠时间。 我们会发现大部分的票都被一个线程抢走了。 实际上锁只保证互斥访问不管执行线程的顺序。
thread5抢的多说明该线程的竞争能力强别的线程打不过它。
现在的抢票逻辑是抢到票解锁以后该线程又直接去申请锁这就导致了之前持有锁的线程更加容易再次申请到锁。
咱们再12306抢票成功后也不可能立刻再去抢程序还需要做打印订单等等工作所以我们在线程执行最后也睡眠一会儿。 这次就是正常的你来我往了。 3.锁的本质
1加锁对线程的影响
锁必须让所有线程都看到所以锁本身就是共享资源。那谁来保护锁的安全呢
锁是通过加锁和解锁操作的原子性来保证自身的安全的。
一个线程如果成功申请锁那么它就会继续向下执行如果申请不成功呢 我们发现进程线程都还在但线程卡住了。 一个锁只能被申请一次只有锁被释放才能再次申请。当一个线程申请锁失败它就会阻塞不动。
所以我们此时就能理解CPU排队处理线程和串行的关系了
当一个线程申请锁成功进入临界区访问临界资源其他线程要想进入临界区只能阻塞等待等待该进程将锁释放。当一个线程申请锁成功进入临界区访问临界资源在满足条件时也是可以被换下CPU的。而且锁还在该线程的受力其他线程仍然无法申请锁成功。操作系统内不存在锁的概念所以调度器在调度轻量级进程的时候并不会考虑是否有锁。如果调度到了没有锁的进程不进行处理就可以了。
所以站在其他线程的角度锁只有两种状态申请锁前和申请锁后。
站在其他线程的角度当前持有锁的过程就是原子的。
2锁的原理
为了保证加锁的原子性在大多数体系结构都提供了swap或者xchange汇编指令保证加锁只需要一条汇编指令。
下面是加锁和解锁的伪代码xchange是原子的
lock:movb %al, $0//将0写入al寄存器中xchange %al, mutex//将al寄存器的内容与锁的成员变量1交换if(al寄存器的内容 0){return 0;}else{挂起等待; }goto lock;unlock:movb mutex, $1唤醒等待mutex的线程;return 0;在CPU中有一个al寄存器它也是锁的能正常运行的保证之一。
假设有两个线程每个线程中都有加锁的代码。
首先CPU开始处理线程thread1thread2等待被处理。由于thread1是第一次被处理此时需要向al寄存器内写入0。 当线程thread1执行到加锁代码时由于内存中的锁变量储存了一个变量1所以al寄存器会与内存中的这个变量进行数据交换。 在执行临界区代码时很可能thread1还没有解锁该线程就被换下去了。但是CPU和寄存器只有一套那么上下文数据就必须保存后由线程带走同样al寄存器里的1就也被带走了。
当thread2也是初次执行需要在al寄存器写入0。然后同样将al寄存器会与内存中的这个变量进行数据交换但此时锁变量也是00和0交换完还是0。
由于交换后al寄存器内容不大于0所以该线程申请不到锁只能挂起等待。 由于thread2被挂起所以thread1再次被执行此时它的上下文数据被加载回寄存器al寄存器数据为1线程继续运行。
当thread1完成了临界区代码执行就需要将al寄存器的1还回给锁变量thread1的al寄存器重新变回0。然后唤醒等待锁的线程thread2thread1又被挂起。 经过上面过程的描述我们不难发现发现
锁只能被一个线程持有而且由于加锁是一条xchange汇编代码操作是原子性的也不需要担心线程切换的事情。一旦一个线程申请到锁因为即使该线程被切走锁还是在它的上下文数据中。所以其他线程无法拿到锁只能挂起等待只有等锁被释放时才能申请。锁的工作本质上就是锁类变量中的一个标志位1在不同进程间传递的过程只有申请到该标志位或者说持有锁的线程才能执行。形象地说利用锁达到线程串行类似于很多人抢一张入场券。释放锁的过程对原子性的要求不高因为只有持有锁的线程才能释放锁未申请到锁的线程都在挂起。
4.封装锁
锁的成员函数名普遍偏长也不方便使用不如我们自己将锁封装自己使用也方便。
mutex.h
#includepthread.h
class mutex
{
public://构造函数mutex(pthread_mutex_t* p nullptr):_pmutx(p){}//加锁void lock(){pthread_mutex_lock(_pmutx);}//解锁void unlock(){pthread_mutex_unlock(_pmutx);}
private:pthread_mutex_t* _pmutx;
};class LockGuard//这个类型变量的构造和销毁就可以执行加解锁
{
public:LockGuard(pthread_mutex_t* lock_p):_mutex(lock_p){_mutex.lock();//构造函数内加锁}~LockGuard(){_mutex.unlock();//析构函数内解锁}
private:mutex _mutex;
};
我们使用这个封装的锁修改代码。
#includeiostream
#includemutex.h
#includeunistd.h
#includestdio.h
#includevector
using namespace std;#define NUM 5pthread_mutex_t mutx PTHREAD_MUTEX_INITIALIZER;//构建一个全局锁
int tickets 10;class pthread_data
{
public:pthread_t tid;char buffer[64];
};void* start_routine(void* args)
{pthread_data* p (pthread_data*)args;string s;s p-buffer;s Remaining tickets:;while(1){{LockGuard lock(mutx);//左侧的语句块标识这个lock变量的生命周期//构造函数加锁走出代码块时该变量的声明周期结束执行析构函数解锁//这样的加锁模式也叫做RAII加锁if(tickets 0){usleep(1000);--tickets;printf(%s%d\n, s.c_str(), tickets);//不修改临界资源可以不包含在内}else{break;}}usleep(1000);}pthread_exit(nullptr);
}int main()
{vectorpthread_data* vpd;for(int i 0; iNUM; i){pthread_data* pd new pthread_data;snprintf(pd-buffer, sizeof(pd-buffer), thread%d buy ticket:,i1);pthread_create((pd-tid), nullptr, start_routine, (void*)pd);vpd.push_back(pd);}for(auto e : vpd){pthread_join(e-tid, nullptr);delete e;}return 0;
}
正确运行 三、重入和线程安全的理解
1.正确认识重入
1认识重入
同一个函数被不同的执行流调用当前一个流程还没有执行完就有其他的执行流再次进入我们称之为重入。
在信号部分就有重入比如说进程执行一个函数还没执行完就收到了一个信号另一个执行流执行的还是这个函数。而在多线程这里就更好理解了我们上面写的多线程代码都是重入的。
2认识可重入
一个函数在重入的情况下对程序的运行过程和结果没有影响则该函数被称为可重入函数反之是不可重入函数。
常见的可重入情况
不使用全局变量或静态变量。不使用malloc或者new开辟出的空间。不返回静态或全局数据所有数据都有函数的调用者提供。
常见的不可重入情况
调用了malloc/free函数因为malloc函数是用全局链表来管理堆的函数。可重入函数体内使用了静态的数据结构。调用了标准I/O库函数标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
总之函数如果使用了全局数据、静态数据和堆区的数据就是不可重入的反之就是可重入的。
2.正确认识线程安全
线程安全是指多个线程并发同一段代码时会出现相同的结果。不加锁对全局变量或者静态变量操作时一般会出现线程安全问题。
常见线程安全情况
每个线程对全局变量或者静态变量只有读取的权限而没有写入的权限一般来说这些线程是安全的。类或者接口对于线程来说都是原子操作。多个线程之间的切换不会导致该接口的执行结果存在二义性。多线程共同执行的代码段中如果有全局变量或者静态变量并且没有保护那么就是线程不安全的。
常见线程不安全情况
不保护共享变量的函数。函数状态随着被调用状态发生变化的函数。返回指向静态变量指针的函数。
3.可重入与线程安全的联系
可重入与线程安全有以下关系
函数可重入就是线程安全的。这样的代码没有全局或静态变量不会产生数据不一致的问题。函数不可重入如果多个线程并发就有可能引发线程安全问题。对不可重入函数的全局变量需要加锁保护。如果一个函数中有不加锁保护的全局变量或静态变量那这个函数既不可重入多线程并发也不能保证线程安全。
可重入与线程安全的区别
可重入说的是函数的中性属性而线程安全说的是线程并发是否会出问题。可重入函数是线程安全函数的一种因为不存在全局或者静态变量。线程安全不一定保证函数可重入的而可重入函数又一定是线程安全的。因为线程安全的情况可能是对全局变量等进行了加锁。由于线程安全可以通过加锁实现所以线程安全的情况比可重入要多。
四、死锁
1.四个必要条件
死锁形成的四个必要条件互斥、请求与保持、不剥夺、环路等待。
互斥只要用到锁就必定有互斥。请求与保持请求指一个执行流申请其他锁保持指不释放自己已经持有的锁。不剥夺已经持有锁的执行流在不主动释放锁前不能强行剥夺它的锁。环路等待线程ABC都持有一把锁并且不释放。
下图中线程A持有线程B的锁线程B持有线程C的锁线程C持有线程A的锁。这就是一个典型的环路等待ABC都互相等待哪个线程都不运行构成死锁。 2.避免死锁
四个必要条件中只有第一个不能破坏改变后三个任何一个都能避免死锁。
破坏请求与等待——避免锁位释放
当一个执行流在申请另一个锁的时候要先释放已经有的锁再申请新锁。
破坏不剥夺——加锁顺序一致
注意加锁顺序不要构成环路。
避免死锁的建议——资源一次性分配
临界资源尽量一次性分配好不要分散在太多的地方加锁。
避免死锁的算法有兴趣可以了解
死锁检测算法银行家算法
采用算法避免死锁一半都会有一个执行流专门监测其他执行流的状态一旦发现某个执行流长时间不执行就代替它释放锁本质是将那个线程间传递的1再送回到共享区的锁变量中。
总之互斥锁虽然帮助我们实现了线程安全但不合理使用会造成巨大的问题所以我们再以后的代码中尽量少用互斥锁。