Linux系统之----线程互斥与同步
在前面的学习中,线程会共享一些数据(全局变量,堆内存中的数据,静态局部变量),但是这些数据会不会有冲突呢?这便是我们今天要研究的内容~
1.相关概念
1)临界资源
定义:临界资源是指在多线程环境中,多个线程可能会同时访问的共享资源。这些资源可以是内存中的数据、文件、数据库连接等。
问题:当多个线程同时访问临界资源时,可能会导致数据不一致或竞态条件(race condition),即最终结果依赖于线程执行的顺序。
2)临界区
定义:临界区是指访问临界资源的代码段。在这个区域内,线程会执行对共享资源的读写操作。
作用:临界区是线程执行的关键部分,需要特别保护以防止多个线程同时进入,从而避免数据竞争。
3)互斥
定义:互斥是一种机制,确保在任何时刻,临界区内有且只有一个执行流(线程)可以访问临界资源。
实现:互斥通常通过互斥锁(mutex)、信号量(semaphore)等同步原语来实现。这些原语可以控制对临界资源的访问,确保同一时间只有一个线程可以进入临界区。
作用:互斥机制可以保护临界资源,防止数据竞争和不一致性,是多线程编程中保证数据安全的重要手段。
4)原子性
定义:原子性是指一个操作是不可分割的,要么完全执行,要么完全不执行。在多线程环境中,原子操作不会被任何调度机制(如线程切换)打断。
实现:原子性可以通过硬件支持(如原子指令)或软件机制(如锁)来实现。
作用:原子性保证了操作的完整性,避免了在多线程环境中由于操作被打断而导致的数据不一致问题。
2.互斥量(Mutex)
互斥量是一种用于多线程同步的机制,旨在防止多个线程同时访问同一资源,从而避免数据竞争和不一致性的问题。互斥量确保了在任意时刻,只有一个线程能够访问受保护的资源或执行特定的代码段,这段代码称为临界区。
2.1 基本原理
互斥量通常包含两种基本操作:
锁定(Lock):当一个线程想要访问临界资源时,它首先尝试锁定互斥量。如果互斥量未被其他线程锁定,那么该线程成功获取锁,并进入临界区。如果互斥量已经被其他线程锁定,那么该线程将被阻塞,直到互斥量被释放。可以通过这个函数实现:pthread_mutex_lock(&mutex);
我们查看一下·:sudo apt-get install manpages-de manpages-de-dev manpages-dev glibc-doc manpages-posix-dev manpages-posix,执行后在TAB+回车,之后 man pthread_mutex_lock
解锁(Unlock):当线程完成对临界资源的访问后,它会释放互斥量。这允许其他等待的线程尝试获取锁并访问临界资源。可以通过这个函数实现:pthread_mutex_unlock(&mutex);
2.2 互斥量的特性
互斥性:确保在任意时刻,只有一个线程能够持有互斥量,从而访问临界资源。
排他性:互斥量一次只能被一个线程所拥有。
死锁:如果线程在持有互斥量的情况下尝试再次获取同一个互斥量,或者多个线程相互等待对方释放互斥量,可能会导致死锁。而且会很浪费计算机资源!!
说白了,可以把他简单理解为让线程一个一个排队执行!!(自己总结的)
3.互斥锁
为了理解互斥锁的概念,首先我们来先看这样一段代码:
#include <iostream>
#include <cstdio>
#include <stdlib.h>
#include <string>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int ticket = 1000;void* route(void* args)
{char *id=(char*)args;while (1){if(ticket>0){usleep(1000);printf("%s sells tickets:%d\n",id,ticket--);}else{break;}}return nullptr;
}
int main()
{pthread_t t1, t2, t3, t4;pthread_create(&t1,nullptr,route,(void*)"thread-1");pthread_create(&t2,nullptr,route,(void*)"thread-2");pthread_create(&t3,nullptr,route,(void*)"thread-3");pthread_create(&t4,nullptr,route,(void*)"thread-4");pthread_join(t1,nullptr);pthread_join(t2,nullptr);pthread_join(t3,nullptr);pthread_join(t4,nullptr);return 0;
}
这是一个抢票程序,运行的时候却惊奇的发现,票的编号怎么会有负数呢?
原因就在于我们没有用互斥锁一个一个的让线程执行!这几个线程在执行if(ticket>0)这个判断句时,假设最后一次进入了,此时Ticket大于0,确实都能进去,但是,线程有先跑的,有后跑的,先跑的线程拿到了正确的ticket数字,并成功的将其-1了,但是还有部分线程,已经进入了if语句,但是还没有来得及执行--语句,此时已经ticket=0了,之后等他们在执行的时候,就会出现负数情况!!!
3.1 互斥锁的数据类型
pthread_mutex_t
是 POSIX 线程库(通常称为 pthread 库)中用于实现互斥锁的数据类型。互斥锁是一种同步机制,用于在多线程环境中控制对共享资源的访问,确保同一时间只有一个线程可以访问临界资源,从而防止数据竞争和不一致性问题。由此可以解决上述问题
基本用法
使用 pthread_mutex_t
时,通常需要执行以下几个步骤:
初始化互斥锁:在创建互斥锁之前,需要先对其进行初始化。这可以通过
pthread_mutex_init()
函数完成。锁定互斥锁:当线程需要访问临界资源时,它需要先锁定互斥锁。这可以通过
pthread_mutex_lock()
函数完成。如果互斥锁已经被其他线程锁定,调用pthread_mutex_lock()
的线程将被阻塞,直到互斥锁被释放。解锁互斥锁:当线程完成对临界资源的访问后,它需要释放互斥锁。这可以通过
pthread_mutex_unlock()
函数完成。释放互斥锁后,其他等待的线程可以尝试获取锁并访问临界资源。销毁互斥锁:在程序结束时,应该销毁互斥锁以释放资源。这可以通过
pthread_mutex_destroy()
函数完成。
3.2 代码改造
#include <iostream>
#include <cstdio>
#include <stdlib.h>
#include <string>
#include <pthread.h>
#include <unistd.h>
#include "mutex.hpp"
using namespace std;
int ticket = 100;
struct data
{string name;pthread_mutex_t *lockp;
};
Mutex lock;void *route(void *args)
{// char *id=(char*)args;data *d = static_cast<data *>(args);while (1){pthread_mutex_lock(d->lockp); // 2.1 大家都要申请,申请成功,线程会继续相后运行,申请失败,线程会被阻塞if (ticket > 0){usleep(1000);printf("%s sells tickets:%d\n", d->name.c_str(), ticket--);pthread_mutex_unlock(d->lockp);}else{pthread_mutex_unlock(d->lockp);break;}}return (void *)0;
}
int main()
{pthread_mutex_t lock;pthread_mutex_init(&lock, nullptr);data d1 = {"thread-1", &lock};pthread_t t1, t2, t3, t4;pthread_create(&t1, nullptr, route, (void *)&d1);data d2 = {"thread-2", &lock};pthread_create(&t2, nullptr, route, (void *)&d2);data d3 = {"thread-3", &lock};pthread_create(&t3, nullptr, route, (void *)&d3);data d4 = {"thread-4", &lock};pthread_create(&t4, nullptr, route, (void *)&d4);pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);pthread_join(t4, nullptr);pthread_mutex_destroy(&lock);return 0;
}
运行
结果正确!
4.线程同步
4.1 条件变量
条件变量是一种用于线程同步的机制,它允许一个或多个线程在某个条件成立之前挂起(即进入等待状态),直到其他线程通过特定的操作(通常是“通知”或“唤醒”)表明条件已经满足。条件变量通常与互斥锁(mutex)一起使用,以确保在检查条件、挂起线程和唤醒线程的过程中数据的一致性和线程安全。
4.1.1 条件变量的工作原理
等待条件:线程在进入等待状态之前,需要先获取一个互斥锁,检查条件是否满足。
挂起线程:如果条件不满足,线程将释放互斥锁,并将自己挂起(进入等待状态)。
唤醒线程:当条件变为满足时,另一个线程(通常称为“通知者”)将获取互斥锁,通过调用条件变量的唤醒函数来唤醒一个或多个等待线程。
重新检查条件:被唤醒的线程重新获取互斥锁,并再次检查条件是否满足,如果满足则继续执行,否则再次进入等待状态。
可能有点抽象哈,我们举个例子来说明:
4.1.2 举例说明
想象一下,你是一家餐厅的服务员,你的工作是将厨师做好的菜端给客人。但是,你不能在菜还没做好的时候就去端,那样客人会不高兴的。所以,你需要等待厨师告诉你菜已经做好了。
在这个比喻中:
厨师:相当于程序中的一个线程,负责“生产”数据(做菜)。
服务员:相当于另一个线程,负责“消费”数据(端菜给客人)。
菜:相当于程序中的数据。
厨师告诉服务员菜做好了:相当于线程之间的通信,厨师线程通过某种方式(条件变量)告诉服务员线程,数据已经准备好了。
条件变量的工作流程
厨师开始做菜:厨师线程开始执行,准备数据。
服务员等待:服务员线程检查菜是否做好(检查条件),如果没有,就等待(挂起)。
厨师通知服务员:当菜做好后,厨师线程通过某种方式(条件变量)告诉服务员线程,菜已经准备好了。
服务员端菜:服务员线程被唤醒,再次检查菜是否做好(重新获取互斥锁并检查条件),确认后开始端菜。
4.1.3为什么需要条件变量?
如果没有条件变量,服务员可能会不断地检查菜是否做好(忙等待),这会浪费很多精力(CPU资源)。条件变量允许服务员在等待时休息(挂起线程),直到厨师通知他们菜做好了(唤醒线程),这样更加高效。
4.2 同步与竞争
同步(Synchronization)在计算机科学中,尤其是在并发编程领域,指的是协调多个线程或进程执行的机制,以保证它们能够正确地共享资源和数据。同步的主要目的是防止多个线程同时对共享资源进行访问和修改,从而避免数据不一致和程序错误。在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,
4.3 竞态条件
竞态条件(Race Condition)是指在多线程环境中,多个线程访问共享数据时,由于线程执行顺序的不确定性,导致程序的输出或行为出现不可预测的变化。竞态条件通常发生在没有适当同步机制的情况下,当两个或多个线程试图同时修改同一个共享资源时。
4.4 条件变量函数
我们挑一些重要的说明一下:
首先是init,其第一个参数表示要初始化的条件变量,第二个填NULL就好
之后是signal,他的作用是发信号,唤醒线程,注意:唤醒一个等待在指定条件变量上的线程。如果有多个线程正在等待同一个条件变量,那么只有一个线程会被唤醒,具体哪个线程被唤醒是不确定的。
之后是broadcast,他会唤醒所有等待该条件变量的线程,而不是只唤醒一个。
之后是wait,这个就是等待条件满足,第一个参数填条件变量,第二个填互斥锁
最后就是destroy,这个就是销毁了
5.生产者与消费者模型
5.1 模型概念
生产者消费者模型是计算机科学中用于描述和解决多线程或多进程环境下数据同步问题的一种抽象模型。在这个模型中,生产者负责生成数据,消费者则负责处理这些数据。为了协调生产者和消费者之间的工作,通常会引入一个缓冲区(或称为队列),生产者将生成的数据放入缓冲区,而消费者则从缓冲区中取出数据进行处理。这种模型通过解耦生产者和消费者之间的直接交互,降低了系统的耦合度,提高了并发处理能力。可以使用互斥锁(Mutex)来控制对缓冲区的访问,或者使用信号量(Semaphore)来控制生产者和消费者之间的同步。这些同步机制可以防止多个生产者同时向缓冲区写入数据,或者多个消费者同时从缓冲区读取数据,从而避免数据冲突和不一致的问题。
5.2 三种关系
a. 生产者之间是什么关系?
互斥关系
生产者之间的关系通常是互斥的,这意味着在同一时间内,通常只有一个生产者能够访问或修改特定的资源。这种互斥关系是必要的,以防止多个生产者同时对同一资源进行写操作,从而导致数据不一致或损坏。
例如,在数据库系统中,如果多个生产者同时写入同一个数据记录,可能会导致数据冲突或丢失。因此,通常需要某种形式的锁定机制(如排他锁)来确保在同一时间内只有一个生产者可以写入数据。
b. 消费者之间是什么关系?
互斥关系
消费者之间的关系也是互斥的,尤其是在消费者需要修改共享资源时。这种互斥关系确保了数据的一致性和完整性。例如,如果多个消费者同时从同一个缓冲区中读取并修改数据,可能会导致数据混乱。
在某些情况下,消费者之间可能不需要严格的互斥关系,特别是当消费者只读取数据而不进行修改时。然而,如果读取操作需要保证数据的一致性(如在数据库查询中),则可能需要某种形式的同步机制。
c. 生产者和消费者之间是什么关系?
互斥 && 同步
生产者和消费者之间的关系既包括互斥也包括同步:
互斥:在同一时间内,通常只有一个生产者可以写入数据,而消费者在读取数据时也需要确保数据的一致性。这意味着生产者和消费者在访问共享资源时需要某种形式的互斥机制。
同步:生产者和消费者之间需要同步,以确保数据的生产和消费能够协调进行。例如,消费者通常需要等待生产者生成数据(生产者-消费者问题中的常见场景),而生产者可能需要等待消费者处理完数据后才能继续生产新数据。
为此,提炼出“321原则”,即3种关系,2个角色(生产者、消费者),1个交易场所(内存块)
5.3 基于BlockingQueue
的生产者消费者模型
基于BlockingQueue
的生产者消费者模型是一种常见的并发编程模式,它利用了阻塞队列的特性来简化生产者和消费者之间的同步问题。BlockingQueue
是一个支持两个附加操作的队列,这两个操作分别是:当队列已满时,生产者线程阻塞等待;当队列为空时,消费者线程阻塞等待。这种机制可以有效地协调生产者和消费者之间的工作,而不需要显式地使用同步控制(如互斥锁和条件变量)
说的复杂,看个图就明白了~
这个是1v1的,那我要是多v多是不是也可以呢?等下我们就要依据这个模型写一段代码来练习~