线程概念
我们都知道,当一个程序运行起来的时候,系统会进行一系列操作,比如创建一个task_struct 结构体对象, 然后创建一块虚拟地址空间,在内存中开辟一块空间,并且用页表将虚拟地址空间映射到内存上去。
那什么是线程呢?
什么是线程
- 进程中的执行流。
- 一种轻量级进程,而在linux中系统是直接复用的PCB。
- 一般在进程内部运行,本质在进程的地址空间运行,没有自己的地址空间。
- 是CPU调度的基本单位。
现在我们知道了什么是线程,那么将线程与进程对比一下。
- 进程有独立的地址空间,而线程没有。
- 进程之间的地址空间不共享,而线程之间是共享地址空间的。
- 线程时系统调度的基本单位,而进程是资源分配的基本单位。
- 进程至少有一个执行线程(可以有多个),而线程只有一个主进程。
此外,我们知道线程之间共享地址空间,这是因为线程是在进程内部创建的,但是线程也有自己的数据:
- 线程ID
- 自己私有的上下文结构
- 一组寄存器
- 栈区
- errno
- 信号屏蔽字
- 调度优先级
而在同一个进程中的线程之间则共享以下数据:
- 文件描述符表
- 每种信号的处理方式
- 当前工作目录
- 用户id和组id
进程和线程的关系
说了这么多关于线程和进程,那么线程有什么用呢?
线程的优点
在图中,我们的进程是由一系列执行流+虚拟内存(mm_struct)+页表+在内存中开辟的空间组成。而创建一个线程则只需要创建新的PCB,并且在进程中给它分配它所需的资源即可,而不需要开辟的内存空间或是创建新的虚拟内存。
而在之前,我们学习进程切换的时候,我们需要切换PCB和mm_struct和物理地址,而线程切换则只用将执行流更换即可。
那么线程的优点就呼之欲出了。
- 创建的代价对比进程很小
- 线程之间的切换的工作量很少
- 线程占用的资源少
- 可以充分利用多处理器的并行功能
线程的缺点
线程当然不只有优点,它也有自己的缺点。
-
性能缺失
- 一个计算密集型线程无法和其他进程共享处理器,若计算密集型的线程的数量大于可用的处理器,会有较大的性能损失,增加了额外的同步和调度开销,但是可用资源不变。
-
健壮性降低
- 若是在一个多线程程序中,因时间分配上出现偏差或者共享了不可共享的数据时,会出现巨大错误。
-
缺乏访问控制
- 进程是访问控制的基本粒度,在一个线程中调用OS函数,会对整个进程造成影响。
-
编程难度提高
- 编写和调试一个多线程程序十分困难。
此外,由于线程是进程的执行流,若是一个线程出现异常,会导致该线程崩溃,从而整个进程崩溃。
并且若是单个线程出现野指针或者除零错误,也会导致进程崩溃。
线程的用途
- 合理使用多线程,可以提高CPU密集型程序的执行效率
- 合理使用多线程,可以提高IO密集型程序的用户体验。
线程的用途我们平时都有接触过,比如用浏览器一边搜索一遍下载文件,这些都是线程的用途。
线程控制
在上面我们提到过linux中的线程是直接复用的PCB,从而linux中并没有真正的进程。因此linux无法提供直接创建线程的系统调用接口,而只能提供调用轻量级进程的接口。
但是程序猿和操作系统们都只认线程,于是linux提供了一个库——用户级线程库。
程序猿们通过调用该库中的函数来创建线程,然后库中的函数再创建轻量级进程。
而我们想要进行线程控制等操作,就必须在编译的时候连接该线程库,否则无法创建线程。
连接线程库只需要在编译后面加上 '-lpthread'或者'-pthread'即可,之后在文件中包含对应的头文件即可。
在知道如何连接线程库后,我们再来看看如何创建线程。
创建线程
- 成功时返回0,失败返回错误号。
- 第一个参数 pthread_t* 是一个传出参数,用来保存分配好的线程ID。
- 第二个参数是线程的属性,通常传NULL,表示使用默认属性。
- 第三个参数是一个函数指针,也是该线程所执行任务。
- 第四个参数是一个万能接收参数,可以用结构体封装自己需要传入的参数。
接下来我们看看具体创建线程的过程吧。
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdio>
#include<cassert>
using namespace std;void* start_routine(void* args)
{while(true){sleep(1);cout<<"I am pthread"<<endl;}}int main()
{pthread_t pt;int time = 0;pthread_create(&pt,NULL,start_routine,NULL);while(true){cout<<"I am main thread"<<endl;sleep(1);}//主进程退出了,线程也会退出return 0;
}
一般而言,一个进程内部只能有一个循环执行,而这里有两个循环,就说明成功创建了线程。
而在该程序运行的时候,我们使用 'ps -aL' 命令,能够查看到所有轻量级进程。
我们发现,查看到的数据除了PID这个老熟人,还有一个 LWP — 轻量级进程ID。
而CPU调度的时候,就是以 LWP 为标识符表示特定的执行流。
需要提一下的是,此处的LWP和函数的第一个参数并不是同一个ID。
- LWP是进程用来标示该线程的ID,方便系统调度。
- thread是 pthread_create 第一个参数,指向一个虚拟内存单元,该单元地址就是该线程的ID。
phtread_self函数
- 返回调用该函数的线程的线程ID,也就是该线程的所处的空间地址。
需要注意的是,线程是由线程库提供的函数和结构来描述的,创建一个线程也是需要经过库的。
而我们每次创建一个线程都是在库中创建,操作系统会通过mmap区来找到对应的线程。
线程ID就是该线程在库中的地址,而线程的独立的栈也是在库中创建的。
pthread_exit函数
- 传递的参数必须是全局或者是动态分配的,否则会导致传递失败。
- 想要单独退出某一个线程必须使用该函数,使用exit会导致整个进程退出。
pthread_cancel函数
- 将想关闭的线程的id传递进去,就能够成功关闭。
pthread_join函数
- 成功返回0,错误返回错误码。
- 第一个参数是被回收线程的线程id。
- 第二个参数是用来接收线程运行函数的返回值。
线程也是需要等待的,若不等待就会造成内存泄漏,因此每一个进程都需要使用该函数来等待。
并且不同方式终止的线程,用pthread_join得到的终止状态是不同的。
- 线程通过return返回,则retval中存储的是线程函数返回值。
- 线程是被别的线程调用pthread_cancel终止掉,则retval中储存的是PTHREAD_CANCELED
- 线程是自己调用pthread_exit终止的,则retval中存储的是传给pthread_exit的参数。
- 若是对线程终止状态不感兴趣,可以传NULL给retval。
pthread_detach函数
- 成功返回0,错误返回错误码。
- 默认情况下,线程是joinable的,若没对线程进行join时,就会导致资源泄露。
- 当不关心某一线程的返回值时,可以使用该函数。将对应的线程id传进去,即可分离该线程。
- 线程join和线程分离是冲突的,分离过的函数不可join。
- 可以线程本身进行分离,也可其他线程进行分离。
- 使用分离就不用担心线程进入僵尸状态
线程互斥量
线程之间共享一块地址空间,也就是说它们之间的全局变量都是一样的,而这就引发了线程安全问题。
比如我们写下这样的代码,多个线程共享一个全局变量。
#include <iostream>
#include <pthread.h>
#include <unistd.h>using namespace std;int g_val = 1000;void *start_routine(void *args)
{while (1){if (g_val > 0){usleep(100);cout << "I am pthread process,g_val : " << g_val << " pid : " << pthread_self() << endl;g_val--;}elsebreak;}
}int main()
{pthread_t pid1, pid2, pid3, pid4,pid5,pid6;pthread_create(&pid1, NULL, start_routine, NULL);pthread_create(&pid2, NULL, start_routine, NULL);pthread_create(&pid3, NULL, start_routine, NULL);pthread_create(&pid4, NULL, start_routine, NULL);pthread_create(&pid5, NULL, start_routine, NULL);pthread_create(&pid6, NULL, start_routine, NULL);pthread_join(pid1, NULL);pthread_join(pid2, NULL);pthread_join(pid3, NULL);pthread_join(pid4, NULL);pthread_join(pid5, NULL);pthread_join(pid6, NULL);return 0;
}
根据逻辑当g_val = 0时就会退出,但是实际上并没有。
反而是到了负数还没有退出。
这就是线程安全问题,我们可以给将全局变量前加上"__thread",将这个全局变量变成线程局部可见的变量,这样每个线程都会有这个变量,不会互相打扰。
- 这种现象一般是多线程频繁切换时会发生。
- 当线程时间片到了,或者来了更高优先级线程时,会发生线程切换。
- 当线程从内核态返回用户态时就会检查这个调度状态,可以切换就会切换线程。
上述代码发生的原因就是线程频繁切换,当g_val为1,线程A进入if条件后,线程A时间片到了,线程A被切换成线程B,但是g_val依旧是1,然后线程B进入 if 条件后,继续后面的操作,将g_val减到0;
然后线程B被切换了,线程A带着自己的上下文又被切回来了,它已经进入到if条件内部了,于是线程A也继续对g_val减1,于是g_val成了负数。
这就是线程安全问题,而为了解决该问题,就出现了线程互斥的概念。
互斥量
想要了解互斥量,我们需要明白一些概念。
- 多个执行流安全访问的共享资源就是临界资源。
- 多个执行流中访问临界资源的代码就是临界区。
- 让多个执行流串行的访问共享资源就是互斥。
- 对一个资源进行操作时,要么不做,要么做完——原子性。
而我们的互斥量就是保证执行流串行的变量,而这个变量我们叫做锁——互斥锁。
互斥锁
互斥锁是一个变量——mutex,它有两种初始化方式。
而锁这个东西也需要手动释放。
当我们初始化一个锁后,就能够使用这个锁的lock和unlock函数,来保护我们的共享资源。
使用这个lock函数有几点需要注意
- 当这个互斥量未上锁时,该函数会锁定该互斥量,返回成功
- 若是该互斥量已上锁了,那么该函数就会陷入阻塞,等待该互斥量解锁
当我们使用互斥量对上述代码进行加锁后,就不会出现这样的问题了。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>using namespace std;int g_val = 10000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;void *start_routine(void *args)
{string name = static_cast<char *>(args);while (1){pthread_mutex_lock(&lock);if (g_val > 0){cout << "I am pthread process,g_val : " << g_val << " " << name << endl;g_val--;pthread_mutex_unlock(&lock);usleep(500);}else{pthread_mutex_unlock(&lock);break;}}
}int main()
{pthread_t pid1, pid2, pid3, pid4, pid5, pid6;pthread_create(&pid1, NULL, start_routine, (void *)"thread1");pthread_create(&pid2, NULL, start_routine, (void *)"thread2");pthread_create(&pid3, NULL, start_routine, (void *)"thread3");pthread_create(&pid4, NULL, start_routine, (void *)"thread4");pthread_create(&pid5, NULL, start_routine, (void *)"thread5");pthread_create(&pid6, NULL, start_routine, (void *)"thread6");pthread_join(pid1, NULL);pthread_join(pid2, NULL);pthread_join(pid3, NULL);pthread_join(pid4, NULL);pthread_join(pid5, NULL);pthread_join(pid6, NULL);return 0;
}
当所有进程共用一个锁时,它们调用lock函数会导致互相竞争,只有竞争成功的线程才能访问临界资源,其他进程都只能挂起等待解锁,然后再次竞争。
而即便访问临界区的线程被切走了,它也是带着锁被切走的,其他线程也只能在申请锁那里阻塞等待。
通过这里我们得知,++或者--在代码中或许只有一条,但是它们并不是原子性的,而锁能够做到保护临界资源,就是因为它的所有操作都是原子的,那么它是怎么做到的呢?
- 为了实现互斥量的功能,大部分都是通过swap或者exchange指令,来把寄存器中的数据和内存单元的数据进行交换,由于只有一条指令,因此保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
我们的lock和unlock就是通过这样的指令来完成上锁的。
- lock:首先我们的锁初始化后,它的值为1,而lock函数首先用一个寄存器,里面存0,然后通过xchgb指令,将寄存器和锁的内容进行交换,于是锁为0,寄存器为1,之后便只用判断寄存器的值即可上锁。
- unlock:解锁过程则是相反,直接使用movb指令将1放入锁中,然后唤醒其他等待的进程,最后返回0。
讲完锁后再讲点用锁的小细节。
- 保证临界区的粒度很小,也就是说临界区中只保留必要的资源,能够提高效率。
- 加锁行为必须一视同仁,访问公共资源的线程必须都要加锁。
线程安全和可重入
由于线程之间共享资源,因而出现了线程安全和可重入问题,这里稍微了解下。
线程安全:多个线程并发同一段代码时,不会出现不同的结果。
重入:同一个函数被不同的执行流调用,前一个流程还未完成,后一个执行流就再次进入。
- 可重入:一个函数在重入的情况下,结果不会不同且没有任何问题。
- 不可重入:一个函数在重入的情况下,结果不同或者有问题。
常见的线程不安全情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
常见的线程安全情况
- 线程对全局变量或者静态函数只可读取,不可写入
- 类或者接口都是原子操作
- 多个线程切换不会导致该接口的执行结果存在二义性
常见的不可重入的函数
- 调用了malloc/free函数,因为malloc是采用全局链表管理堆的
- 调用了I/O标准库函数,因为该库中很多实现都是以不可重入方式实现的
- 可重入函数体使用了静态数据结构
常见的可重入的函数
- 不使用全局变量或静态变量
- 不使用用malloc或者new开辟出的空间
- 不调用不可重入函数 不返回静态或全局数据,所有数据都有函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
线程安全和可重入的关系
- 函数是可重入的,那就是线程安全的
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
线程安全和可重入的区别
- 可重入函数是线程安全函数的一种
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
死锁
在由于互斥锁的出现,线程又出现了新的问题——死锁问题。
死锁:进程中的各个线程均占有不可释放的资源,又互相申请被其他线程占用不会释放的资源而造成永久等待状态
而造成死锁出现的条件有四个。
死锁出现的条件
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
那么如何避免死锁
避免死锁
- 破坏死锁的四个条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性释放
死锁是多线程场景中常见的问题,需要注意。
线程同步
在之前的场景中,我们解决了线程之间互相访问共享资源从而导致的线程安全问题。
但是我们发现,这种互斥的多线程的线程之间是无序的,并且很有可能会导致一个竞争力强的线程反复访问临界资源,这样多线程的优势就体现不出来,于是程序猿们就发明了线程同步的概念和方案。
线程同步
- 在保证数据安全的前提下,让线程可以按照某中特定的顺序访问临界资源,可以有效避免饥饿问题
- 竞太条件:计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件
而线程同步的方案我们有条件变量。
条件变量
初始化和销毁
- 参数一就是需要初始化的条件变量的地址
- 参数二是设置条件变量属性,一般为NULL
- 和互斥量相同的是,我们的条件变量也可以设置为全局变量,这样就不用初始化和销毁了
- 使用完后的条件变量就需要调用destroy进行销毁
条件变量等待
- 条件变量实现同步最主要的函数之一便是等待函数
- 参数一是用来等待的条件变量
- 参数二是用来等待的锁
条件变量唤醒
- 用来唤醒被等待的条件变量的函数即是这两个
- 参数都是等待的条件变量
- signal一次唤醒一个线程
- broadcast一次全部唤醒
了解完条件变量的主要函数后,先来看看如何实现同步的吧。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>
#include <stdio.h>using namespace std;int g_val = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;void *start_routine(void *args)
{string name = static_cast<char *>(args);while (1){pthread_mutex_lock(&lock);if (g_val > 0){pthread_cond_wait(&cond, &lock);cout << "I am pthread process,g_val : " << g_val << " " << name << "id " << pthread_self() << endl;g_val--;pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock);break;}}
}int main()
{pthread_t t[6];for (int i = 0; i < 6; i++){char* name = new char[64];snprintf(name, 64, "thread %d", i + 1);pthread_create(&t[i], NULL, start_routine, name);}while (1){pthread_cond_signal(&cond);cout << "main thread wakeup one thread..." << endl;sleep(1);}for (int i = 0; i < 6; i++){pthread_join(t[i], NULL);}return 0;
}
通过条件变量,我们发现成功使得线程以一定的顺序来进行任务。
但是条件变量的使用需要有一定的规范。
条件变量使用规范
- 必须要有一个线程来对条件变量进行解锁,否则会一直卡住。
在之前发现,wait函数需要配合mutex互斥量才能使用,但是为什么需要有互斥量呢?
pthread_cond_wait需要mutex的原因
首先我们先明确wait函数的作用。
wait函数的作用
- 首先释放mutex
- 然后阻塞,等待别的线程唤醒
- 被唤醒后让该线程获取mutex
那么如果wait函数是这样的作用,那为啥不去掉mutex参数,这样写呢?
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
就像这样的场景,我们先在条件外加锁,然后进入条件后便解锁,然后调用wait函数,最后再重新加锁和解锁。
而这样就出现了一个空档期,因为这个条件变量内部的wait函数没有互斥量保护,那么当解锁后,而wait阻塞之前,也许线程就被替换了,轮到另外一个线程工作,而这时用来唤醒其他线程的唤醒进程发出唤醒信号,但是这时线程已经被替换了,就会导致错过wait信号,从而可能导致线程被阻塞堵死。
因此wait函数一定需要互斥量才能保证不出错。
基于BlockingQueue的生产消费模型
阻塞队列的生产消费模型是基于生产者和消费者的数据结构,和普通队列相比,在它内部队列为空时,消费者线程进行取结果的操作会被阻塞,而内部队列满时,生产者线程进行存任务的操作就会被阻塞。
该消费模型能够将生产者和消费者解耦,将生产任务和消费任务分开,并且可以同步执行多个任务或者同步生产多个任务。当任务所需耗时越来越长,那么这个模型的高效之处就更加明显了
通过了解该生产消费模型我们能明白,实现该模型我们需要用到条件变量。不过此处只是提一嘴,具体代码实现在另一篇博客中。
POSIX信号量
在我们的基于阻塞队列的生产消费模型之中,有一个问题,就是利用条件变量实现同步,会将一整个队列看成一个整体,上锁时就一并上了锁,但是实际上可能一个线程只需要访问该队列中的一小部分的资源,为了解决该问题,我们引入了信号量。
信号量概念:信号量实际上是一个计数器,是描述临界资源中资源数目的计数器,信号量能够更细粒度的对临界资源进行管理。
首先我们先看看信号量的具体相关函数。
信号量的初始化
- 参数一是需要初始化的信号量的地址
- 参数二是一个整数,0表示线程间共享,非0表示进程间共享
- 参数三是一个整数,表示信号量的初始值
等待信号量
- 参数是表示需要等待的信号量
- 会使信号量-1
- 信号量为0会阻塞
释放信号量
- 参数是表示需要释放的信号量
- 会使信号量+1
接下来我们试试通过该信号量实现线程互斥和同步功能。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <string>
#include <stdio.h>using namespace std;int g_val = 1000;
sem_t sem;void *start_routine(void *args)
{string name = static_cast<char *>(args);while (1){sem_wait(&sem);if (g_val > 0){cout << "I am pthread process,g_val : " << g_val << " " << name << "id " << pthread_self() << endl;g_val--;usleep(500);sem_post(&sem);}else{sem_post(&sem);break;}}
}int main()
{pthread_t t[6];for (int i = 0; i < 6; i++){char* name = new char[64];snprintf(name, 64, "thread %d", i + 1);pthread_create(&t[i], NULL, start_routine, name);}sem_init(&sem,0,6);//同时可以有六个运行for (int i = 0; i < 6; i++){pthread_join(t[i], NULL);}return 0;
}
我们发现,使用了信号量后,不仅实现了多线程的互斥功能,也实现了多线程的同步功能。
基于环形队列的生产消费模型
该生产消费模型是通过使用数组模拟环状数组来实现的。
在一个数组上通过两个信号量分别记录空间资源和数据资源的多少,从而实现该模型。
同样的,该模型在此不详细叙述,详细代码会在另一篇博客。
线程池
在多线程的场景下,我们可能会碰到需要创建多个线程的任务,但是若是想在短时间内开辟很多个线程是十分耗时费力的,因此设计出线程池来同时维护多个线程,这样需要使用时能够随时调度池内线程进行任务。
线程池:
- 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。
- 线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。
- 避免了在处理短时间任务时创建与销毁线程的代价。
- 线程池不仅能够保证内核的充分利用,还能防止过分调度。
- 可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
线程池应用场景
- 需要大量的线程来完成任务,且完成任务的时间比较短。
- 对性能要求苛刻的应用。
- 接收突发性的大量请求,但不至于使服务器因此产生大量线程的应用。
作为池化技术的一种,线程池为多线程场景提供了很好的环境,也更加便于管理员管理线程
线程安全的单例模式
单例模式:
只有一个对象的模式。
在很多服务器开发场景中,经常需要加载很大的数据加载到内存中去,此时就需要单例模式登场了。
而在单例模式中,最为出色的莫过于懒汉模式和饿汉模式。
饿汉模式实现
template<class T>class Singleton{static T data;public:static T* getInstance(){return &data;}
};
简单来说饿汉模式就是该类在一开始就创建好一个对象,随时都可以使用。
懒汉模式实现
template<class T>
class Singleton{volatile static* T data;static mutex mutex;public:static T* getInstance(){if(data == NULL){mutex.lock();if(data == NULL){data = new T();}mutex.unlock();}return data;}
};
懒汉模式就是典型的“延时加载”,能够优化服务器的启动速度。
只有在懒汉模式使用的时候,才会创建一个对象;而且由于是动态开辟的,因此懒汉模式需要使用互斥锁来防止冲突,并且内部需要两次判空才能保证不重复开辟空间。
STL,智能指针和线程安全
STL容器并不是线程安全的。
因为STL容器初衷是为了将性能开辟到极致,加锁解锁会影响到容器的性能。
其次,STL不同的容器需要不同的加锁方式,不同的加锁方式也会影响到性能。
因此STL默认线程不安全。
智能指针
unique_ptr只在当前代码块生效,因此不涉及线程安全问题。
而shared_ptr需要引用计数,因此存在线程安全问题,不过标准库基于原子操作的方式保证shared_ptr能够高效的使用原子的引用计数。
其他常见的锁
- 悲观锁:每次取数据时,都认为数据会被其他线程修改,因此在取数据前就会加锁。
- 乐观锁:每次取数据时,都认为数据不会被其他线程修改,因此不会加锁,但是在更新时,会进行比较(通过CAS和版本号机制)。
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。相等更新,不等则失败,重试,一般是一个自旋的过程。
读写锁
在生活中,我们常遇到这样的问题:相比与修改操作,更多的是读取的操作更多,若是给读操作上锁,则会降低程序的效率,因此我们需要读写锁来避免这种情况。
- 注意:写独占,读共享,读锁优先级高。
写独占:当线程加写锁成功,其他线程不可读加锁,不可加写锁。
读共享:当线程加读锁成功,其他线程也可以加读锁,但不可加写锁。
初始化读写锁
#include<iostream>
#include<pthread.h>
#include<vector>
#include<unistd.h>
using namespace std;int ticket = 1000;
pthread_rwlock_t rwlock;
vector<int> v;
int i = 0;
void* wfunc(void* args)
{while(1){pthread_rwlock_wrlock(&rwlock);v.push_back(i);i++;cout<<"writeing..."<<endl;pthread_rwlock_unlock(&rwlock);sleep(1);}
}void* rfunc(void* args)
{while(1){pthread_rwlock_rdlock(&rwlock);for(int i = 0;i<v.size();i++){cout<<v[i]<<" ";}cout<<endl;pthread_rwlock_unlock(&rwlock);sleep(1);}
}int main()
{pthread_t w[5];pthread_t r[5];pthread_rwlock_init(&rwlock,NULL);for(int i = 0;i<5;i++){pthread_create(w+i,NULL,wfunc,NULL);pthread_create(r+i,NULL,rfunc,NULL);}for(int i = 0;i<5;i++){pthread_join(w[i],NULL);pthread_join(r[i],NULL);}return 0;
}
我们通过读写锁,发现每次写时,没有线程读,而线程读时,也没有线程写。
读写锁能够很好的将读操作和写操作分开,面对读操作远多于写操作时,使用读写锁是个很好的选择。