深圳住房和建设局官网站首页,最佳建站模板,网站seo分析工具,wordpress网站搬迁一.线程同步#xff08;一#xff09;.概念线程同步是一种多线程关系#xff0c;指的是线程之间按照特定顺序访问临界资源#xff0c;进而能够避免线程饥饿问题。所谓线程饥饿指的是某个线程长期“霸占”临界资源#xff0c;导致其他线程无法访问该资源。而通过线程同步机…一.线程同步一.概念线程同步是一种多线程关系指的是线程之间按照特定顺序访问临界资源进而能够避免线程饥饿问题。所谓线程饥饿指的是某个线程长期“霸占”临界资源导致其他线程无法访问该资源。而通过线程同步机制能够有效避免饥饿的情况发生。可以理解为将线程先排成一排就像队列一样访问临界资源时按照队列顺序一个接着一个的访问。二.同步与互斥的关系之前介绍过线程互斥的概念链接在此Linux——什么是互斥与互斥锁当我们明白同步的概念后需要梳理一下互斥与同步的关系。首先不管是线程互斥还是线程同步指的都是线程与线程之前的一种联系。很多文章都在说“同步是一种复杂的互斥互斥是特殊的同步”本人解释如下互斥的要求是在同一时间只能有一个线程访问临界资源也就是线程并发执行同步的条件是线程按次序执行也就是说同步的条件有两个有序和并发。同步在互斥的基础上更加强调了线程有序因此“同步是一种复杂的互斥”。同时同步条件之一是有序而互斥没有顺序要求。也就是说不管有序还是无序只要满足同一时间只有一个线程访问临界资源就是互斥。换句话说有序的互斥也就是同步即“互斥是一种特殊的同步”。总结一下不管是互斥还是同步共有条件是“同一时间只有一个线程访问临界资源”。因此同步与互斥都是用来处理多线程中访问临界资源问题的手段。但同步比互斥更加强调线程顺序的重要性。三.条件变量使用条件变量是达成线程同步的一种手段。顾名思义条件变量需要线程达成某种条件进而完成同步。满足条件时线程继续运行不满足条件时线程排队等待直到其他线程发送特定信号解除等待。linux提供了条件变量的类型和接口头文件和互斥锁相同为pthread.h编译时需要在命令后加-pthread。g xxx.cpp -o xxx -pthread; //gcc同理pthread_cond_t为条件变量类型使用条件变量完成线程同步前必须先定义条件变量对象。pthread_cond_t cond;①初始化和销毁使用条件变量必须先进行初始化操作linux提供了两种初始化方式系统接口和系统宏定义。返回值为0代表成功非0代表失败条件变量相关的其他接口同理。pthread_cond_init(cond, nullptr);//参数二为条件变量属性默认为NULL即可
pthread_cond_init cond PTHREAD_COND_INITALIZER;当程序不再使用该条件变量时请销毁它pthread_cond_destroy(cond);②阻塞等待根据特定情况可以使用条件变量使线程阻塞pthread_cond_wait(cond, mtx);//第二个参数为互斥锁且必须是已经初始化以及完成加锁。使用上通常伴随条件判断如果满足或不满足某种条件时便会触发wait函数进而使线程阻塞并进入等待队列如果多个线程都触发了wait函数就会按照顺序依次进入等待队列。示例如下pthread_mutex_lock(mtx);
while(条件)
{//达成某种条件时线程便会阻塞等待并入等待队列pthread_cond_wait(cond, mtx);
}
pthread_mutex_unlock(mtx);该接口内部会将线程放在等待队列上并解除传递的互斥锁这也就是为什么函数第二个参数是互斥锁mutex等到结束阻塞的信号后函数内部会重新争夺互斥锁待加锁成功后退出wait函数。图示解析如下③解除阻塞当满足特定条件时其他线程可以发送信号让等待队列中的线程停止阻塞。pthread_cond_broadcast(cond);//该条件变量下的所有阻塞线程继续运行
pthread_cond_signal(cond);//该条件变量下按照等待顺序让一个阻塞线程继续运行使用上会配合pthread_cond_wait接口使用达到线程同步的目的。伪代码如下//线程一
pthread_mutex_lock(mtx);
while(条件)
{//尚不构成某种条件不能获取临界资源pthread_cond_wait(cond, mtx);
}
...//达成条件处理临界资源
pthread_mutex_unlock(mtx);//线程二
...
if(条件)
{//达成某种条件解除阻塞让其他线程访问临界资源pthread_cond_signal(cond);
}尤其需要注意的是在判断线程是否需要阻塞时一定要用while循环判断而不是if条件判断。这是因为线程可能因为意外情况结束阻塞但是此时条件尚未达成因此需要在解除阻塞后再次判断是否依旧达成条件。四.条件变量实际应用就使用场景而言一个经典的案例就是基于阻塞队列的生产消费模型。相关代码在这篇博客中Linux——生产消费者模型阻塞队列形式这里主要解释条件变量在其中的应用而不再具体讨论模型的实现。当容器为空时消费者线程需要阻塞在临界区外由于是多线程可能会有很多线程阻塞在临界区之外。这时这些线程就需要按顺序“排队”代码而言就是调用pthread_cond_wait接口使线程阻塞。当生产者线程将数据写入容器后发送信号给阻塞队列按等待队列次序让消费者线程依次运行处理临界资源。同理当容器满时通过另一个条件变量使生产者线程阻塞当消费者处理数据后发送信号给生产者使其继续运行。需要注意的是生产者与消费者是两个不同的条件变量。这是因为二者阻塞在不同的等待队列中只有生产者产出数据才能解除消费者的阻塞同时只有消费者消费数据才能解除生产者的阻塞。但双方并不会发生死锁问题这是因为二者共用一个互斥锁生产者与消费者是并发执行的互斥关系。伪代码示意如下//生产者
void* producerFunction(void* arg)
{pthread_mutex_lock(mtx);while(容器已满){pthread_cond_wait(cond1, mtx);}...//将资源放入容器的过程pthread_cond_signal(cond2);//发送信号使消费者消费数据pthread_mutex_unlock(mtx);...
}
//消费者
void* consumerFunction(void* arg)
{pthread_mutex_lock(mtx);while(容器为空){pthread_cond_wait(cond2, mtx);}...//处理临界资源的过程pthread_cond_signal(cond1);//发送信号使生产者生产数据pthread_mutex_unlock(mtx);...}二.POSIX信号量一.概念与条件变量相同信号量也是应用于线程同步的一种技术。本质上信号量是一种资源预定机制。就好比电影院售票票数固定谁能买到票谁就能看电影没有票就看不到。信号量就是电影票提前设定好信号量的最大值象征着有多少线程能访问临界资源信号量--就是卖出一张票说明有一个线程拥有了访问资源的权力信号量就是退掉了一张票说明有一个线程访问过临界资源或者取消访问同理也可以参考智能指针的引用计数。信号量--的操作称为P操作即线程预定了资源信号量的操作称为V操作即临界资源访问完毕或取消预订。需要注意P操作叫做预定资源并不是真正获取了临界资源。什么意思呢就像我们去蜜雪冰城买奶茶服务员给了我们一张小票上面写着我们是第几单前头还有多少单。此时的我们并没有拥有奶茶而是“预定”了一杯在未来某时就会获得奶茶。取得信号量的线程或者叫完成P操作的线程就是预定成功了在未来某时就能访问临界资源。同样的当我们等待奶茶时如果临时取消不想买了把票退给服务员就是V操作中的取消预定我们兑换小票取得奶茶就是V操作的临界资源访问完毕。V操作的两种情况就本质而言都是信号量只不过一个是没有访问临界资源另一个访问完成。二.使用linux提供了使用信号量的系统接口。定义在头文件semaphore.h中。编译时需要在命令后加上-pthread。g xxx.cpp -o xxx -pthread信号量类型为sem_t类型。①初始化和销毁首先需要定义sem_t类型对象并完成对信号量的初始化。初始化的主要目的就是确定该信号量当前值。返回值含义与条件变量接口相同0代表成功非0代表失败。sem_t sem;
sem_init(sem, 0, 5);
//参数pshared0代表线程间共享非0代表进程间共享
//参数value即设定的该信号量当前值这里就代表该信号量当前还能分配给5个线程当不再使用信号量时请及时销毁这些和条件变量异曲同工。sem_destroy(sem);②P操作信号量--当线程需要访问临界资源时请先调用sem_wait接口申请信号量如果此时有剩余信号量那么申请成功信号量--如果此时信号量为0代表没有剩余信号量可以申请此时线程阻塞在wait接口。sem_wait系统调用本身是原子性的也就是说P操作本身是线程安全的。sem_wait(sem);//申请失败会阻塞
sem_trywait(sem);//申请失败不会阻塞③V操作信号量当线程完成对临界资源的访问后需要归还信号量便于其他线程获取访问资源的资格。也就是使信号量。同样V操作也是原子性即线程安全的。sem_post(sem);在实际应用场景中需要我们先去申请信号量P操作申请成功后再加锁访问临界资源当不再访问时解锁后归还信号量V操作。伪代码流程如下//sem、mtx必须是多线程共享都能访问的资源也就是说这些线程看到的必须是同一份sem、mtx
//一般而言可以把sem、mtx作为arg参数传给线程函数
void* threadFunction(void* arg)
{...sem_wait(sem);//先申请信号量pthread_mutex_lock(mtx);//申请信号量成功加锁...//访问临界资源pthread_mutex_unlock(mtx);//解锁sem_post(sem);//归还信号量...
}三.实际应用基于循环队列的生产消费模型在上文中我们提到了基于阻塞队列形式的生产消费模型那里是使用条件变量来完成生产者与消费者的同步过程。而生产消费模型还可以是循环队列的形式使用信号量来完成线程同步的过程。首先简单说一下什么是循环队列本质就是长度固定的数组从头开始插入资源当插入资源位于最后一个位置的下一个时再从头开始插入资源也就是把线性的数组“头尾相连”变成逻辑上的环形结构。不再具体解释讲解循环队列的文章网上很多。这里重点说明信号量在生产消费模型中是怎么使用的。图示如下首先我们需要知道信号量作为一种预定机制要信号量清楚在生产消费模型中是在预定什么这一点非常重要对于生产者是预定队列位置确保有位置能供自己放入资源数据。对于消费者是预定容器循环队列资源确保容器中的现有资源有自己一份。生活中的例子比比皆是比如蛋糕店生产蛋糕就要确保柜台上还有位置能放蛋糕而顾客要看柜台上是否还有蛋糕确定能不能买到。想清楚这一点就不难发现生产者和消费者是预定了两种不同的资源空位和容器剩余资源。因此我们要定义两个信号量来代表这两种资源。并且在初始化时空位数量要为队列长度剩余资源数量为0。sem_t placeSem;//生产者预定的空位
sem_t dataSem;//消费者预定的容器剩余资源
sem_init(placeSem, 0, 队列长度);
sem_init(dataSem, 0, 0);在使用时生产者先预定空位placeSem进行P操作然后加锁把数据放入队列再解锁最后使容器剩余资源数量1dataSem进行V操作。伪代码如下//生产者
void* producerFunction(void* arg)
{sem_wait(placeSem);//先预定空位pthread_mutex_lock(prod);//生产者与生产者是互斥关系加生产者间的互斥锁...//资源放入容器中pthread_mutex_unlock(prod);//解锁sem_post(dataSem);//容器剩余资源数量1...
}需要说明的是加锁的过程可以在P操作预定空位之前但是没有必要。因为加锁后的线程一定能访问临界资源也就是说加锁后的线程肯定是预定了空位的。进行信号量P操作的目的是为了消费者与生产者线程同步且锁加在P操作之后还能降低粒度。可能还会有疑问为什么最后不归还空位使placeSem1呢请先看消费者的处理过程之后会进行说明。对于消费者而言先预定容器剩余资源dataSem进行P操作然后加锁获取容器数据再解锁最后使容器空位1placeSem进行V操作。伪代码如下//消费者
void* consumerFunction(void* arg)
{sem_wait(dataSem);//预定剩余资源pthread_mutex_lock(consum);//消费者之间是互斥关系加消费者间的互斥锁...//获取容器资源pthread_mutex_unlock(consum);//解锁sem_post(placeSem);//容器空位1...
}这时我们会有两个问题一是为什么生产之后不立即释放预定的空位placeSem的V操作二是为什么生产者和消费者的互斥锁不是同一个回答第一个问题首先就逻辑上而言当完成生产时我们预定的那个空位已经被生产的数据所填满此时那个空位已经不再存在。其次就资源层面来讲当生产者生产数据放入空位后如果此时空位数量还是生产前那么多未免太不合理了吧。因此只有在消费者消费数据后才能空位1也就是在消费者函数中进行placeSem的V操作。同理只有生产者生产数据后代表剩余资源的dataSem信号量才能进行V操作。回答第二个问题首先我们知道在阻塞队列形式的生产消费模型中生产者与消费者的互斥锁是同一个这是因为生产者与消费者是互斥关系本质原因是防止生产者与消费者访问同一个资源。但是在阻塞队列形式中虽然生产者与消费者共享同一个容器但是容器内部会定义两个变量分别代表生产者与消费者各自访问的资源在队列中的下标。而基于信号量的特性当队列为空时消费者一定会阻塞在申请剩余资源的P操作那里只有当生产者生产数据后使剩余资源1消费者才能进入临界区获取资源。也就是说消费者永远在生产者的“屁股后头”那么双方访问的下标永远不会相同。因此生产者和消费者只需要各自加锁。从生产消费模型的两种形式能看出信号量和条件变量都是用来完成线程同步的工具但是条件变量可以一次性唤醒所有线程而信号量不行。同样地信号量能够根据此时的计数值记录状态而条件变量不行。并且信号量的一大使用特色是作为进程间同步的工具而条件变量是作为线程间同步的工具。三.综合应用线程池一.自制线程池线程池是利用池化技术维护多个线程当需要处理任务时便调度维护的线程这样不仅可以保证对内核的充分利用还可以避免过分调度。主要的应用场景是任务处理过程短且需要大量线程的环境。比如Web服务器完成网页请求就属于这类情况有大量的网页点击需求任务小但是需求大使用线程池能够避免大量的线程创建的等待时间。而会话请求就不太合适因为会话时间相比于创建线程要长很多线程池的优点就不太明显。此外如果是要求迅速响应的任务和瞬间需求大量线程的应用因为瞬间创建大量线程可能导致内存极限进而出错线程池技术都比较合适。概念上线程池本身也是一种生产消费模型。生产过程就是获取任务到线程池中消费过程就是调度具体的线程处理任务。因此我们可以提前创建多个线程作为消费者使用循环队列作为存储任务的容器。当容器中有任务时按照次序调度线程处理任务。源码threadPool/threadPoolPlus · 纽盖特/linux - 码云 - 开源中国 (gitee.com)伪代码如下/*
优化线程池
将队列划分为生产消费两个
生产者生产数据后,当数据满足一定数量时交换生产消费队列
同时能使生产与消费互斥关系降到最低只有在交换队列时才会互斥
*/
struct ThreadData{//线程数据...//线程名、线程id等数据void* _arg;//记录该线程的线程池指针因为线程池_threadFunc函数为静态无法直接使用threadPool对象资源和成员函数
};
class Thread{//线程类
public:Thread(.../*其他线程数据*/, tFunc func, void* arg nullptr):_func(func){...//记录其他线程数据_data._arg arg;}void run()//启动线程函数传参{pthread_create(_data._tid, nullptr, _func, (void*)_data);}void join(){pthread_join(_data._tid, nullptr);}private:ThreadData _data;tFunc _func;//线程调度的函数
};#define THREAD_NUM 5//消费者线程数量
#define QUEUE_MAX_SIZE 5//默认容器大小
templateclass T
class ThreadPool{static void* _threadFunc(void* arg)//线程执行任务的函数{//通过参数获取threadPool对象因为该函数是静态没有this指针ThreadData* data (ThreadData*)arg;ThreadPoolT* pool (ThreadPoolT*)data-_arg;while(true)//某线程循环等待处理队列任务{T task;{...//获取消费者锁while(pool-isEmpty()){...//当队列空时阻塞等待生产者交换队列}task pool-getTask();//获取任务...//解除消费者锁}task();//执行资源内容}return nullptr;}void swapQueue(){...//交换生产者消费者队列}
public:bool isFull()//判断队列是否已满{return _quP-size() QUEUE_MAX_SIZE;}bool isEmpty()//判断队列是否已空{return _quC-size() 0;}T getTask()//从阻塞队列中获取任务{T task _quC-front();_quC-pop();return task;}
public:ThreadPool(size_t num THREAD_NUM)//参数定义线程池线程数量:_num(num){for(size_t i1; i_num; i)//创建线程{...//记录线程名编号//传递this作为线程函数参数是因为_threadFunc为static,无法看见阻塞队列//传递this指针给线程调度函数时能获取threadPool对象_threads[i - 1] new Thread(线程名, 编号, _threadFunc, this);}pthread_mutex_init(_consum, nullptr);pthread_mutex_init(_prod, nullptr);pthread_cond_init(_cond, nullptr);}~ThreadPool(){for(size_t i0; i_num; i)//销毁线程{_threads[i]-join();delete _threads[i];}pthread_mutex_destroy(_consum);pthread_mutex_destroy(_prod);pthread_cond_destroy(_cond);}void pushTask(const T task)//获取任务至阻塞队列中{{...//加锁将任务写入生产队列中解锁}while(isFull()){...//当任务满时获取消费锁交换队列pthread_cond_signal(_cond);//向消费者发送信号..//解锁}}void start()//启动线程{for(size_t i 0; i _num; i){_threads[i]-run();//启动线程}}pthread_mutex_t _consum;pthread_mutex_t _prod;pthread_cond_t _cond;
private:queueT* _quC new queueT();//消费者队列queueT* _quP new queueT();//生产者队列size_t _num;//线程数量Thread* _threads[THREAD_NUM];//线程组
};二.拓展学习thrmgr线程池参考源码linux线程池thrmgr源码解析 - 一字千金 - 博客园 (cnblogs.com)相比于我们自制的线程池thrmgr提供了主动销毁线程池的函数并能防止出现某些线程长期得不到调度的情况。当然thrmgr相比于我们自制的线程池肯定还有不少丰富这里我们重点谈论这两个优势。首先自制的线程池采用析构函数销毁线程也就是RAII技术。但是thrmgr线程池提供了thrmgr_destroy函数用来销毁线程。当然这也是因为thrmgr并不是将调度函数封装在线程池类中而是像malloc和free一样利用函数接口的形式调用线程池。线程池结构体中记录了当前还有多少线程“存活”主线程调用thrmgr_destroy函数后首先向所有线程发送信号让其结束然后主线程阻塞在当前位置。当最后一个线程结束时会向主线程发送信号停止阻塞然后完成线程池资源的释放。thrmgr防止线程长期得不到调度的方式也很简单当线程没有任务时采用pthread_cond_timedwait函数阻塞等待该函数第三个参数为设定的timespec时间类型结构体当检测超时后自动停止阻塞并返回特定值根据返回值就能判断线程是否是因为超时而停止阻塞一旦判断超时线程跳出等待任务的循环然后结束本线程。四.其他线程安全问题一.单例模式单例模式分为懒汉和饿汉两种形式。懒汉是在使用单例是才分配空间饿汉是在程序加载时main函数启动之前就分配对空间。当使用饿汉模式时因为是在main函数之前此时只有一个线程因此不会存在线程安全问题。但是懒汉模式下如果是在线程调度的函数中才第一次使用单例那么就有可能有多个线程给单例分配空间。也就是说分配的空间就是一种临界资源。因此当使用懒汉模式时需要在分配空间时加锁。template typename T
class Singleton {
public:static T* GetInstance() {if (inst nullptr) { lock.lock(); //加锁保护if (inst nullptr) {//再次判断是否为空因为分配空间后其他线程可能取得锁进入临界区inst new T();}lock.unlock();}return inst;}
private:...//禁用拷贝构造等操作volatile static T* inst; // 设置 volatile 关键字防止编译器优化.static std::mutex lock;//互斥锁确保分配空间的原子性
};二.STL与智能指针STL并不是线程安全的因此需要使用者自己维护。对于智能指针而言unique_ptr因为只能有一个使用者因此不用考虑线程安全问题。shared_ptr内部在改变引用计数时标准库将其实现为原子性操作因此shared_ptr内部也是线程安全的。但是指针引用的空间在使用时并不是线程安全因此当使用shared_ptr时建议把它按照临界资源考虑正常上锁。三.读写者问题在实际开发中可能会有资源需要经常访问但是很少修改的情况比如网络小说就是会有大量读者频繁但是作者只有一位且对比阅读来讲修改次数极少。这时如果采用生产消费模型就不太合适了。此时的读者并不会修改数据也就是说消费者之间并没有互斥关系。因此不需要在消费者之间加互斥锁进而支持多线程同时访问临界资源提高程序效率。这就是读写者模型。同时默认情况下读者的优先级高于写者换句话说当读者线程与写者线程同时访问临界资源时会阻塞写者直到所有读者访问完毕。linux系统提供了读写锁和对应系统调用接口使用方式如下pthread_rwlock_t rwlock;//定义读写锁
pthread_rwlockattr_t attr;//定义读写锁属性读写者优先级
//自定义读写优先级默认读者优先级高
int pthread_rwlockattr_setkind_np(attr, int pref);
/*
pref 共有 3 种选择
PTHREAD_RWLOCK_PREFER_READER_NP 默认读者优先可能会导致写者饥饿
PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先目前有可能与默认情况一致的BUG
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先但写者不能递归加锁
*/
//初始化
int pthread_rwlock_init(rwlock, attr);
//销毁
int pthread_rwlock_destroy(rwlock);
//加锁和解锁
int pthread_rwlock_rdlock(rwlock);//读者加锁
int pthread_rwlock_wrlock(rwlock);//写者加锁
int pthread_rwlock_unlock(rwlock);//解锁简单是稳定的前提。— Edsger Dijkstra如有错误敬请斧正