目录
一、Linux线程概念
二、线程的特性
1、线程的优点
2、线程的缺点
3、线程异常
4、线程用途
三、进程与线程
四、Linux线程控制
1、创建线程
2、线程退出
3、等待线程
4、线程取消
5、其他接口
5.1、获取自己的线程id
6、线程分离
五、线程库
六、线程互斥
1、进程线程间的互斥相关背景概念
2、互斥量
3、互斥量实现原理
七、封装
1、线程封装
2、锁的封装
八、线程安全
1、概念
2、常见的线程不安全的情况
3、常见的线程安全的情况
4、常见不可重入的情况
5、常见可重入的情况
6、可重入与线程安全联系
7、可重入与线程安全区别
九、死锁
十、线程同步
1、同步与竞态
2、条件变量
2.1、条件变量接口
2.2、为什么 pthread_cond_wait 需要互斥量
2.3、条件变量使用规范
十一、生产者消费者模型
十二、线程池
1、线程池实现简易代码-版本一
2、自己封装线程和锁实现线程池-版本二
3、自己封装线程和锁实现单例模式线程池-版本三
十三、线程安全的单例模式
1、饿汉方式实现单例模式
2、懒汉方式实现单例模式
3、懒汉方式实现单例模式(线程安全版本)
十四、STL,智能指针和线程安全
1、STL中的容器是否是线程安全的
2、智能指针是否是线程安全的
十五、其他常见的各种锁
十六、读者写者问题
1、读写锁
2、 读写锁接口
一、Linux线程概念
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
在CPU中有多个寄存器,有的寄存器中保存当前进程的PCB,有的寄存器保存当前进程的页表,还有一些寄存器指向当前进程执行的指令、代码、堆栈等等。当切换进程时,只需要把这些寄存器中的内容保存并切换,就可以换到另一个进程。
现在一个进程有多个PCB结构 task_struct ,都指向同一个地址空间。把地址空间中例如代码区的代码划分成多个部分,每一个 task_struct 在未来执行时,都执行同一个地址空间的不同部分的代码。这样一个进程中就存在了多个执行流,我们称每一个 task_struct 都是一个单独的线程。因此线程是一个执行分支,执行粒度比进程更细。
CPU在执行OS的代码切换 task_struct 时,由于这些 task_struct 都指向同一个地址空间,所以就不需要再更新寄存器中保存的地址空间和页表了。CPU内部有运算器、控制器、寄存器、MMU、硬件cache等等。其中 硬件cache 是高速缓存,它会把一部分热点数据预先加载进来,并在接下来的执行中有很大的概率命中这些数据,从而提高整机的效率。所以在多线程执行代码和数据时,cache中会缓存各种数据,当CPU在切换执行流时,由于进程没变,所以缓存内容也不变。如果进程改变,就需要把cache中缓存的数据设置为无效,并重新加载新进程的代码和数据。因此,线程的调度成本更低,主要体现在不用对cache进行切换。
所以现在重新理解进程,进程应该包含一批执行流、地址空间、页表以及代码和数据。进程是承担分配系统资源的基本实体。线程是CPU调度的基本单位。
在Windows操作系统,内核中有真线程,名为TCB :线程控制块。因为 TCB 属于 PCB,所以还需要维护进程与线程之间的调度关系算法,这过于复杂。
在Linux中,由于线程的控制块与进程控制块相似性非常高,所以直接复用了PCB的结构体,用PCB模拟线程的TCB。所以Linux没有真正意义上的线程,而是用进程方案模拟的线程。这样做的好处是复用代码和结构更简单,好维护,效率更高,也更安全。
二、线程的特性
1、线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
线程不是越多越好,正常情况下最合适的原则是:进程/线程与cpu个数/核数保持一致 - I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程不是越多越好,但是比计算密集型应用可以多一些
2、线程的缺点
- 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。 - 健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。 - 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。 - 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
3、线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
4、线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
三、进程与线程
进程是资源分配的基本单位。线程是调度的基本单位。线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器:保存当前线程所执行的上下文数据,体现了线程的切换特性。
- 栈:线程在执行时,也会在内部形成临时变量,为了保证每一个执行流之间的数据不会互相干扰,就需要拥有自己独立的栈。体现了线程的临时运行特性。
- errno
- 信号屏蔽字
- 调度优先级
进程的多个线程共享同一地址空间,因此Text Segment(代码区)、Data Segment(数据区)都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
进程和线程的关系如下图:
四、Linux线程控制
在Linux操作系统的视角,Linux下没有真正意义上的线程,而是用进程模拟的线程(LWP)。所以,Linux不会提供直接创建线程的系统调用,而是提供创建轻量级进程的接口。但是由于用户只认线程,所以库会对下将Linux接口封装,对上给用户提供线程控制的接口,这种库被称为用户级线程库。 pthread 库在任何系统都要自带,因此也称原生线程库。
1、创建线程
创建线程函数:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
pthread_create 函数不是系统调用接口,而是一个库函数。
pthread_create 函数的参数列表中, thread 是线程id。 attr 是线程属性,一般设置为nullptr。函数指针用来回调式的执行目标函数。 arg 是传递给回调函数的参数。线程创建成功返回0,失败返回-1,并且对全局变量 errno 赋值以指示错误。
创建线程代码:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;void* thread_run(void* args)
{while(true){cout << "new thread running" << endl;sleep(1);}return nullptr;
}int main()
{pthread_t t;pthread_create(&t, nullptr, thread_run, nullptr);while(true){cout << "main thread running, new thread id: " << t << endl;sleep(1);}return 0;
}
因为 pthread_create 函数是库函数,所以在编译的时候需要加上库选项:
g++ -o $@ $^ -std=c++11 -lpthread
编译后查看库:
可以看到pthread库存在的位置。
运行程序:
这两个线程的PID是相同的,LWP不同。
如果需要向线程执行的函数传递参数,则可以使用如下方式:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;#define NUM 10void* thread_run(void* args)
{char* name = (char*)args;while(true){cout << "new thread running, my name is: " << name << endl;sleep(1);}delete name;return nullptr;
}int main()
{pthread_t tids[NUM];for(int i = 0; i < NUM; ++i){char* tname = new char[64];//char tname[64] //这种写法不行,因为线程传参时,传递的是数组的地址,随着数组被重写,线程读取到的值也被更新了,无法达到预期效果snprintf(tname, 64, "thread-%d", i + 1);pthread_create(tids + i, nullptr, thread_run, tname);}while(true){cout << "main thread running" << endl;sleep(1);}return 0;
}
运行观察结果:
因为参数 arg 是一个空指针类型,所以不只是可以传递整型、字符型变量,还可以传递类:
class ThreadData
{
public:ThreadData(const string& name, int id, time_t createTime): _name(name), _id(id), _createTime(createTime){}~ThreadData(){}public:string _name;int _id;uint64_t _createTime;
};void* thread_run(void* args)
{ThreadData* td = static_cast<ThreadData*>(args);while(true){cout << "thread is runing, name is: " << td->_name << " create time: " << td->_createTime << " index: " << td->_id << endl;sleep(1);break;}delete td;return nullptr;
}int main()
{pthread_t tids[NUM];for(int i = 0; i < NUM; ++i){char tname[64];snprintf(tname, 64, "thread-%d", i + 1);ThreadData* td = new ThreadData(tname, i + 1, time(nullptr));pthread_create(tids + i, nullptr, thread_run, td);sleep(1);}while(true){sleep(1);}return 0;
}
运行观察结果:
2、线程退出
如果线程执行 return 指令退出时,只有当前线程会退出。如果线程执行 exit 指令退出时,整个进程都会退出,即全部线程都会退出。
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用 pthread_ exit 终止自己。
- 一个线程可以调用 pthread_ cancel 终止同一进程中的另一个线程。
线程退出函数:
void pthread_exit(void *retval);
pthread_exit 函数的参数列表中, retval 是一个输出型参数,用于取出新线程退出的相关结果。
pthread_exit((void*)1);
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的,或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
3、等待线程
注意:主线程退出,全部线程退出。因此新线程创建出来后,需要被主线程等待,否则会出现类似于僵尸进程的问题。
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
等待线程函数:
int pthread_join(pthread_t thread, void **retval);
pthread_join 函数的参数列表中, thread 是线程id。 retval 是一个输出型参数,用于取出新线程退出的相关结果。线程等待成功返回0,失败返回错误码 errno 。
等待线程退出代码:
void* ret = nullptr;for(int i = 0; i < NUM; ++i)
{int n = pthread_join(tids[i], &ret);if(n != 0) cerr << "pthread_join error" << endl;cout << "thread quit: " << (uint64_t)ret << endl;
}
与等待进程退出不同,waitpid 的参数中,status里不光包含子进程退出的结果,还包含进程信号。而 pthread_join 函数的参数中,只有新线程的退出结果,并不关心信号,是因为当一个线程出现异常接收到信号时,所有的线程都会退出,主线程也会退出,也就等不到信号了。
因为参数 ret 是一个空指针类型,所以不只是可以传递整型、字符型变量,还可以传递类:
#define NUM 10enum
{OK=0,ERROR
};class ResultData
{
public:ResultData(int top):_status(OK),_top(top),_result(0){}~ResultData(){}
public:int _status;int _top;int _result;
};void* thread_run(void* args)
{ResultData* rd = static_cast<ResultData*>(args);for(int i = 1; i < rd->_top; ++i){rd->_result += i;}cout << " cal done!" << endl;pthread_exit(rd);
}int main()
{pthread_t tids[NUM];for(int i = 0; i < NUM; ++i){char tname[64];snprintf(tname, 64, "thread-%d", i + 1);ResultData* rd = new ResultData(100 + i * 5);pthread_create(tids + i, nullptr, thread_run, rd);sleep(1);}void* ret = nullptr;for(int i = 0; i < NUM; ++i){int n = pthread_join(tids[i], &ret);if(n != 0) cerr << "pthread_join error" << endl;ResultData* rd = static_cast<ResultData*>(ret);if(rd->_status == OK){cout << "[1, " << rd->_top << "] 计算的结果是:" << rd->_result << endl;}delete rd;}cout << "all thread quit..." << endl;return 0;
}
运行观察结果:
可以通过 retval 参数检测线程运行的结果。
4、线程取消
如果thread线程被别的线程调用 pthread_ cancel 异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED 。
线程取消函数:
int pthread_cancel(pthread_t thread);
编写代码:
void* threadRun(void* args)
{const char* name = (const char*)args;int cnt = 5;while(cnt--){cout << name << " is running: " << cnt << endl;sleep(1);}pthread_exit((void*)11);
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");sleep(3);pthread_cancel(tid);void* ret = nullptr;pthread_join(tid, &ret);cout << " new thread exit: " << (int64_t)ret << endl;return 0;
}
运行观察结果:
可以看到线程退出码是 -1 , 代表线程被取消。
5、其他接口
5.1、获取自己的线程id
获取自己的线程id的函数:
pthread_t pthread_self(void);
6、线程分离
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
当线程已经被分离,却仍然被join时,则pthread_join函数会调用失败。
线程分离函数:
int pthread_detach(pthread_t thread);
编写代码:
void* threadRoutine(void* args)
{string name = static_cast<const char*>(args);int cnt = 5;while(cnt){cout << name << " : " << cnt-- << endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");pthread_detach(tid);int n = pthread_join(tid, nullptr);if(n != 0)cerr << "error: " << n << " : " << strerror(n) << endl;sleep(10);return 0;
}
在主线程中对新线程进行分离,并在分离后继续join新进程,运行观察结果:
发现pthread_join函数立刻调用失败,返回错误码,并且执行sleep指令暂时不退出。而新线程继续正常执行。如果不在主线程中写sleep指令,则主线程会立刻退出,并连带所有线程退出。
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
编写代码:
void* threadRoutine(void* args)
{pthread_detach(pthread_self());string name = static_cast<const char*>(args);int cnt = 5;while(cnt){cout << name << " : " << cnt-- << endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");int n = pthread_join(tid, nullptr);if(n != 0)cerr << "error: " << n << " : " << strerror(n) << endl;return 0;
}
编译运行:
发现结果与线程没分离时一致,这是因为线程被创建出来后,谁先调度是由调度器决定的。于是虽然新线程被创建出来了,但还没来的及调度执行分离函数,就被主线程先执行join函数等待了。为了避免这个问题,在创建完新线程后,主线程等待1秒再开始执行:
运行观察结果:
结果符合预期。
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
五、线程库
我们知道,进程中的线程可以随时访问库中的代码和数据。所以关于线程管理的代码,比如线程切换的代码就可以放如库中,由库对线程进行管理。
库对线程的管理也是依照先描述再组织的原则实现的。当创建进程时,OS会在内核中创建相应的数据结构 LWP ,并使用库来封装管理,形成用户层的 TCB 结构。TCB与LWP的关系,可以对照文件系统的 struct FILE 和内核中文件描述符表的关系来理解。
线程库对线程先描述在组织,描述组织的方式如下图所示:
mmap 区域是共享区。动态库中有多个为线程创建的描述结构体,等同于 TCB 结构。结构体中主要有三个字段,其中第一个字段:struct pthread 包括了线程的属性,第二个字段:线程局部存储用于保存用 __thread 修饰的全局变量,第三个字段:线程栈保存本线程产生的临时数据。
为了能够找到这些描述结构体,就把这些结构体的起始地址保存起来,形成 pthread_t 类型的线程ID,用于标识线程相关属性集合的起始地址。
所有线程都要有自己独立的栈结构,主线程用的是进程系统栈,新线程用的是库中提供的栈。
我们创建新线程时,使用的 pthread_create 函数就是线程库的库函数,在库函数中封装了系统调用 clone :
int clone(int (*fn)(void *), void *child_stack,int flags, void *arg, .../* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
在调用 pthread_create 函数时,会把回调函数的指针传递给系统调用 clone ,并且在库中创建新线程的结构体,并把线程栈地址传给clone的参数 child_stack 。 在调用不同的线程时,通过更新寄存器 esp、ebp 的值就可以实现栈的切换,关于栈帧的内容可以参考文章《函数栈帧的创建与销毁》。因此就算多个线程共同调用同一个函数,因为线程栈不同,所创建的临时变量的地址也不同。
六、线程互斥
1、进程线程间的互斥相关背景概念
- 临界资源:任何一个时刻,都只允许一个执行流进行访问的共享资源。
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
- 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。比如对变量val实现++操作,转换成汇编实际上是三行代码:把变量加载到寄存器,进行++,把结果返回变量,原子性就是这三行代码要么一个都不做,要么都做完。
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。多个线程并发的操作共享变量,会带来一些问题。
为了避免这种问题,需要做到以下三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
2、互斥量
对共享资源进行加锁操作相关函数:
#include <pthread.h>int pthread_mutex_destroy(pthread_mutex_t *mutex);int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
函数参数列表中的类型 pthread_mutex_t 称为互斥锁。定义完互斥锁后,使用函数 pthread_mutex_init 对互斥锁初始化,使锁变为可工作状态。锁用完之后,需要使用 pthread_mutex_destroy 函数销毁锁。
初始化锁还有一种静态分配的方法:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁。
#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_trylock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex);
当锁变为可用状态后,需要使用 pthread_mutex_lock 函数对相关线程进行加锁操作。返回值:成功返回0,失败返回错误号。一旦加锁成功,就可以继续执行对应的执行流,如果加锁失败,则会把对应执行流阻塞住。执行流完成后,需要使用 pthread_mutex_unlock 函数进行解锁。
具体用法如下:
#include <iostream>
#include <cstring>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <pthread.h>using namespace std;int tickets = 1000; //加锁保护class TData
{
public:TData(const string &name, pthread_mutex_t* mutex):_name(name),_pmutex(mutex){}~TData(){}
public:string _name;pthread_mutex_t* _pmutex;
};void* threadRoutine(void* args)
{TData* td = static_cast<TData*>(args);while(1){pthread_mutex_lock(td->_pmutex); // 所有线程都要遵守这个规则if(tickets > 0){usleep(2000);//模拟抢票花费的时间cout << td->_name << " get a tickets: " << tickets-- << endl;pthread_mutex_unlock(td->_pmutex);}else{pthread_mutex_unlock(td->_pmutex);break;}usleep(13);}return nullptr;
}int main()
{pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr);pthread_t tids[4];int n = sizeof(tids)/sizeof(tids[0]);for(int i = 0; i < n; ++i){char name[64]; snprintf(name, 64, "thread-%d", i + 1);TData* td = new TData(name, &mutex);pthread_create(tids + i, nullptr, threadRoutine, td);}for(int i = 0; i < n; ++i){pthread_join(tids[i], nullptr);}pthread_mutex_destroy(&mutex);return 0;
}
编译运行:
结果符合预期。
加锁操作有几点注意事项:
- 凡是访问同一个临界资源的线程,都要进行加锁保护,并且必须加同一把锁,这是规则,不能有例外。
- 在给执行流加锁时,只需要给临界区加锁就可以了。加锁的本质是让代码串行化,因此让临界区的代码越少越好,这样比较节省运行时间。
- 线程访问临界区的时候,需要先加锁,这意味着所有的线程都可以先看到同一把锁,因此锁本身就是一个公共资源,锁需要保证自己的安全。所以加锁和解锁本身就是原子性的。
- 临界区可以是一行代码,也可以是一批代码。 在执行临界区代码的时候,线程有可能会被切换,但是因为锁并没有被释放,所以其他线程都无法成功的申请到锁,会被阻塞起来。因此线程被切换不会导致资源被别的执行流更新替换。这也是互斥带来的串行化的体现。
- 对于线程而言,有意义的状态只有两种:持有锁、不持有锁,不存在其他中间状态。只有当线程的工作做完之后,才会归还锁。原子性就体现在这里。
3、互斥量实现原理
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下:
加锁代码的 movb 指令本质上是调用线程,向自己的上下文写入 0 。 xchgb 指令本质上是将共享数据交换到自己的私有上下文中,即加锁操作,因为这里是一条汇编指令,就保证了加锁操作的原子性。假设共享资源 mutex 中存放的是 1 ,那么交换操作并没有新增任何的 1 , 1 只会进行流转,保证了锁的唯一性。
七、封装
1、线程封装
使用线程库在语言层面封装线程,代码如下:
//Thread.hpp
#pragma once#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdlib>using namespace std;class Thread
{
public:typedef enum{NEW = 0,RUNNING,EXITED}ThreadStatus;typedef void (*func_t)(void*);public:Thread(int num, func_t func, void* args):_tid(0),_status(NEW),_func(func),_args(args){char name[128];snprintf(name, sizeof(name), "thread-%d", num);_name = name;}int status(){return _status;}string threadname(){return _name;}pthread_t threadid(){return _tid;}//因为类的成员函数有this指针,占用了回调函数的void*类型的参数,所以要定义成static类型static void* runHelper(void* args){Thread* ts = (Thread*)args;//_func(ts->_args );//(*ts)(); //使用仿函数的形式调用func函数ts->_func(ts->_args);}void operator()(){_func(_args);}void run(){int n = pthread_create(&_tid, nullptr, runHelper, this); //这里传参传的是 this 指针,为了static类型的回调函数可以访问类属性和其他成员函数if(n != 0)exit(1);_status = RUNNING;}void join(){int n = pthread_join(_tid, nullptr);if(n != 0){cerr << "main thread join thread " << _name << " error" << endl;return;}_status = EXITED;}~Thread(){}private:pthread_t _tid;string _name;func_t _func; //线程未来要执行的回调void* _args; //回调函数的参数,可以设置成模板ThreadStatus _status;
};//ThreadTest.cc
#include <iostream>
#include <string>
#include <unistd.h>
#include "Thread.hpp"using namespace std;void threadRun(void* args)
{string message = static_cast<const char*>(args);while(1){cout << "线程已被创建, " << message << endl;sleep(1);}
}int main()
{Thread t1(1, threadRun, (void*)"hello world");cout << "thread name: " << t1.threadname() << ", thread id: " << t1.threadid() << ", thread status: " << t1.status() << endl;t1.run();cout << "thread name: " << t1.threadname() << ", thread id: " << t1.threadid() << ", thread status: " << t1.status() << endl;t1.join();cout << "thread name: " << t1.threadname() << ", thread id: " << t1.threadid() << ", thread status: " << t1.status() << endl;return 0;
}
2、锁的封装
如果手动加锁,还需要在用完锁时主动解锁,很容易忘记。所以使用类来封装锁,通过构造与析构函数完成锁的添加和解除。
//LockGuard.hpp
#pragma once#include <iostream>
#include <pthread.h>using namespace std;class Mutex //自己不维护锁,由外部传入
{
public:Mutex(pthread_mutex_t* mutex):_pmutex(mutex){}void lock(){pthread_mutex_lock(_pmutex); }void unlock(){pthread_mutex_unlock(_pmutex);}~Mutex(){}private:pthread_mutex_t* _pmutex;
};class LockGuard
{
public:LockGuard(pthread_mutex_t* mutex):_mutex(mutex){_mutex.lock();}~LockGuard(){_mutex.unlock();}
private:Mutex _mutex;
};//ThreadTest.cc
#include <iostream>
#include <string>
#include <unistd.h>
#include "Thread.hpp"
#include "lockGuard.hpp"using namespace std;int tickets = 1000; //加锁保护
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void threadRoutine(void* args)
{string message = static_cast<const char*>(args);while(1){usleep(13);LockGuard lockguard(&mutex);if(tickets > 0){usleep(2000);//模拟抢票花费的时间cout << message << " get a tickets: " << tickets-- << endl;}else{break;}}
}int main()
{Thread t1(1, threadRoutine, (void*)"hello world1");Thread t2(2, threadRoutine, (void*)"hello world2");Thread t3(3, threadRoutine, (void*)"hello world3");Thread t4(4, threadRoutine, (void*)"hello world4");t1.run();t2.run();t3.run();t4.run();t1.join();t2.join();t3.join();t4.join();return 0;
}
八、线程安全
1、概念
- 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
- 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
2、常见的线程不安全的情况
- 不保护共享变量的函数。
- 函数状态随着被调用,状态发生变化的函数。
- 返回指向静态变量指针的函数。
- 调用线程不安全函数的函数。
3、常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
- 类或者接口对于线程来说都是原子操作。
- 多个线程之间的切换不会导致该接口的执行结果存在二义性。即加锁后其他进程无法访问共享资源。
4、常见不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
- 可重入函数体内使用了静态的数据结构。
5、常见可重入的情况
- 不使用全局变量或静态变量。
- 不使用用malloc或者new开辟出的空间。
- 不调用不可重入函数。
- 不返回静态或全局数据,所有数据都有函数的调用者提供。
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
6、可重入与线程安全联系
- 函数是可重入的,那就是线程安全的。
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
7、可重入与线程安全区别
- 可重入函数是线程安全函数的一种。
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
九、死锁
死锁是指在一组进程中的各个线程均占有不会释放的资源,但因互相申请被其他线程所占用的不会释放的资源而处于的一种永久等待状态。
死锁四个必要条件:
- 互斥条件:一个资源每次只能被一个执行流使用。
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。
避免死锁:
- 破坏死锁的四个必要条件:
1、不加锁。
2、主动释放锁。
3、控制线程统一释放锁(申请锁与释放锁无需在同一个线程内实现)。
4、按照顺序申请锁。 - 避免锁未释放的场景。
- 资源一次性分配。
十、线程同步
1、同步与竞态
- 饥饿:一个线程频繁连续的申请锁,但是申请到锁之后什么都不做。导致其他进程无法申请到锁,其他进程就处于饥饿状态。
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
- 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。
2、条件变量
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。条件变量是一个数据类型,其中有一个队列,供线程等待使用。
2.1、条件变量接口
条件变量初始化:
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:
cond:要初始化的条件变量
attr:NULL
销毁:
int pthread_cond_destroy(pthread_cond_t *cond)
等待条件满足:
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量
唤醒等待:
//唤醒在条件队列中等待的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);//按照队列顺序逐个唤醒线程
int pthread_cond_signal(pthread_cond_t *cond);
2.2、为什么 pthread_cond_wait 需要互斥量
- 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
- 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。
- 按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码:
// 错误的设计 pthread_mutex_lock(&mutex); while (condition_is_false) {pthread_mutex_unlock(&mutex);//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过pthread_cond_wait(&cond);pthread_mutex_lock(&mutex);} pthread_mutex_unlock(&mutex);
-
由于解锁和等待不是原子操作。调用解锁之后, pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个 pthread_cond_wait 。所以解锁和等待必须是一个原子操作。
-
int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进入该函数后,
会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。
2.3、条件变量使用规范
等待条件代码:
pthread_mutex_lock(&mutex);
while (条件为假)pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
给条件发送信号代码:
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
十一、生产者消费者模型
有关于生产者消费者模型,由于篇幅过长,专门写了一篇博客《生产者消费者模型》。大家可以点击查看。
十二、线程池
线程池:
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
线程池的应用场景:
- 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.
线程池的种类:
线程池示例:
- 创建固定数量线程池,循环从任务队列中获取任务对象,
- 获取到任务对象后,执行任务对象中的任务接口
线程池本质上是一种变形的生产者消费者模型,生产者是用户,消费者是操作系统本身。
1、线程池实现简易代码-版本一
//threadPool_V1.hpp
#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include <string>
#include <unistd.h>
#include <pthread.h>using namespace std;const static int N = 5;template<class T>
class ThreadPool
{
public:ThreadPool(int num = N):_num(num),_threads(num){pthread_mutex_init(&_lock, nullptr);pthread_cond_init(&_cond, nullptr);}void lockQueue(){pthread_mutex_lock(&_lock);}void unlockQueue(){pthread_mutex_unlock(&_lock);}void threadWait(){pthread_cond_wait(&_cond, &_lock);}void threadWakeup(){pthread_cond_signal(&_cond);}bool isEmpty(){return _tasks.empty();}T popTask(){T t = _tasks.front();_tasks.pop();return t;}static void* threadRoutine(void* args){pthread_detach(pthread_self()); //进行线程分离ThreadPool<T>* tp = static_cast<ThreadPool<T>*>(args);while(1){//1、检测有没有任务//2、有:处理//3、无:等待//必须要加锁tp->lockQueue();while(tp->isEmpty()){//等待,condtp->threadWait();}T t = tp->popTask(); //从公共区域拿到私有区域tp->unlockQueue();//t.run(); //处理任务不应该在临界区中进行t();cout << "thread handler done, result: " << t.formatRes() << endl;}}void init(){}void start(){for(int i = 0; i < _num; i++){pthread_create(&_threads[i], nullptr, threadRoutine, this);}}void pushTask(const T& t){lockQueue();_tasks.push(t);threadWakeup();unlockQueue();}~ThreadPool(){pthread_mutex_destroy(&_lock);pthread_cond_destroy(&_cond);}private:vector<pthread_t> _threads;int _num;queue<T> _tasks; //使用stl的自动扩容特性pthread_mutex_t _lock;pthread_cond_t _cond;
};//task.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>class Task
{
public:Task(){}Task(int x, int y, int op): _x(x), _y(y), _op(op), _result(0), _exitCode(0){}void operator()(){switch (_op){case '+':_result = _x + _y;break;case '-':_result = _x - _y;break;case '*':_result = _x * _y;break;case '/':{if (_y == 0)_exitCode = -1;else_result = _x / _y;}break;case '%':{if (_y == 0)_exitCode = -2;else_result = _x % _y;}break;default:break;}usleep(100000);}std::string formatArg(){return std::to_string(_x) + _op + std::to_string(_y) + "= ?";}std::string formatRes(){return std::to_string(_result) + "(" + std::to_string(_exitCode) + ")";}~Task(){}private:int _x;int _y;char _op;int _result;int _exitCode;
};//main.cc
#include "threadPool_V1.hpp"
#include "task.hpp"
#include <memory>int main()
{unique_ptr<ThreadPool<Task>> tp(new ThreadPool<Task>());tp->init();tp->start();while(1){//充当生产者,从网络中读取数据,构建成任务,推送给线程池int x, y;char op;cout << "please Enter x> ";cin >> x;cout << "please Enter y> ";cin >> y;cout << "please Enter op(+-*/%)> ";cin >> op;Task t(x, y, op);tp->pushTask(t);//sleep(1);}return 0;
}
运行观察结果:
2、自己封装线程和锁实现线程池-版本二
//Task.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>class Task
{
public:Task(){}Task(int x, int y, int op): _x(x), _y(y), _op(op), _result(0), _exitCode(0){}void operator()(){switch (_op){case '+':_result = _x + _y;break;case '-':_result = _x - _y;break;case '*':_result = _x * _y;break;case '/':{if (_y == 0)_exitCode = -1;else_result = _x / _y;}break;case '%':{if (_y == 0)_exitCode = -2;else_result = _x % _y;}break;default:break;}usleep(100000);}std::string formatArg(){return std::to_string(_x) + _op + std::to_string(_y) + "= ?";}std::string formatRes(){return std::to_string(_result) + "(" + std::to_string(_exitCode) + ")";}~Task(){}private:int _x;int _y;char _op;int _result;int _exitCode;
};//Thread.hpp
#pragma once#include <iostream>
#include <pthread.h>
#include <string>
#include <cstdlib>using namespace std;class Thread
{
public:typedef enum{NEW = 0,RUNNING,EXITED}ThreadStatus;typedef void (*func_t)(void*);public:Thread(int num, func_t func, void* args):_tid(0),_status(NEW),_func(func),_args(args){char name[128];snprintf(name, sizeof(name), "thread-%d", num);_name = name;}int status(){return _status;}string threadname(){return _name;}pthread_t threadid(){return _tid;}//因为类的成员函数有this指针,占用了回调函数的void*类型的参数,所以要定义成static类型static void* runHelper(void* args){Thread* ts = (Thread*)args;//_func(ts->_args );//(*ts)(); //使用仿函数的形式调用func函数ts->_func(ts->_args);}void operator()(){_func(_args);}void run(){int n = pthread_create(&_tid, nullptr, runHelper, this); //这里传参传的是 this 指针,为了static类型的回调函数可以访问类属性和其他成员函数if(n != 0)exit(1);_status = RUNNING;}void join(){int n = pthread_join(_tid, nullptr);if(n != 0){cerr << "main thread join thread " << _name << " error" << endl;return;}_status = EXITED;}~Thread(){}private:pthread_t _tid;string _name;func_t _func; //线程未来要执行的回调void* _args; //回调函数的参数,可以设置成模板ThreadStatus _status;
};//lockGuard.hpp
#pragma once#include <iostream>
#include <pthread.h>using namespace std;class Mutex //自己不维护锁,由外部传入
{
public:Mutex(pthread_mutex_t* mutex):_pmutex(mutex){}void lock(){pthread_mutex_lock(_pmutex); }void unlock(){pthread_mutex_unlock(_pmutex);}~Mutex(){}private:pthread_mutex_t* _pmutex;
};class LockGuard
{
public:LockGuard(pthread_mutex_t* mutex):_mutex(mutex){_mutex.lock();}~LockGuard(){_mutex.unlock();}
private:Mutex _mutex;
};//ThreadPool_V2.hpp
#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include "Thread.hpp"
#include "lockGuard.hpp"using namespace std;const static int N = 5;template <class T>
class ThreadPool
{
public:ThreadPool(int num = N): _num(num){pthread_mutex_init(&_lock, nullptr);pthread_cond_init(&_cond, nullptr);}pthread_mutex_t* getlock(){return &_lock;}void threadWait(){pthread_cond_wait(&_cond, &_lock);}void threadWakeup(){pthread_cond_signal(&_cond);}bool isEmpty(){return _tasks.empty();}T popTask(){T t = _tasks.front();_tasks.pop();return t;}static void threadRoutine(void *args){ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);while (1){// 1、检测有没有任务// 2、有:处理// 3、无:等待// 必须要加锁T t;{LockGuard lockguard(tp->getlock());while (tp->isEmpty()){// 等待,condtp->threadWait();}t = tp->popTask(); // 从公共区域拿到私有区域}// t.run(); //处理任务不应该在临界区中进行t();cout << "thread handler done, result: " << t.formatRes() << endl;}}void init(){for (int i = 0; i < _num; i++){_threads.push_back(Thread(i, threadRoutine, this));}}void start(){for (auto &t : _threads){t.run();}}void check(){for (auto &t : _threads){cout << t.threadname() << " runing... " << endl;}}void pushTask(const T &t){LockGuard lockguard(&_lock);_tasks.push(t);threadWakeup();}~ThreadPool(){for (auto &t : _threads){t.join();}pthread_mutex_destroy(&_lock);pthread_cond_destroy(&_cond);}private:vector<Thread> _threads;int _num;queue<T> _tasks; // 使用stl的自动扩容特性pthread_mutex_t _lock;pthread_cond_t _cond;
};//main.cc
#include "threadPool_V2.hpp"
#include "task.hpp"
#include <memory>int main()
{unique_ptr<ThreadPool<Task>> tp(new ThreadPool<Task>());tp->init();tp->start();while(1){//充当生产者,从网络中读取数据,构建成任务,推送给线程池int x, y;char op;cout << "please Enter x> ";cin >> x;cout << "please Enter y> ";cin >> y;cout << "please Enter op(+-*/%)> ";cin >> op;Task t(x, y, op);tp->pushTask(t);//sleep(1);}return 0;
}
运行观察结果:
3、自己封装线程和锁实现单例模式线程池-版本三
//ThreadPool_V4.hpp
#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include "Thread.hpp"
#include "lockGuard.hpp"using namespace std;const static int N = 5;template <class T>
class ThreadPool
{
private:ThreadPool(int num = N): _num(num){pthread_mutex_init(&_lock, nullptr);pthread_cond_init(&_cond, nullptr);}ThreadPool(const ThreadPool<T>& tp) = delete;void operator=(const ThreadPool<T>& tp) = delete;
public:static ThreadPool<T>* getinstance(){if(instance == nullptr){LockGuard lockguard(&instance_lock);if(instance == nullptr){instance = new ThreadPool<T>();instance->init();instance->start();}}return instance;}pthread_mutex_t *getlock(){return &_lock;}void threadWait(){pthread_cond_wait(&_cond, &_lock);}void threadWakeup(){pthread_cond_signal(&_cond);}bool isEmpty(){return _tasks.empty();}T popTask(){T t = _tasks.front();_tasks.pop();return t;}static void threadRoutine(void *args){ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);while (1){// 1、检测有没有任务// 2、有:处理// 3、无:等待// 必须要加锁T t;{LockGuard lockguard(tp->getlock());while (tp->isEmpty()){// 等待,condtp->threadWait();}t = tp->popTask(); // 从公共区域拿到私有区域}// t.run(); //处理任务不应该在临界区中进行t();cout << "thread handler done, result: " << t.formatRes() << endl;}}void init(){for (int i = 0; i < _num; i++){_threads.push_back(Thread(i, threadRoutine, this));}}void start(){for (auto &t : _threads){t.run();}}void check(){for (auto &t : _threads){cout << t.threadname() << " runing... " << endl;}}void pushTask(const T &t){LockGuard lockguard(&_lock);_tasks.push(t);threadWakeup();}~ThreadPool(){for (auto &t : _threads){t.join();}pthread_mutex_destroy(&_lock);pthread_cond_destroy(&_cond);}private:vector<Thread> _threads;int _num;queue<T> _tasks; // 使用stl的自动扩容特性pthread_mutex_t _lock;pthread_cond_t _cond;static ThreadPool<T>* instance;static pthread_mutex_t instance_lock;
};template <class T>
ThreadPool<T>* ThreadPool<T>::instance = nullptr;template<class T>
pthread_mutex_t ThreadPool<T>::instance_lock = PTHREAD_MUTEX_INITIALIZER;//main.cc
#include "threadPool_V4.hpp"
#include "task.hpp"
#include <memory>int main()
{// unique_ptr<ThreadPool<Task>> tp(new ThreadPool<Task>());// tp->init();// tp->start();while(1){//充当生产者,从网络中读取数据,构建成任务,推送给线程池int x, y;char op;cout << "please Enter x> ";cin >> x;cout << "please Enter y> ";cin >> y;cout << "please Enter op(+-*/%)> ";cin >> op;Task t(x, y, op);ThreadPool<Task>::getinstance()->pushTask(t);//tp->pushTask(t);//sleep(1);}return 0;
}
十三、线程安全的单例模式
1、饿汉方式实现单例模式
template <typename T>
class Singleton {static T data;
public:static T* GetInstance() {return &data;}
};
只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例。
2、懒汉方式实现单例模式
template <typename T>
class Singleton
{static T* inst;
public:static T* GetInstance() {if (inst == NULL) {inst = new T();} return inst;}
};
存在一个严重的问题:线程不安全。第一次调用 GetInstance 的时候,如果两个线程同时调用,可能会创建出两份 T 对象的实例。
3、懒汉方式实现单例模式(线程安全版本)
// 懒汉模式, 线程安全
template <typename T>
class Singleton
{volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.static std::mutex lock;
public:static T* GetInstance() {if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提高性能.lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.if (inst == NULL) {inst = new T();} lock.unlock();} return inst;}
};
注意事项:
- 加锁解锁的位置。
- 双重 if 判定, 避免不必要的锁竞争。
- volatile关键字防止过度优化。
十四、STL,智能指针和线程安全
1、STL中的容器是否是线程安全的
不是。
原因是:STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。
而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)。因此 STL 默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。
2、智能指针是否是线程安全的
对于 unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题。
对于 shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证 shared_ptr 能够高效,原子的操作引用计数。
十五、其他常见的各种锁
- 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
- 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
- 自旋锁:是一种轮询机制,不断的检测锁是否空闲。线程在申请锁时,是通过自旋的方式,还是挂起等待的方式,是由访问临界区要花费多长时间决定的。
使用自旋锁的接口函数:pthread_spin_lock。 - 公平锁。
- 非公平锁。
十六、读者写者问题
1、读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
注意:写独占,读共享,读锁优先级高
2、 读写锁接口
设置读写优先:
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
/*
pref 共有 3 种选择
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和
PTHREAD_RWLOCK_PREFER_READER_NP 一致PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
*/
初始化:
int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock,const pthread_rwlockattr_t* restrict attr);
销毁:
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);
加锁和解锁:
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); //读者加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); //写者加锁int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); //释放锁
读写锁案例:
#include <vector>
#include <sstream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <pthread.h>volatile int ticket = 1000;
pthread_rwlock_t rwlock;void* reader(void* arg)
{char* id = (char*)arg;while (1) {pthread_rwlock_rdlock(&rwlock);if (ticket <= 0) {pthread_rwlock_unlock(&rwlock);break;} printf("%s: %d\n", id, ticket);pthread_rwlock_unlock(&rwlock);usleep(1);} return nullptr;
}void* writer(void* arg)
{char* id = (char*)arg;while (1) {pthread_rwlock_wrlock(&rwlock);if (ticket <= 0) {pthread_rwlock_unlock(&rwlock);break;} printf("%s: %d\n", id, --ticket);pthread_rwlock_unlock(&rwlock);usleep(1);} return nullptr;
}struct ThreadAttr
{pthread_t tid;std::string id;
};std::string create_reader_id(std::size_t i)
{// 利用 ostringstream 进行 string 拼接std::ostringstream oss("thread reader ", std::ios_base::ate);oss << i;return oss.str();
} std::string create_writer_id(std::size_t i)
{// 利用 ostringstream 进行 string 拼接std::ostringstream oss("thread writer ", std::ios_base::ate);oss << i;return oss.str();
}void init_readers(std::vector<ThreadAttr>& vec)
{for (std::size_t i = 0; i < vec.size(); ++i) {vec[i].id = create_reader_id(i);pthread_create(&vec[i].tid, nullptr, reader, (void*)vec[i].id.c_str());}
} void init_writers(std::vector<ThreadAttr>& vec)
{for (std::size_t i = 0; i < vec.size(); ++i) {vec[i].id = create_writer_id(i);pthread_create(&vec[i].tid, nullptr, writer, (void*)vec[i].id.c_str());}
} void join_threads(std::vector<ThreadAttr> const& vec)
{// 我们按创建的 逆序 来进行线程的回收for (std::vector<ThreadAttr>::const_reverse_iterator it = vec.rbegin(); it !=vec.rend(); ++it) {pthread_t const& tid = it->tid;pthread_join(tid, nullptr);}
} void init_rwlock()
{
#if 0 // 写优先pthread_rwlockattr_t attr;pthread_rwlockattr_init(&attr);pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);pthread_rwlock_init(&rwlock, &attr);pthread_rwlockattr_destroy(&attr);
#else // 读优先,会造成写饥饿pthread_rwlock_init(&rwlock, nullptr);
#endif
} int main()
{// 测试效果不明显的情况下,可以加大 reader_nr// 但也不能太大,超过一定阈值后系统就调度不了主线程了const std::size_t reader_nr = 1000;const std::size_t writer_nr = 2;std::vector<ThreadAttr> readers(reader_nr);std::vector<ThreadAttr> writers(writer_nr);init_rwlock();init_readers(readers);init_writers(writers);join_threads(writers);join_threads(readers);pthread_rwlock_destroy(&rwlock);
}