贵阳网站建设多点互动创业it外包公司
贵阳网站建设多点互动,创业it外包公司,wordpress+move插件,企业免费建网站一、进程线程间通信的相关概念
临界资源#xff1a;多线程执行流共享的资源就叫做临界资源。确切的说#xff0c;临界资源在同一时刻只能被一个执行流访问。临界区#xff1a;每个线程内部#xff0c;访问临界资源的代码#xff0c;就叫做临界区。互斥#xff1a;通过互…一、进程线程间通信的相关概念
临界资源多线程执行流共享的资源就叫做临界资源。确切的说临界资源在同一时刻只能被一个执行流访问。临界区每个线程内部访问临界资源的代码就叫做临界区。互斥通过互斥操作能够保证在任何时刻有且只有一个执行流进入临界区访问临界资源通常对临界资源起保护作用。原子性不会被任何调度机制打断的操作该操作只有两态要么完成要么未完成 二、互斥锁
2.1 竞态条件
大部分情况线程使用的数据都是局部变量变量存储在线程栈空间内。这种情况变量归属单个线程其他线程无法访问这种变量。但有时候很多变量都需要在线程间共享这样的变量称为共享变量可以通过数据的共享完成线程之间的交互。比如全局数据、堆空间数据。多个线程并发的操作共享变量会带来数据竞争冲突以及数据不一致等竞态条件问题。
竞态条件
竞态条件Race Condition是指多个线程或进程同时访问共享资源并且对资源的访问顺序不确定导致最终结果的正确性依赖于线程执行的具体时序。竞态条件可能导致不可预测的结果破坏程序的正确性和一致性。竞态条件通常发生在多个线程或进程同时对共享资源进行读写操作时其中至少一个是写操作。当多个线程或进程同时读写共享资源时由于执行顺序的不确定性可能会导致数据的不一致性、丢失、覆盖等问题。
测试程序
int tickets 100; //共有100张票void *ThreadRoutine(void *name)
{while (1){if (tickets 0){usleep(rand() % 1000); // 模拟业务过程花费的时间printf(%s sells ticket:%d\n, (char *)name, tickets);--tickets;}else{break;}usleep(rand() % 1000); // 模拟处理其他业务花费的时间}return nullptr;
}int main()
{srand((unsigned)time(nullptr));pthread_t tid1, tid2, tid3, tid4;pthread_create(tid1, nullptr, ThreadRoutine, (void *)child thread 1);pthread_create(tid2, nullptr, ThreadRoutine, (void *)child thread 2);pthread_create(tid3, nullptr, ThreadRoutine, (void *)child thread 3);pthread_create(tid4, nullptr, ThreadRoutine, (void *)child thread 4);pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);pthread_join(tid4, nullptr);return 0;
}运行结果 同一编号的票被多个线程售出某些线程售出了负数编号的票
该程序存在竞态条件问题即公共变量tickets被多执行流同时访问和修改。 提示除了多线程进程外信号处理函数也是异步执行的多执行流执行同样存在竞态条件问题。 并发运行问题
例如tickets 0和--tickets操作并不是原子性操作而是对应三条汇编指令
将数据从内存加载到寄存器当前线程的上下文中进行逻辑运算或算数运算将数据写回内存
在这三条步骤的其中任何一步该线程都有可能被切换切换前线程上下文会被保存。其他线程在执行时也对tickets进行了访问和修改。当原线程再次被CPU调度执行时恢复上下文数据此时的寄存器与内存就会发生数据不一致的错误。
并行运行问题
多核CPU允许多线程并行同时运行。在ThreadRoutine函数中由于没有对访问tickets的操作进行互斥可能会导致多个线程同时读取和修改tickets变量从而产生不可预测的结果。
例如当多个线程同时执行if (tickets 0)语句时可能会出现以下情况
线程A和线程B同时读取tickets的值为1。线程A先执行--tickets操作将tickets的值减为0。线程B再执行--tickets操作将tickets的值减为-1。
这样就会出现某些线程售出了负数编号的票。 2.2 互斥锁的基本用法
为了解决竞态条件问题可以使用互斥锁Mutex来保护对tickets变量的访问。互斥锁可以确保在同一时间只有一个线程能够访问临界区对tickets变量的访问从而避免数据竞争的发生。 下面是互斥锁的基本使用方法
定义互斥锁变量在使用互斥锁之前需要先定义一个互斥锁变量。可以使用pthread_mutex_t类型来声明互斥锁变量例如pthread_mutex_t mutex;初始化互斥锁在使用互斥锁之前需要对互斥锁进行初始化。 静态初始化在定义互斥锁变量时使用PTHREAD_MUTEX_INITIALIZER宏进行初始化。动态初始化可以使用pthread_mutex_init函数来初始化互斥锁例如pthread_mutex_init(mutex, NULL);。第一个参数是要初始化的互斥锁变量第二个参数是互斥锁的属性通常使用NULL表示使用默认属性 加锁在访问共享资源之前需要先加锁。可以使用pthread_mutex_lock函数来加锁例如pthread_mutex_lock(mutex);。如果互斥锁已经被其他线程锁定那么当前线程会被阻塞直到互斥锁被解锁。访问共享资源在互斥锁被锁定的情况下可以安全地串行访问共享资源。解锁在访问共享资源完成后需要解锁互斥锁以便其他线程可以继续访问共享资源。可以使用pthread_mutex_unlock函数来解锁例如pthread_mutex_unlock(mutex);。销毁互斥锁 不再需要使用互斥锁时需要将其销毁。可以使用pthread_mutex_destroy函数来销毁互斥锁例如pthread_mutex_destroy(mutex);。静态初始化的互斥锁在程序结束时会自动被系统回收无需手动销毁。不要销毁一个已经加锁的互斥量对于已经销毁的互斥量要确保后面不会有线程再尝试加锁
我们将上面的售票程序加入互斥锁
int tickets 100; //临界资源
// 定义一个全局的互斥锁变量并利用宏进行初始化静态初始化
pthread_mutex_t mtx PTHREAD_MUTEX_INITIALIZER;void *ThreadRoutine(void *name)
{while (1){// 在访问共享资源之前需要先加锁。pthread_mutex_lock(mtx);//临界区if (tickets 0){usleep(rand() % 1000); // 模拟业务过程花费的时间printf(%s sells ticket:%d\n, (char *)name, tickets);--tickets;// 在访问共享资源完成后需要解锁互斥锁。pthread_mutex_unlock(mtx);}else{// 在访问共享资源完成后需要解锁互斥锁。pthread_mutex_unlock(mtx);break;}// 在此处解锁不行如果线程执行break就不会解锁互斥锁。其他线程会被一直阻塞。usleep(rand() % 1000); // 模拟处理其他业务花费的时间}return nullptr;
}运行结果 需要注意的几点 在访问共享资源完成后需要解锁互斥锁。否则其他线程会被一直阻塞。需要特别注意break, goto等跳转语句跳过解锁函数。 被互斥锁锁定的临界区只能串行执行互斥访问虽然保证了多执行流访问临界资源的安全性但是会在一定程度上降低程序的效率。 尽量保证被互斥锁锁定的代码都是访问临界资源的代码不要将其他无关的操作也放入临界区中。因为相比并发或并行执行临界区串行执行的效率较低。
再次改进上面的代码
#define THREAD_NUM 5
int tickets 100;//声明一个ThreadData类使线程入口函数的参数更多样化。
class ThreadData
{
public:string _tname; //线程名pthread_mutex_t *_pmtx; //互斥锁变量的地址ThreadData(const string tname, pthread_mutex_t *pmtx): _tname(tname),_pmtx(pmtx){};
};void *ThreadRoutine(void *arg)
{ThreadData *td (ThreadData *)arg;while (1){// 在访问临界资源前进行加锁pthread_mutex_lock(td-_pmtx);if (tickets 0){usleep(rand() % 1000); // 模拟业务过程花费的时间printf(%s sells ticket:%d\n, td-_tname.c_str(), tickets);--tickets;// 不再访问临界资源时需要解锁。pthread_mutex_unlock(td-_pmtx);}else{// 不再访问临界资源时需要解锁。pthread_mutex_unlock(td-_pmtx);break;}usleep(rand() % 1000); // 模拟处理其他业务花费的时间}delete td; // 释放各自的ThreadData结构空间return nullptr;
}int main()
{srand((unsigned)time(nullptr));// 在主线程栈区创建互斥锁变量pthread_mutex_t mtx;// 调用pthread_mutex_init初始化互斥锁动态初始化pthread_mutex_init(mtx, nullptr);// 循环创建子线程pthread_t tid[THREAD_NUM];for (int i 0; i THREAD_NUM; i){string tmp child thread ;tmp to_string(i 1);ThreadData *td new ThreadData(tmp, mtx);pthread_create(tid i, nullptr, ThreadRoutine, td); //传入ThreadData对象的指针}// 循环等待子线程for (int i 0; i THREAD_NUM; i){pthread_join(tid[i], nullptr);}// 在不再需要使用互斥锁时需要将其销毁。动态初始化的互斥锁需要进行销毁而静态初始化不需要pthread_mutex_destroy(mtx);return 0;
}新的问题 加锁了之后线程在执行临界区代码时是否会被切换会有问题吗? 会被切换但不会有问题虽然被切换了但是你是持有锁被切换的, 所以其他抢票线程要执行临界区代码也必须先申请锁而它是无法申请成功的。所以也不会让其他线程进入临界区就保证了临界区中数据一致性 对于访问临界资源的线程而言临界区代码要么全部执行成功要么全部不执行访问临界资源的操作不可被中断不能同时执行其他线程的临界区代码这就是原子性的体现。 要访问临界资源每一个线程都必须先申请锁而锁本身就是一种共享资源那么谁来保证锁的安全呢 所以为了保证锁的安全申请和释放锁必须是原子的 2.3 互斥锁的原理
在汇编层面一条汇编语句要么已经执行完要么就还没有执行是原子性的。为了实现互斥锁操作大多数体系结构都提供了swap或exchange汇编指令该指令的作用是把寄存器和内存单元的数据相交换由于只有一条指令保证了原子性。即使是多处理器平台并行访问内存的总线周期也有先后一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。CPU的寄存器数据本质就是当前执行流的上下文。寄存器的存储空间被所有执行流共享但是寄存器的内容是被每一个执行流私有的。所以在切换线程时要将当前线程的寄存器数据保存到其PCB中并恢复下一个线程的寄存器数据。
以下是加锁的核心汇编伪代码
lock:movb $0, %al // 将数值0move到al寄存器中xchgb %al, mutex //交换al寄存器与mutex变量内存的数据if(al寄存器的内容 0){return 0;}else挂起等待;goto lock; //跳转到lock标签再次申请锁我们可以将互斥锁变量mutex理解成一个整形变量值为1表示互斥锁未被线程持有值为0表示互斥锁已经被其他线程锁定。创建互斥锁变量并进行初始化后其默认值为1。由于exchange汇编指令是原子的所以不管线程如何切换只有一个线程能够将mutex内存中的1值交换到自己的寄存器当中即该线程的上下文中。而线程上下文是线程的私有数据实现了公有到私有的转换。同时寄存器当中的0值被交换到了mutex中其他线程再进行交换也只能交换到0。在进行if判断时交换到1值的线程执行return 0可以安全地进入临界区访问临界资源而交换到0值的线程阻塞等待直到互斥锁被解锁这些线程才会被唤醒然后再次尝试申请锁。
以下是解锁的核心汇编伪代码
unlock:movb $1, mutex //将数值1move到mutex变量内存唤醒等待mutex的线程;return 0;当持有锁的线程访问完临界资源后会将mutex变量重新置为1即解锁互斥锁。同时应该唤醒等待互斥锁解锁的线程让他们再次竞争申请锁。
回答之前的问题 谁来保证锁的安全呢 为了保证锁的安全申请和释放锁必须是原子的在设计加锁时通过一条原子性的exchange指令保证了加锁和解锁的原子性。 加锁了之后线程在临界区中是否会切换会有问题吗? 线程在临界区中也可能会被切换但他是持有锁被切换的所谓持有锁切换是指互斥锁的1值保存在当前线程的上下文被当前线程私有。而其他线程即使被CPU调度执行也无法抢占互斥锁也就无法访问临界区代码。所以不会有任何问题。 三、可重入函数和线程安全
可重入函数同一个函数被多个执行流同时进入就叫重入。如果该函数在被重入执行的过程中不会出现任何错误则被称为可重入函数。反之就是不可重入函数。线程安全多个线程并发执行时在没有锁保护的情况下访问了共享资源如全局或静态变量堆区数据等会出现数据竞争从而导致数据冲突数据不一致等线程安全问题。
3.1 线程安全的情况
仅使用本地局部数据或者通过制作全局数据的本地拷贝来保护全局数据。使用互斥锁Mutex来保护对共享资源的访问。互斥锁可以确保在同一时间只有一个线程能够访问临界区从而避免数据竞争的发生。每个线程对共享资源只有读取的权限而没有写入的权限一般来说这些线程是安全的。不调用线程不安全的函数
3.2 可重入函数的情况
仅使用本地局部数据或者通过制作全局数据的本地拷贝来保护全局数据。使用互斥锁Mutex来保护对共享资源的访问。如全局、静态变量或其他共享资源。不调用不可重入函数
常见不可重入的情况
调用了malloc/free函数因为Linux内核是用全局链表和全局红黑树结构来组织和管理堆空间的。请看提示调用了标准I/O库函数因为标准I/O库的很多实现都以不可重入的方式使用了全局数据结构。 提示 关于Linux内核中的堆区管理请阅读【多线程】线程的概念 {Linux内核中的堆区管理虚拟地址到物理地址的转换页页框页表MMU内存管理单元Linux线程概念轻量级进程线程共享进程的资源线程的优缺点线程的用途}-CSDN博客关于多执行流调用不可重入函数插入链表节点请阅读【信号】信号处理 {信号处理的时机内核态和用户态信号捕捉的原理信号处理函数signal, sigaction可重入函数volatile关键字SIGCHLD信号}-CSDN博客 3.3 区别和联系
联系
函数是可重入的那就是线程安全的。函数是不可重入的那就不能由多个线程使用有可能引发线程安全问题。
区别
可重入函数是线程安全函数的一种。线程安全不一定是可重入的而可重入函数则一定是线程安全的。如果将对临界资源的访问加上锁则这个函数是线程安全的。但如果这个重入函数加锁还未释放则会产生死锁因此是不可重入的。 四、死锁
死锁Deadlock是指在并发系统中两个或多个进程或线程因为互相等待对方释放资源而无法继续执行的状态。在死锁状态下进程无法前进也无法释放资源导致系统无法正常运行。
死锁通常发生在多个进程或线程同时竞争有限的资源时每个进程都在等待其他进程释放资源而自己又无法释放已经占有的资源。 特殊情况一个执行流一把互斥锁也可能导致死锁即加锁后不解锁再次申请锁。 死锁的发生需要满足以下四个条件也被称为死锁的必要条件 互斥条件一个资源每次只能被一个执行流使用不加锁自然就不会产生死锁。 请求与保持条件一个执行流因请求资源而阻塞时对已获得的资源保持不放。 不剥夺条件一个执行流已获得的资源在未使用完之前不能强行剥夺。 循环等待条件若干执行流之间形成一种头尾相接的循环等待资源的关系使得每个执行流都在等待下一个执行流所占有的资源。
当这四个条件同时满足时就可能发生死锁。一旦发生死锁系统将无法自动解除死锁状态需要通过人工干预来解决。
为了避免死锁的发生可以采取以下策略 破坏互斥条件例如允许多个进程或线程同时访问某些资源。 破坏请求与保持条件例如要求进程或线程在执行之前一次性获取所有需要的资源否则在等待资源时释放已经占有的资源。 破坏不可剥夺条件例如允许系统强制剥夺某些进程或线程的资源。 破坏循环等待条件例如通过对资源进行排序按照固定的顺序申请资源避免交叉申请循环等待。T1,T2都先申请R1再申请R2 其他方法精简临界区代码缩短持有锁的时间合并临界区资源一次性分配一把锁
死锁是并发系统中的一个重要问题对系统的性能和可靠性有很大影响。因此在设计和实现并发系统时需要合理地分配和管理资源以避免死锁的发生。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/87839.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!