目录
一、linux线程的概念
1.1 什么是线程
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
通过下图详细描述线程
- 常规OS的做法(比如windows)
- linuxOS的做法
通过上图可以得知,linux没有为线程专门设计TCB,而是用进程TCB来模拟线程,一个进程可以有多个线程,且至少有一个线程,这样就不需要维护复杂的的进程和线程的关系。进程向OS要资源,线程向进程要资源。
1.2 linux线程与接口关系的认识
1.3 线程和进程各自的共享资源和私有资源
如何验证?
ps -aL :
查看轻量级进程(线程)
linuxOS调度的时候,看的是PID还是LWP??答:LWP!
如何理解我们之前单独一个进程的情况??答:PID == LWP!
二、线程的优点
三、线程的缺点
- 性能损失
- 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低
- 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
- 缺乏访问控制
- 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编程难度提高
- 编写与调试一个多线程程序比单线程程序困难得多
四、线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
五、线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
进程和线程的关系如下图:
六、线程控制
6.1 POSIX线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
- 要使用这些函数库,要通过引入头文<pthread.h>
- 链接这些线程函数库时要使用编译器命令的“-lpthread”选项
6.2 创建线程
pthread_create函数
功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg)
;
参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
错误检查:
- 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
- pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
- pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
pthread_t pthread_self(void);
//获取线程ID
使用举例:
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
void* thread_run(void* args)
{
while(1)
{
printf("我是新线程[%s],我创建的线程ID是: %u\n", (const char*)args, pthread_self());
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, thread_run, (void*)"new thread");
while(1)
{
printf("我是主线程, 我创建的线程ID是: %u\n", tid);
sleep(1);
}
return 0;
}
输出结果:
6.3 线程等待
为什么要线程等待?
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
pthread_join函数
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr)
;
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
线程等待实例代码:
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
void* thread_run(void* args)
{
int num = *((int*)args);
while(1)
{
printf("我是新线程[%d],我创建的线程ID是: %lu\n", num, pthread_self());
sleep(5);
break;
}
return (void*)111;
}
#define NUM 1
int main()
{
pthread_t tid[NUM];
for(int i = 0; i < NUM; i++)
{
pthread_create(tid + i, nullptr, thread_run, (void*)&i);
sleep(1);
}
//void* 32/4, 64/8, 指针变量, 本身就可以充当某种容器保存数据
void* status = nullptr;
//退出信息,异常呢?不需要处理
//这里的返回值可以是其他的变量,对象的地址(不是临时的)
pthread_join(tid[0], &status);
printf("ret: %lld\n", (long long)status);
// while(1)
// {
// printf("我是主线程, 我创建的线程ID是: %lu, 我的线程ID是: %lu\n", tid, pthread_self());
// sleep(1);
// }
return 0;
}
输出结果:
6.4 线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit终止自己(exit是终止进程,终止所有线程)。
- 一个线程可以调用pthread_ cancel取消线程。
pthread_exit函数
功能:线程终止
原型
void pthread_exit(void *value_ptr)
;
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
pthread_cancel函数
功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread)
;
参数
thread:线程ID
**返回值:**成功返回0;失败返回错误码
实例代码:
通过调用pthread_ exit函数退出线程
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
void* thread_run(void* args)
{
int num = *((int*)args);
while(1)
{
printf("我是新线程[%d],我创建的线程ID是: %lu\n", num, pthread_self());
sleep(5);
break;
}
//return (void*)111;
pthread_exit((void*)123);
}
#define NUM 1
int main()
{
pthread_t tid[NUM];
for(int i = 0; i < NUM; i++)
{
pthread_create(tid + i, nullptr, thread_run, (void*)&i);
sleep(1);
}
//void* 32/4, 64/8, 指针变量, 本身就可以充当某种容器保存数据
void* status = nullptr;
//退出信息,异常呢?不需要处理
//这里的返回值可以是其他的变量,对象的地址(不是临时的)
pthread_join(tid[0], &status);
printf("ret: %lld\n", (long long)status);
sleep(3);
// while(1)
// {
// printf("我是主线程, 我创建的线程ID是: %lu, 我的线程ID是: %lu\n", tid, pthread_self());
// sleep(1);
// }
return 0;
}
输出结果:
通过调用pthread_cancel取消线程
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
void* thread_run(void* args)
{
int num = *((int*)args);
while(1)
{
printf("我是新线程[%d],我创建的线程ID是: %lu\n", num, pthread_self());
sleep(1);
}
//return (void*)111;
//pthread_exit((void*)123);
}
#define NUM 1
int main()
{
pthread_t tid[NUM];
for(int i = 0; i < NUM; i++)
{
pthread_create(tid + i, nullptr, thread_run, (void*)&i);
sleep(1);
}
printf("wait sub thread...\n");
sleep(5);
printf("cancel sub thread...\n");
pthread_cancel(tid[0]);
//void* 32/4, 64/8, 指针变量, 本身就可以充当某种容器保存数据
void* status = nullptr;
//退出信息,异常呢?不需要处理
//这里的返回值可以是其他的变量,对象的地址(不是临时的)
pthread_join(tid[0], &status);
printf("ret: %lld\n", (long long)status);
sleep(3);
// while(1)
// {
// printf("我是主线程, 我创建的线程ID是: %lu, 我的线程ID是: %lu\n", tid, pthread_self());
// sleep(1);
// }
return 0;
}
输出结果:
如果我们取消主线程的话,主线程会失效(僵尸状态),但不会退出。
6.5 线程分离
实例代码:
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
void* thread_run(void* args)
{
pthread_detach(pthread_self());
int num = *((int*)args);
while(1)
{
printf("我是新线程[%d],我创建的线程ID是: %lu\n", num, pthread_self());
sleep(2);
break;
}
//return (void*)111;
//pthread_exit((void*)123);
}
#define NUM 1
int main()
{
pthread_t tid[NUM];
for(int i = 0; i < NUM; i++)
{
pthread_create(tid + i, nullptr, thread_run, (void*)&i);
sleep(1);
}
printf("wait sub thread...\n");
sleep(5);
printf("cancel sub thread...\n");
pthread_cancel(tid[0]);
//void* 32/4, 64/8, 指针变量, 本身就可以充当某种容器保存数据
void* status = nullptr;
//退出信息,异常呢?不需要处理
//这里的返回值可以是其他的变量,对象的地址(不是临时的)
int ret = pthread_join(tid[0], &status);
printf("ret: %d, status: %lld\n",ret, (long long)status);
sleep(3);
// while(1)
// {
// printf("我是主线程, 我创建的线程ID是: %lu, 我的线程ID是: %lu\n", tid, pthread_self());
// sleep(1);
// }
return 0;
}
输出结果:
通过以上可以得出,返回值大于0,等待失败了,说明线程分离过后,不能再进行线程等待了。
6.6 线程ID及进程地址空间布局
pthread_ create
函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。- 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
pthread_ create
函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。- 线程库NPTL提供了
pthread_ self
函数,可以获得线程自身的ID:pthread_t pthread_self(void);
pthread_t
到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。
通过上图可以知道,只要我们拿到虚拟地址就能够快速地找到现成的所有东西,所以线程ID是一个被映射进当前进程地址空间的,线程库当中的一个地址数据,用它来充当线程ID,库里面包含了所有与线程相关的数据。只要拿到线程ID,也就拿到了库中线程的地址,就能拿到该线程的所有的运行时的用户级数据。
七、Linux线程互斥
7.1 进程线程间的互斥相关背景概念
- 感性认识
- 理性认识
实例代码:
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
int tickets = 1000;//临界资源
//为了让更多个线程进行切换,线程什么时候可能切换(1.时间片到了 2.检测的时间点:从内核态返回用户态的时候)
void* ThreadRoutine(void* args)
{
int id = *(int*)args;
delete (int*)args;
//购票的时候不能出现负数的情况
//临界区
while(1)
{
if(tickets > 0)
{
usleep(1000);
printf("%d sells ticket: %d\n", id, tickets);
tickets--;
}
else
{
break;
}
}
}
#define NUM 5
int main()
{
pthread_t tid[NUM];
for(int i = 0; i < NUM; i++)
{
int* id = new int(i);
pthread_create(&tid[i], nullptr, ThreadRoutine, id);
}
for(int i = 0; i < NUM; i++)
{
pthread_join(tid[i], nullptr);
}
return 0;
}
输出结果:
为什么可能无法获得争取结果?
if
语句判断条件为真以后,代码可以并发的切换到其他线程usleep
这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段--ticket
操作本身就不是一个原子操作
取出ticket--部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>
--
操作并不是原子操作,而是对应三条汇编指令:
- load :将共享变量ticket从内存加载到寄存器中
- update : 更新寄存器里面的值,执行-1操作
- store :将新值,从寄存器写回共享变量ticket的内存地址
要解决以上问题,需要做到三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
7.2 互斥量的接口
7.2.1 初始化互斥量
方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
;
方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrictattr)
;
参数:
mutex:要初始化的互斥量
attr:NULL
7.2.2 销毁互斥量
销毁互斥量需要注意:
- 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex)
;
7.2.3 互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex)
;
int pthread_mutex_unlock(pthread_mutex_t *mutex)
;
返回值:成功返回0,失败返回错误号
调用 pthread_ lock
时,可能会遇到以下情况:
7.2.4 改进上面的销售系统
实例代码:
#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
//对临界区加锁
class Tickets
{
public:
Tickets(int count = 1000)
:_tickets(count)
{
pthread_mutex_init(&_mtx, nullptr);
}
bool GetTickets()
{
//注意该bool变量不会被所有线程共享,因为是在栈上开辟的,每个线程的栈是私有的。
int ret = true;
//加锁
pthread_mutex_lock(&_mtx);//我们想要访问临界资源前,先要申请锁,锁要被所有线程看到。所以锁也要保证安全。原理:lock,unlock->是原子的!.一行代码是原子的:只有一行汇编的情况。
//执行这部分代码时是串行的
if(_tickets > 0)
{
usleep(1000);
printf("%lu sells ticket: %d\n",pthread_self(), _tickets);
_tickets--;//抢票
}
else
{
//没有票
printf("票已经被抢空。\n");
ret = false;
}
//释放锁
pthread_mutex_unlock(&_mtx);
return ret;
}
~Tickets()
{
pthread_mutex_destroy(&_mtx);
}
private:
int _tickets;
pthread_mutex_t _mtx;
};
//为了让更多个线程进行切换,线程什么时候可能切换(1.时间片到了 2.检测的时间点:从内核态返回用户态的时候)
void* ThreadRoutine(void* args)
{
Tickets* t = (Tickets*)args;
//购票的时候不能出现复数的情况
//临界区
while(1)
{
if(t->GetTickets() == false)
{
break;
}
}
}
#define NUM 5
int main()
{
Tickets* t = new Tickets();
pthread_t tid[NUM];
for(int i = 0; i < NUM; i++)
{
pthread_create(&tid[i], nullptr, ThreadRoutine, (void*)t);
}
for(int i = 0; i < NUM; i++)
{
pthread_join(tid[i], nullptr);
}
return 0;
}
输出结果:
由上可以得出,加锁过后,没有出现负数票,说明加锁之后各个线程在访问临界资源时,就不会相互干扰了。
7.2.5 互斥量实现原理
- 经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题。
- 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下。
7.3 可重入VS线程安全
7.3.1 概念
- 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
- 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
7.3.2 常见的线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
7.4 常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
7.5 常见不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
- 可重入函数体内使用了静态的数据结构
7.6 常见可重入的情况
- 不使用全局变量或静态变量
- 不使用malloc或者new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有数据都有函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
7.7 可重入与线程安全联系
- 函数是可重入的,那就是线程安全的
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
7.8 可重入与线程安全区别
- 可重入函数是线程安全函数的一种
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
八、常见锁概念
8.1 死锁
- 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
8.1.1 死锁四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
8.2 避免死锁
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
8.3 避免死锁算法
- 死锁检测算法(了解)
- 银行家算法(了解)
九、linux线程同步
9.1 条件变量
- 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
- 例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量
9.1.1 同步概念和竞争条件
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解
9.1.2 条件变量函数
-
初始化
-
动态分配
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrictattr)
;
参数:
cond:要初始化的条件变量
attr:NULL -
静态分配(不需要销毁)
pthread_cond_t cond = PTHREAD_COND_INITIALIZER
;
-
-
销毁
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)
;//唤醒等待队列中第一个线程
使用举例:
#include<iostream>
#include<unistd.h>
#include<string>
#include<pthread.h>
using namespace std;
pthread_mutex_t mtx;
pthread_cond_t cond;
void* control(void* args)
{
string name = (char*)args;
while(1)
{
cout << "master say: begin work" << endl;
//pthread_cond_signal:唤醒在条件变量下等待的一个线程
pthread_cond_signal(&cond);
sleep(1);
}
}
void* work(void* args)
{
int number = *(int*)args;
delete (int*)args;
while(1)
{
pthread_cond_wait(&cond, &mtx);
cout << "worker: " << number << " is working..." << endl;
}
}
#define NUM 3
int main()
{
//初始化变量
pthread_mutex_init(&mtx, nullptr);
pthread_cond_init(&cond, nullptr);
pthread_t master;
pthread_t worker[NUM];
pthread_create(&master, nullptr, control, (void*)"master");
for(int i = 0; i < NUM; i++)
{
int* number = new int(i);
pthread_create(&worker[i], nullptr, work, (void*)number);
}
pthread_join(master, nullptr);
for(int i = 0; i < NUM; i++)
{
pthread_join(worker[i], nullptr);
}
//销毁变量
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&cond);
return 0;
}
输出结果:
从以上代码可以得出结论:
十、生产者消费者模型
- 321原则(便于记忆)
10.1 生产者消费者模型基本原理
函数当中的生产者消费者模型
线程A相当于生产者,线程B相当于消费者。
生活当中的生产者消费者模型
由上可知,普通人就相当于消费者,超市相当于缓冲区(内存空间, STL等), 供货商就相当于生产者。
10.2 基于BlockingQueue的生产者消费者模型
10.2.1 BlockingQueue
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
10.2.2 C++ queue模拟阻塞队列的生产消费模型
BlockQueue.hpp
#pragma once
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
#include<time.h>
#include<queue>
using namespace std;
namespace ns_blockqueue
{
template<class T>
class BlockQueue
{
public:
BlockQueue(const int count = 5)
:_cap(count)
{
pthread_mutex_init(&_mtx, nullptr);
pthread_cond_init(&_is_empty, nullptr);
pthread_cond_init(&_is_full, nullptr);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mtx);
pthread_cond_destroy(&_is_empty);
pthread_cond_destroy(&_is_full);
}
//const& :输入
//*:输出
//&:输入输出
void Push(const T& in)
{
LockQueue();
//临界区
while(IsFull())
{
//等待的,把线程挂起,我们当前是持有锁的!!!
ProducterWait();
}
//向队列放数据,生产函数
_bq.push(in);
WakeUpConsumer();
UnlockQueue();
}
void Pop(T* out)
{
LockQueue();
while(IsEmpty())
{
//无法消费
ConsumerWait();
}
//从队列里拿数据,消费函数
*out = _bq.front();
_bq.pop();
WakeUpProduct();
UnlockQueue();
}
private:
bool IsFull()
{
return _bq.size() == _cap;
}
bool IsEmpty()
{
return _bq.empty();
}
void LockQueue()
{
pthread_mutex_lock(&_mtx);
}
void UnlockQueue()
{
pthread_mutex_unlock(&_mtx);
}
void ProducterWait()
{
//1. 调用时,会首先自动释放锁,然后再挂起自己
//2. 返回时,会首先自动竞争锁,获取到锁时,才能返回!
pthread_cond_wait(&_is_empty, &_mtx);
}
void ConsumerWait()
{
pthread_cond_wait(&_is_full, &_mtx);
}
void WakeUpProduct()
{
pthread_cond_signal(&_is_empty);
}
void WakeUpConsumer()
{
pthread_cond_signal(&_is_full);
}
private:
queue<T> _bq;//阻塞队列
int _cap;//队列元素上限
pthread_mutex_t _mtx;//保护临界资源的锁
//1. 当生产满的时候,就不要再生产了(不要竞争锁了),而是应该让消费者来消费
//2. 当消费光的时候,就不要再消费了(不要竞争锁了),而是应该让生产者来生产
pthread_cond_t _is_empty;//_bq空了,生产者者要在该条件变量下等待是否为空
pthread_cond_t _is_full;//_bq满了,消费者者要在该条件变量下等待是否为满
};
}
CpTest.cpp
#include "BlockQueue.hpp"
using namespace ns_blockqueue;
void* consumer(void* args)
{
BlockQueue<int>* bq = (BlockQueue<int>*)args;
while(1)
{
sleep(1);
int data = 0;
bq->Pop(&data);
cout << "消费者消耗数据: " << data << endl;
}
}
void* producter(void* args)
{
BlockQueue<int>* bq = (BlockQueue<int>*)args;
while(1)
{
sleep(1);
//1.制造数据
int data = rand() % 50 + 1;
cout << "生产者生产数据: " << data << endl;
bq->Push(data);
}
}
int main()
{
srand(time(0));
BlockQueue<int>* bq = new BlockQueue<int>();
pthread_t c;
pthread_t p;
pthread_create(&c, nullptr, consumer, (void*)bq);
pthread_create(&p, nullptr, producter, (void*)bq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
return 0;
}
输出结果:
浅谈代码编写时遇到的一些问题
代码改进(新增任务处理,比如实现一个简单的加减乘除算术处理)
BlockQueue.hpp
#pragma once
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
#include<time.h>
#include<queue>
using namespace std;
namespace ns_blockqueue
{
template<class T>
class BlockQueue
{
public:
BlockQueue(const int count = 5)
:_cap(count)
{
pthread_mutex_init(&_mtx, nullptr);
pthread_cond_init(&_is_empty, nullptr);
pthread_cond_init(&_is_full, nullptr);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mtx);
pthread_cond_destroy(&_is_empty);
pthread_cond_destroy(&_is_full);
}
//const& :输入
//*:输出
//&:输入输出
void Push(const T& in)
{
LockQueue();
//临界区
while(IsFull())
{
//等待的,把线程挂起,我们当前是持有锁的!!!
ProducterWait();
}
//向队列放数据,生产函数
_bq.push(in);
WakeUpConsumer();
UnlockQueue();
}
void Pop(T* out)
{
LockQueue();
while(IsEmpty())
{
//无法消费
ConsumerWait();
}
//从队列里拿数据,消费函数
*out = _bq.front();
_bq.pop();
WakeUpProduct();
UnlockQueue();
}
private:
bool IsFull()
{
return _bq.size() == _cap;
}
bool IsEmpty()
{
return _bq.empty();
}
void LockQueue()
{
pthread_mutex_lock(&_mtx);
}
void UnlockQueue()
{
pthread_mutex_unlock(&_mtx);
}
void ProducterWait()
{
//1. 调用时,会首先自动释放锁,然后再挂起自己
//2. 返回时,会首先自动竞争锁,获取到锁时,才能返回!
pthread_cond_wait(&_is_empty, &_mtx);
}
void ConsumerWait()
{
pthread_cond_wait(&_is_full, &_mtx);
}
void WakeUpProduct()
{
pthread_cond_signal(&_is_empty);
}
void WakeUpConsumer()
{
pthread_cond_signal(&_is_full);
}
private:
queue<T> _bq;//阻塞队列
int _cap;//队列元素上限
pthread_mutex_t _mtx;//保护临界资源的锁
//1. 当生产满的时候,就不要再生产了(不要竞争锁了),而是应该让消费者来消费
//2. 当消费光的时候,就不要再消费了(不要竞争锁了),而是应该让生产者来生产
pthread_cond_t _is_empty;//_bq空了,生产者者要在该条件变量下等待是否为空
pthread_cond_t _is_full;//_bq满了,消费者者要在该条件变量下等待是否为满
};
}
Task.hpp
#pragma once
#include<iostream>
#include<map>
#include<functional>
using namespace std;
namespace ns_task
{
class Task
{
public:
Task(int x = 1, int y = 1, char op = '+')
:_x(x)
,_y(y)
,_op(op)
{}
void run()
{
int ans = _func_map[_op](_x, _y);
cout << "当前任务正在被【" << pthread_self() << "】处理: " << _x << " " << _op << " " << _y << " = " << ans << endl;
}
~Task()
{}
private:
int _x;
int _y;
char _op;
public:
static map<char,function<int(int,int)>> _func_map;
};
map<char,function<int(int,int)>> Task::_func_map = {
{'+', [](int x, int y)->int{return x + y;}},
{'-', [](int x, int y)->int{return x - y;}},
{'*', [](int x, int y)->int{return x * y;}},
{'/', [](int x, int y)->int{return x / y;}},
{'%', [](int x, int y)->int{return x % y;}}
};
}
CpTest.cpp
#include "BlockQueue.hpp"
#include "Task.hpp"
using namespace ns_blockqueue;
using namespace ns_task;
void* consumer(void* args)
{
BlockQueue<Task>* bq = (BlockQueue<Task>*)args;
while(1)
{
sleep(1);
Task t;
//1. 获取数据
bq->Pop(&t);
//2. 将数据推送到任务队列中
t.run();
// int data = 0;
// bq->Pop(&data);
// cout << "消费者消耗数据: " << data << endl;
}
}
void* producter(void* args)
{
BlockQueue<Task>* bq = (BlockQueue<Task>*)args;
string ops = "+-*/%";
while(1)
{
sleep(1);
int x = rand() % 20 + 1;
int y = rand() % 10 + 1;
char op = ops[rand() % 5];
Task t(x, y, op);
cout << "producter发送任务: " << x << " " << op << " " << y << " = ?" << endl;
bq->Push(t);
// //1.制造数据
// int data = rand() % 50 + 1;
// cout << "生产者生产数据: " << data << endl;
// bq->Push(data);
}
}
int main()
{
srand(time(0));
BlockQueue<Task>* bq = new BlockQueue<Task>();
pthread_t c1;
pthread_t c2;
pthread_t c3;
pthread_t p;
pthread_create(&c1, nullptr, consumer, (void*)bq);
pthread_create(&c2, nullptr, consumer, (void*)bq);
pthread_create(&c3, nullptr, consumer, (void*)bq);
pthread_create(&p, nullptr, producter, (void*)bq);
pthread_join(c1, nullptr);
pthread_join(c2, nullptr);
pthread_join(c3, nullptr);
pthread_join(p, nullptr);
return 0;
}
输出结果:
十一、POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
11.1 回顾信号量的概念
11.2 认识信号量对应的函数
11.2.1 信号量作用的原理
11.2.2 初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value)
;
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值
11.2.3 销毁信号量
int sem_destroy(sem_t *sem)
;
11.2.4 等待信号量
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem)
; //P()
11.2.5 发布信号量
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem)
;//V()
11.3 基于环形队列的生产消费模型
11.3.1基于环形队列的生产消费模型的原理
- 环形队列采用数组模拟,用模运算来模拟环状特性
11.3.2 结合sem基于环形队列的生产消费模型的代码实现
ring_queue.hpp
#pragma once
#include<iostream>
#include<vector>
#include<semaphore.h>
#include<pthread.h>
#include<unistd.h>
using namespace std;
namespace ns_ring_queue
{
template<class T>
class RingQueue
{
public:
RingQueue(int cap = 5)
:_rq(cap)
,_cap(cap)
,_c_step(0)
,_p_step(0)
{
sem_init(&_blank_sem, 0, cap);
sem_init(&_data_sem, 0, 0);
pthread_mutex_init(&_c_mtx, nullptr);
pthread_mutex_init(&_p_mtx, nullptr);
}
~RingQueue()
{
sem_destroy(&_blank_sem);
sem_destroy(&_data_sem);
pthread_mutex_destroy(&_c_mtx);
pthread_mutex_destroy(&_p_mtx);
}
void Push(const T& in)
{
//生产接口
sem_wait(&_blank_sem);//p(空位置)
pthread_mutex_lock(&_p_mtx);
_rq[_p_step] = in;
_p_step++;
_p_step %= _cap;
pthread_mutex_unlock(&_p_mtx);
sem_post(&_data_sem);
}
void Pop(T* out)
{
//消费接口
sem_wait(&_data_sem);//p(非空位置)
pthread_mutex_lock(&_c_mtx);
*out = _rq[_c_step];
_c_step++;
_c_step %= _cap;
pthread_mutex_unlock(&_c_mtx);
sem_post(&_blank_sem);
}
private:
vector<T> _rq;
int _cap;
//生产者关心空位置
sem_t _blank_sem;
//消费者关心非空位置
sem_t _data_sem;
int _c_step;//消费者位置
int _p_step;//生产者位置
pthread_mutex_t _c_mtx;
pthread_mutex_t _p_mtx;
};
}
Task.hpp
#pragma once
#include<iostream>
#include<map>
#include<functional>
using namespace std;
namespace ns_task
{
class Task
{
public:
Task(int x = 1, int y = 1, char op = '+')
:_x(x)
,_y(y)
,_op(op)
{}
void run()
{
int ans = _func_map[_op](_x, _y);
printf("当前任务正在被【%lu】处理: %d %c %d = %d\n\n", pthread_self(), _x, _op, _y, ans);
}
~Task()
{}
private:
int _x;
int _y;
char _op;
public:
static map<char,function<int(int,int)>> _func_map;
};
map<char,function<int(int,int)>> Task::_func_map = {
{'+', [](int x, int y)->int{return x + y;}},
{'-', [](int x, int y)->int{return x - y;}},
{'*', [](int x, int y)->int{return x * y;}},
{'/', [](int x, int y)->int{return x / y;}},
{'%', [](int x, int y)->int{return x % y;}}
};
}
ring_cp.cpp
#include "ring_queue.hpp"
#include "Task.hpp"
using namespace ns_ring_queue;
using namespace ns_task;
void* consumer(void* args)
{
RingQueue<Task>* rq = (RingQueue<Task>*)args;
while(1)
{
sleep(1);
Task t;
rq->Pop(&t);
t.run();
// int data = 0;
// rq->Pop(&data);
// printf( "消费者消耗数据:%d ,我是:%lu \n" ,data, pthread_self());
}
}
void* producter(void* args)
{
RingQueue<Task>* rq = (RingQueue<Task>*)args;
string ops = "+-*/%";
while(1)
{
sleep(1);
int x = rand() % 20 + 1;
int y = rand() % 10 + 1;
char op = ops[rand() % 5];
Task t(x, y, op);
printf("producter【%lu】发送任务: %d %c %d = ?\n\n", pthread_self(), x, op, y);
rq->Push(t);
// //1.制造数据
// int data = rand() % 50 + 1;
// printf("生产者生产数据: %d\n", data);
// rq->Push(data);
}
}
int main()
{
RingQueue<Task>* rq = new RingQueue<Task>();
pthread_t c1;
pthread_t c2;
pthread_t c3;
pthread_t p1;
pthread_t p2;
pthread_create(&c1, nullptr, consumer, (void*)rq);
pthread_create(&c2, nullptr, consumer, (void*)rq);
pthread_create(&c3, nullptr, consumer, (void*)rq);
pthread_create(&p1, nullptr, producter, (void*)rq);
pthread_create(&p2, nullptr, producter, (void*)rq);
pthread_join(c1, nullptr);
pthread_join(c2, nullptr);
pthread_join(c3, nullptr);
pthread_join(p1, nullptr);
pthread_join(p2, nullptr);
return 0;
}
输出结果:
十二、线程池
12.1 什么叫做线程池
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
12.2 线程池的应用场景
- 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.
12.3 实现一个线程池
thread_pool.pp
#pragma once
#include<iostream>
#include<string>
#include<vector>
#include<queue>
#include<unistd.h>
using namespace std;
namespace ns_thread_pool
{
template<class T>
class ThreadPool
{
public:
ThreadPool(int num = 5)
:_num(num)
{
pthread_mutex_init(&_mtx, nullptr);
pthread_cond_init(&_cond, nullptr);
_tid.resize(num);
for(int i = 0; i < num; i++)
{
pthread_create(&_tid[i], nullptr, Rountine, (int*)this);
}
}
~ThreadPool()
{
for(int i = 0; i < _num; i++)
{
pthread_join(_tid[i], nullptr);
}
pthread_mutex_destroy(&_mtx);
pthread_cond_destroy(&_cond);
}
//在类中让线程执行类成员方法,是不可行的,因为类成员函数都隐含由this指针
//必须让线程执行静态方法
static void* Rountine(void* args)
{
ThreadPool<T>* tp = (ThreadPool<T>*)args;
while(1)
{
tp->Lock();
while(tp->IsEmpty())
{
//任务队列为空,让线程挂起
tp->Wait();
}
//该任务队列中一定有一个任务了
T t;
tp->PopTask(&t);
tp->Unlock();
t.run();
}
}
void PushTask(const T& in)
{
Lock();
_task_queue.push(in);
Unlock();
//有任务了,唤醒一个线程
WakeUp();
}
void PopTask(T* out)
{
*out = _task_queue.front();
_task_queue.pop();
}
bool IsEmpty()
{
return _task_queue.empty();
}
private:
void Lock()
{
pthread_mutex_lock(&_mtx);
}
void Unlock()
{
pthread_mutex_unlock(&_mtx);
}
void Wait()
{
pthread_cond_wait(&_cond, &_mtx);
}
void WakeUp()
{
pthread_cond_signal(&_cond);
}
private:
int _num;//线程数量
queue<T> _task_queue;//任务队列,该成员是一个临界资源
vector<pthread_t> _tid;
pthread_mutex_t _mtx;
pthread_cond_t _cond;
};
}
Task.hpp
#pragma once
#include<iostream>
#include<map>
#include<functional>
#include<stdlib.h>
#include<time.h>
#include<string>
using namespace std;
namespace ns_task
{
class Task
{
public:
Task(int x = 1, int y = 1, char op = '+')
:_x(x)
,_y(y)
,_op(op)
{}
void run()
{
int ans = _func_map[_op](_x, _y);
cout << "当前任务正在被【" << pthread_self() << "】处理: " << _x << " " << _op << " " << _y << " = " << ans << endl;
}
~Task()
{}
private:
int _x;
int _y;
char _op;
public:
static map<char,function<int(int,int)>> _func_map;
};
map<char,function<int(int,int)>> Task::_func_map = {
{'+', [](int x, int y)->int{return x + y;}},
{'-', [](int x, int y)->int{return x - y;}},
{'*', [](int x, int y)->int{return x * y;}},
{'/', [](int x, int y)->int{return x / y;}},
{'%', [](int x, int y)->int{return x % y;}}
};
}
main.cpp
#include "thread_pool.hpp"
#include "Task.hpp"
using namespace ns_thread_pool;
using namespace ns_task;
int main()
{
srand(time(0));
ThreadPool<Task> tp(5);
string ops = "+-*/%";
while(1)
{
sleep(1);
int x = rand() % 10 + 1;
int y = rand() % 10 + 1;
char op = ops[rand() % 5];
Task t(x, y ,op);
tp.PushTask(t);
}
return 0;
}
输出结果:
十三、其他常见的各种锁
悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
挂起等待特性的锁vs自旋锁
通过等待时间来决定使用哪种锁,如果等待时间比较长就使用挂起等待特性的锁,等待时间短的话就是用自旋锁。
挂起等待特性的锁:本质就是将自己长时间挂起等待(有成本的)。
自旋锁:和"轮询"很像,因为时间短,一个进程会不断的检测另一个进程的状态,自旋的过程。
十四、读者写者问题
14.1 读者写者模型基本理论
14.2 读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
基本原理:
通过以下伪代码可以模拟以上行为:
14.3 读写锁接口
- 设置读写优先
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref)
;
- pref 共有 3 种选择
- PTHREAD_RWLOCK_PREFER_READER_NP : (默认设置) 读者优 先,可能会导致写者饥饿情况
- PTHREAD_RWLOCK_PREFER_WRITER_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)
;