线程
概念
线程是进程内的一个执行分支,一个进程里面包含了独立的PCB,地址空间,页表,还有申请的物理空间,如果我们今天有一个"进程",我们不用给这个"进程"重新分配进程地址空间和页表,也不用给这个"进程"在物理内存中开辟一段新的空间,只分配一个结构体给这个"进程",这个"进程"只需要执行正文代码里的一部分代码,执行粒度更细,这个进程被我们称为线程(TCB),线程在进程的地址空间内运行,因为从地址空间里面可以通过页表映射拿到资源,线程是操作系统调度的基本单位,线程是进程内部的执行流资源,进程是资源分配的基本实体,由于线程类似于数据和代码少一点的进程,所以线程TCB可以复用进程PCB的数据结构和调度算法,线程<=执行流(轻量级进程)<=进程
Linux里没有线程的概念,因为Linux是用进程模拟线程,所以Linux里只有轻量级进程的概念,也就是执行流,Linux里也只有轻量级进程的接口,所以Linux在用户层上封装了一个第三方pthread线程库
线程间切换效率更高,因为cpu和内存之间有一个cache缓存,如果我们要进程切换,那么cache缓存里的热数据要全部丢弃,下一个进程放上cpu的时候还要重新热缓存,但如果是线程切换的时候,是不需要热缓存的,所以线程也是需要时间片的。
但我们怎么知道在cpu上切换的是进程还是线程呢?其实每一个task_struct都有身份表示,对于线程,有主线程和新线程的区别
线程之间是没有保护的,一个线程出问题,其他线程也会跟着出问题,比如说我们用kill杀掉一个线程,整个进程都会被杀掉,因为发的信号线程进程共享,最终导致整个进程出问题,每一个时间片只能有一个线程运行
线程共享进程数据,像文件描述符表,信号处理方法,用户id和组id,当前工作目录等,但也拥有自己的一部分数据,比如说线程ID,信号屏蔽字,一组寄存器(独立的线程上下文),栈(线程之间运行不会出现执行流错错乱的问题),errno,调度优先级等
线程分配资源,本质上就是分配地址空间范围,代码是有地址的,这个地址是虚拟地址,让线程执行不同函数即可
线程控制
创建
由主线程给我们创建新线程
第一个参数thread是一个输出型参数,表示创建的线程的id,pthread_t是一个无符号长整数的封装,也就是unsigned long int
第二个参数表示线程的属性,一般设为nullptr
第三个参数是一个函数指针,返回值是void*,参数也是一个void*
最后一个参数是输入型参数,创建线程成功,新线程回调线程函数的时候要传参,相当于传到第三个参数的函数里面
当我们创建线程成功的时候,会返回0,如果创建失败,则会为我们返回错误码
void在不同平台下的大小是不一样的,所以void并不能用来定义变量,void*可以用来接受任意的指针类型
cpp
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
void* thread_func(void* argc)
{
while(1)
{
cout<<"i am new thread,pid is "<<getpid()<<endl;
sleep(2);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,thread_func,nullptr);
//主线程
while(1)
{
cout<< "i am main thread,pid is "<<getpid()<<endl;
sleep(1);
}
return 0;
}

可以看到上面的两个执行流都在运行
如果我们想查看当前用户启动的所有轻量级进程
bash
ps -aL

LWP(light weight process)指的是轻量级进程,pid等于LWP说明这个线程为主线程,但这是操作系统里使用的概念,而不是用户的概念,所以LWP和tid并不一样。如果我们向线程LWP为24780或者24781发送kill -9 信号,默认是发给进程的,所以无论是向哪一个线程发信号,最后这个进程都会退出
cpp
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
void* thread_func(void* argc)
{
const char* str=(char*)argc;
while(1)
{
cout<<str<<" i am new thread,pid is "<<getpid()<<endl;
sleep(2);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,thread_func,(void*)"hello world");
//主线程
while(1)
{
cout<< "i am main thread,pid is "<<getpid()<<",new thread tid is "<<tid<<endl;
sleep(1);
}
return 0;
}

如果在main函数和thread_func函数同时调用另外的函数another_func,都可以执行,这也就是这个方法可以被多个线程同时进入,也就是函数another_func被重入了,关于全局变量,已初始化的和未初始化的,都可以被两个线程看到,因为线程共享地址空间,只是执行的正文代码不一样。
主线程和新线程不知道哪一个先运行,但主线程一定是最后退出的,所以线程也是需要等待的
线程的参数和返回值不仅仅可以传整形字符串这些,也可以传递类的对象
cpp
#include<pthread.h>
#include<iostream>
using namespace std;
class between
{
public:
between(int begin,int end)
:_begin(begin)
,_end(end)
{}
int _begin;
int _end;
};
class count
{
public:
count(int sum,int cred)
:_sum(sum)
,_cred(cred)
{}
int _sum;
int _cred;
};
void* sumcount(void* argc)
{
between* val=static_cast<between*>(argc);
int sum=0;
for(int i=val->_begin;i<=val->_end;i++)
{
sum+=i;
}
count* pcnt=new count(sum,1);
return (void*)pcnt;
}
int main()
{
pthread_t tid;
between b(1,100);
pthread_create(&tid,nullptr,sumcount,&b);
void* ret;
pthread_join(tid,&ret);
count* retval=static_cast<count*>(ret);
cout<<"sum is "<<retval->_sum<<",credict is "<<retval->_cred<<endl;
return 0;
}

等待
新线程创建后,主线程也是需要等待新线程的,也是需要得到新线程退出信息的,造成类似于进程里的僵尸进程的问题,所以线程等待有两个目的,一个是防止内存泄漏,第二个是获得新线程的退出结果
这里的第一个参数是线程的tid
第二个参数,因为线程执行的函数的返回值是void*,所以这个retval是一个输出型参数,要把一级指针的结果传递出去,就需要二级指针
这里的返回值成功返回的是0,如果返回失败返回的是错误码
cpp
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
void* thread_func(void* argc)
{
cout<<"i am new thread"<<endl;
sleep(5);
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,thread_func,(void*)"hello world");
//主线程
pthread_join(tid,nullptr);
cout<<"i am main thread"<<endl;
return 0;
}

获得一个线程的退出结果是通过这个线程的返回值
cpp
#include<pthread.h>
#include<iostream>
#include<unistd.h>
using namespace std;
void* pthreadfunc(void* argc)
{
const char* str=(const char*)argc;
cout<<"i am new pthread "<<str<<endl;
return (void*)1;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,pthreadfunc,(void*)"hello world");
void* ret;
//这里默认是阻塞等待的
pthread_join(tid,&ret);
//这里不能强转为int,因为void*是8字节,整形是4字节
cout<<"i am main pthread ,ret is "<<(long long)ret<<endl;
return 0;
}

终止
退出线程
exit不能用来终止进程,而不能用来终止线程,如果我们在线程的任何一个部分调用exit,最后进程就全部退出了,所以我们需要其他的工具来终止线程,比如说return和pthread_exit
cpp
#include<pthread.h>
#include<iostream>
#include<unistd.h>
using namespace std;
void* pthreadfunc(void* argc)
{
const char* str=(const char*)argc;
cout<<"i am new pthread "<<str<<endl;
pthread_exit((void*)11);
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,pthreadfunc,(void*)"hello world");
void* ret;
pthread_join(tid,&ret);
cout<<"i am main pthread ,ret is "<<(long long)ret<<endl;
return 0;
}

取消线程
还可以使用pthread_cancel去给目标线程发送一个取消请求,如果这个线程是通过pthread_cancel退出的,那么返回值是-1(PTHREAD_CANCELED),这是一个宏
cpp
#include<pthread.h>
#include<iostream>
#include<unistd.h>
using namespace std;
void* pthreadfunc(void* argc)
{
const char* str=(const char*)argc;
cout<<"i am new pthread "<<str<<endl;
sleep(5);
//pthread_exit((void*)11);
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,pthreadfunc,(void*)"hello world");
sleep(3);//保证新线程存在
void* ret;
pthread_cancel(tid);
pthread_join(tid,&ret);
cout<<"i am main pthread ,ret is "<<(long long)ret<<endl;
return 0;
}

获取
下图这个函数可以获取线程id
cpp
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
void* thread_func(void* argc)
{
const char* str=(char*)argc;
while(1)
{
cout<<str<<" i am new thread,tid is "<<pthread_self()<<endl;
sleep(2);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,thread_func,(void*)"hello world");
//主线程
while(1)
{
cout<< "i am main thread,pid is "<<getpid()<<",main thread tid is "<<pthread_self()<<endl;
sleep(1);
}
return 0;
}

c++11
c++11可以支持多线程,而我们上面使用的都是Linux系统里的原生线程库
cpp
#include<thread>
#include<iostream>
using namespace std;
void func()
{
cout<<"i am c++11 pthread"<<endl;
}
int main()
{
thread t(func);
t.join();
return 0;
}

如果我们没有指定编译需要c++11,就会报错
如果我们没有指定pthread库,在编译的时候没有报错,但在执行的时候会报错
所以c++11的线程库在底层依旧是操作系统的原生线程库支持的,但如果我们在Linux下的c++线程库是由Linux支持的,在Windows下的c++线程库是由Windows支持的
线程控制块TCB
clone
平常fork的底层其实是clone的原理,但这个接口其实我们是用不了的,所以做这个库就被线程库封装了,线程要创建需要独立栈和回调函数,系统自己定义独立栈,把数据放在里面,然后把回调函数暴露出去,供用户使用,原生线程库是需要加载到内存的,线程库其实是放在进程地址空间的共享区
所以线程库是需要被多个线程使用的,线程库要管理多个线程,所以线程库也需要管理这些线程,由用户层维护的
所以我们可以发现线程是有一个称为TCB的数据结构管理起来的,tid其实就是共享区的起始地址,也就是pthread_create的第一个参数返回的值,转化为16进制就可以发现这是一个共享区的地址
线程栈
用户级线程+内核级LWP才是Linux线程
线程栈里存储的是在线程里的临时变量,比如说返回值,函数中的局部变量,函数栈帧等,使得线程之间的函数调用链不会被影响,当一个调用函数比较深的时候,就会不断往线程栈里创建栈帧,,一个函数的局部变量不会影响另外一个函数,所以当我们定义多个线程,并且这多个线程进入同一个函数,我们会发现这个函数里的临时变量并不会互相影响,临时变量的地址也不一样
cpp
#include<pthread.h>
#include<iostream>
#include<string>
#include<vector>
#include<unistd.h>
using namespace std;
#define NUM 3
class pthread_stack
{
public:
pthread_stack(const string& name="")
:_name(name)
{}
public:
string _name;
};
void Init(pthread_stack* it,int i)
{
string name="pthreadname "+to_string(i);
it->_name=name;
}
void* pthread_func(void* args)
{
int cnt=0;
pthread_stack* cur=static_cast<pthread_stack*>(args);
while(cnt<10)
{
cout<<"pthread name is "<<cur->_name<<"cnt is "<<cnt<<",&cnt is "<<&cnt<<endl;
cnt++;
sleep(1);
}
delete cur;
return nullptr;
}
int main()
{
vector<pthread_t> tids;
for(int i=0;i<NUM;i++)
{
pthread_t tid;
pthread_stack* cur=new pthread_stack;
Init(cur,i);
sleep(1);
pthread_create(&tid,nullptr,pthread_func,cur);
tids.push_back(tid);
}
for(int i=0;i<tids.size();i++)
{
pthread_join(tids[i],nullptr);
}
return 0;
}

可以看到每一个线程的cnt都是从0开始的,而且每一个线程的cnt地址都不一样
如果我们的主线程想访问某一个线程在这个函数里的数据,可以定义一个全局变量指针,拿到这个数据的地址,虽然主线程和新线程是在不同的栈区,但主线程和新线程还是共享地址空间的,所以主线程可以通过这种方式访问新线程的变量,不仅是主线程和新线程之间可以看到对方的数据,任意一个线程和线程之间都可以看到对方的数据,因为全局变量是被所有线程共享的,在地址空间上只有一个
如果线程需要一个私有的全局变量
cpp
__thread int g_val;
cpp
#include<pthread.h>
#include<iostream>
#include<string>
#include<vector>
#include<unistd.h>
using namespace std;
#define NUM 3
__thread int g_val=0;
class pthread_stack
{
public:
pthread_stack(const string& name="")
:_name(name)
{}
public:
string _name;
};
void Init(pthread_stack* it,int i)
{
string name="pthreadname "+to_string(i);
it->_name=name;
}
void* pthread_func(void* args)
{
int cnt=0;
pthread_stack* cur=static_cast<pthread_stack*>(args);
g_val++;
while(cnt<10)
{
cout<<"pthread name is "<<cur->_name<<"cnt is "<<cnt<<",&cnt is "<<&cnt<<",g_val is "<<g_val<<endl;
cnt++;
sleep(1);
}
delete cur;
return nullptr;
}
int main()
{
vector<pthread_t> tids;
for(int i=0;i<NUM;i++)
{
pthread_t tid;
pthread_stack* cur=new pthread_stack;
Init(cur,i);
sleep(1);
pthread_create(&tid,nullptr,pthread_func,cur);
tids.push_back(tid);
}
for(int i=0;i<tids.size();i++)
{
pthread_join(tids[i],nullptr);
}
return 0;
}
可以看到每一个全局变量最后的值都是1,而不是3,这种就是线程的局部存储
但__thread只能用来定义内置类型,而不能用来定义自定义类型
线程分离
默认情况下,在线程退出的时候,我们要进行pthread_join的动作,但如果我们不关心线程的返回值,pthread_join就是一种负担,这个时候我们就可以直接告诉系统说自动释放资源
cpp
#include<iostream>
#include<pthread.h>
#include<vector>
#include<cstring>
#include<error.h>
#include<string>
#include<unistd.h>
using namespace std;
#define NUM 3
void* pthread_func(void* args)
{
int cnt=1;
string* name=static_cast<string*>(args);
while(cnt--)
{
cout<<name->c_str()<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
vector<pthread_t> tids;
for(int i=0;i<NUM;i++)
{
pthread_t tid;
string* name =new string("i am "+to_string(i)+" thread");
pthread_create(&tid,nullptr,pthread_func,name);
tids.push_back(tid);
sleep(3);
}
//线程分离
for(int i=0;i<tids.size();i++)
{
pthread_detach(tids[i]);
}
//如果线程已经分离,那么join就会出错
for(int i=0;i<tids.size();i++)
{
int n=pthread_join(tids[i],nullptr);
cout<<"n="<<n<<",tid is "<<tids[i]<<",why is "<<strerror(n)<<endl;
}
return 0;
}

可以看到错误码是22,说明在线程分离的情况下,主线程依旧是最后退出,但新线程不能再join,分离的意思是,线程依旧共享一部分资源,但最后进线程结束,系统就自动回收线程,而不是主线程执行这个过程
线程互斥
我们知道,线程大部分使用的数据都是局部变量,其他线程无法获得此线程的全局变量,线程之间是可以共享一些变量的,比如说全局变量,但在共享的时候会带来一些问题
cpp
#include<iostream>
#include<vector>
#include<stdio.h>
#include<cstdio>
#include<unistd.h>
#include<string>
using namespace std;
#define NUM 5
int ticket=1000;
void* get_ticket(void* args)
{
string* name=static_cast<string*>(args);
while(1)
{
if(ticket>0)
{
usleep(1000);
cout<<name->c_str()<<" get a ticket success,get ticket is "<<ticket<<endl;
ticket--;
}
else
break;
}
cout<<"thread "<<name->c_str()<<" exit success"<<endl;
}
int main()
{
vector<pthread_t> tids;
vector<string> names;
for(int i=0;i<NUM;i++)
{
pthread_t tid;
string* name =new string(to_string(i));
pthread_create(&tid,nullptr,get_ticket,name);
tids.push_back(tid);
names.push_back(string(name->c_str()));
}
long long sum=0;
for(int i=0;i<tids.size();i++)
{
void* ret;
pthread_join(tids[i],&ret);
}
return 0;
}

可以看到这边的票数小于1的时候,部分线程还在运行,因为ticket这个变量被存在cpu里,每次要运行一个线程的时候都要先把ticket写入cpu中,然后运算完后再把ticket写回内存,数据被从cpu拿出来的时候,需要带走自己的上下文,所以线程在执行的时候加载到cpu寄存器的本质是把数据的内容变成了自己的上下文,以拷贝的方式独占了一份,然后准备做减一操作的时候,这个线程的时间片到了,切换到其他线程的时间片时,其他线程第一步是恢复上下文,也就是把ticket数据恢复到1000,再继续减,所以当前这个操作并不是安全的,下图可以看到有一些数字是重复的,也就是这个问题,所以减一这个操作不具备原子性
而且我们这个代码对ticket还做了判断,在这几个执行流都判断完之后,都认为ticket还大于0,所以还可以继续执行,所以有好几个线程进入if判断逻辑,在计算的时候,还要从内存中重新读取ticket,而这里读到的数据可能已经被修改了,所以会出现负数的情况
锁
加锁的本质其实是用时间换安全,因为在加锁的时候,只允许一个执行流访问临界资源,锁是一种临界资源,也是一种二元信号量
pthread_mutex_t是库给我们提供的一种数据类型
使用
如果我们把锁定义成全局的,我们就可以用PTHREAD_MUTEX_INITIAIZER这个宏初始化这个锁,也可以调用pthread_mutex_init进行初始化
cpp
//初始化锁方法1
pthread_mutex_t lock;
pthread_mutex_init(&lock,nullptr);
//初始化方法2
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
//释放锁
pthread_mutex_destroy(&lock);
所以我们在上述抢票系统的临界资源加上锁
但其实这样是不合逻辑的,因为如果ticket判断走到else的时候,直接break,这个锁就没有被释放
cpp
#include<iostream>
#include<vector>
#include<stdio.h>
#include<cstdio>
#include<unistd.h>
#include<string>
using namespace std;
#define NUM 5
int ticket=1000;
class data
{
public:
data(const string& name)
:_name(name)
{
pthread_mutex_init(&lock,nullptr);
}
public:
string _name;
pthread_mutex_t lock;
};
void* get_ticket(void* args)
{
data* d=static_cast<data*>(args);
while(1)
{
pthread_mutex_lock(&(d->lock));
if(ticket>0)
{
usleep(1000);
cout<<d->_name.c_str()<<" get a ticket success,get ticket is "<<ticket<<endl;
ticket--;
pthread_mutex_unlock(&(d->lock));
}
else
{
pthread_mutex_unlock(&(d->lock));
break;
}
}
cout<<"thread "<<d->_name.c_str()<<" exit success"<<endl;
return nullptr;
}
int main()
{
vector<pthread_t> tids;
//这里要注意,不能在for循环里面创建锁,因为这样每一个线程都会有一个锁,就会造成一个线程在修改ticket的时候,其他线程无法被阻止
data* d=new data("");
vector<string> names;
for(int i=0;i<NUM;i++)
{
pthread_t tid;
d->_name=to_string(i);
pthread_create(&tid,nullptr,get_ticket,d);
tids.push_back(tid);
names.push_back(string(d->_name.c_str()));
}
long long sum=0;
for(int i=0;i<tids.size();i++)
{
void* ret;
pthread_join(tids[i],&ret);
}
return 0;
}

但其实我们去测试这个代码会发现,这一千张票几乎都是同一个线程抢的,所以我们可以在抢到票时asleep一点时间,就可以解决这个问题,因为这个线程在时间片内一直申请加锁解锁,但在时间片外,这个线程申请锁后,其他线程无法解锁,而这个线程又不能执行,直到又到这个线程的时间片才能再次抢票解锁,所以就导致其他线程的饥饿问题,为了解决这个问题,需要申请锁的线程需要排队,而占有锁的线程不能立刻申请锁,所以从这里可以看出来,锁本身应该也算临界资源,申请锁和释放锁这一过程本身就是原子的
按照一定顺序获取资源称为同步
在临界区的线程是可以被切换的,在线程被切换出去的时候,是持有锁的,所以其他进程无法申请锁,依旧无法执行临界区代码,依旧保持原子性
原理
为了实现互斥锁操作,大多数指令集体系结构都提供了swap或者exchange指令,用于把寄存器和内存单元的数据互换,这样就保证只有一条指令,从而保证了原子性,即使是多个cpu,在访问内存的总线周期也有先后,一个处理机上的交换指令的执行只能等待另外一个处理机交换指令执行结束后才能执行,所以这个指令不管在单处理机上还是多处理机上都是原子的
mutex锁其实相当于是一个变量
假设mutex是1,在执行第二步交换后,开始进入if判断逻辑,发现寄存器的数据大于0,就return,申请锁成功。在这个伪代码里任何时候线程都能被切换走,在切换的时候要把自己的上下文带走,保存自己执行到的位置,假设线程1在执行到第一条语句的时候刚好被切换走,线程1就必须带走自己的上下文,并且记录自己执行的位置,线程2来了,执行movb的时候,实际上是写入线程2的寄存器的,线程2申请锁成功,切换回线程1执行,线程1继续执行xchgb指令,但由于mutex已经是0了,切换失败,线程1挂起等待,直到等待完成后go to lock,在这无数次的交换中,mutex只有一个有效值
在解锁的时候,可以让其他线程归还锁,比如说当死锁的时候,可以由其他进程去解锁
死锁
概念
多线程执行的时候,自己占有别人需要的资源,还需要的资源也被别人占用着,就会造成死锁
因为这个线程已经占用一个锁了,还想重新申请同一把锁,但锁已经被线程本身占用了,所以就会挂起阻塞
产生条件
死锁的产生有四个必要条件,必须同时满足才能产生死锁:
1.互斥条件:一个资源每一次只能让一个执行流使用
2.请求和保持条件:一个执行流因为请求资源阻塞的时候,对已获得的资源保持不放
3.不剥夺条件:一个执行流获得的资源,在使用完之前,不能强行剥夺
4.循环等待条件:若干执行流之间形成头尾相接循环等待资源的关系
避免死锁
我们只需要破坏死锁的产生条件中的其中一个,就可以破坏死锁
1.破坏互斥条件一般无法破坏
2.破坏请求与保持条件:当申请失败的时候,就会把自己本身的资源释放,下图里的trylock()就是用于请求与不保持的
3.破坏不剥夺条件:释放对方的锁
4.加锁顺序一致:在申请锁的时候按照顺序来申请锁,比如说都从锁1开始申请,而不是线程1先申请锁1,线程2先申请锁2,线程1再请求锁2,线程2再请求锁1,这样就会造成循环等待
5.避免锁未释放的情景
6.资源一次性分配,这个线程需要多少资源,就一次性分配给这个线程,而不是先申请一个,再申请一个
线程安全
线程安全指的是多个线程并发执行同一段代码的时候,不会出现不同的结果,如果没有锁的保护,就会出现问题,而重入是指一个函数被不同执行流调用,当前一个执行流还没执行完,另外一个执行流再次调用这个函数,如果结果不会有任何变化,那么我们就称这个函数为可重入函数,反之则为不可重入函数,如果这个函数不可重入,那么肯定线程不安全,如果这个函数可重入,那么一定线程安全。
不保护共享变量的函数,随着被调用状态发生变化的函数,比如说这个函数里有一个静态int变量,每次调用的时候这个变量+1,返回指向静态指针的函数等等
STL容器并不是线程安全的,智能指针大部分都是线程安全的
单例模式
某些类只能具有一个对象,称为单例模式
懒汉模式实现单例
懒汉模式的核心就是延迟加载,这样能够优化服务器的启动速度
cpp
template<class T>
class singleton
{
private:
static T* pdata;
public:
static T* getdata()
{
if(pdata==NULL)
pdata=new T();
return pdata;
}
};
但这个存在线程安全问题,可能两个或者多个线程可以创建多个pdata的实例,所以我们需要对这个代码加锁
饿汉模式实现单例
cpp
template<class T>
class singleton
{
private:
static T data;
public:
static T* getdata()
{
return &data;
}
};
其他锁
悲观锁:在每次取数据的时候,总是担心数据被其他线程修改,于是在取数据之前都会先上锁
乐观锁:每次取数据的时候,总是乐观的认为数据不会被其他线程修改,因此不上锁,但在更新数据的时候,会关心其他数据在更新前有没有对当前数据修改,主要采用两种方式:版本号机制和CAS操作
CAS操作指的是在需要更新数据的时候,判断当前内存的值和曾经取到的值是否相等,如果相等则用新值更新,如果不相等则重试
自旋锁表示,如果一个占有锁的线程处理临界区资源时间并不长,那么其他的线程就不需要挂起,而是处于一种自旋等待的状态,比如说pthread_mutex_trylock,当申请锁失败的时候会返回,如果我们用while死循环和pthread_mutex_trylock一起使用,就是一个简易的自旋锁,库里的pthread_spin_lock就是自旋锁,底层有封装while循环,pthread_spin_trylock表示申请失败就直接返回了。
挂起等待锁用于等待资源时间长的状态,也就是我们平常使用的pthread_mutex_lock
读者写者问题
专用于处理生活中多读少写的情况,写独享,读共享,当没有锁的时候,读和写的请求都被允许,但有写锁的时候,读和写的请求都被拒绝,当只有读锁的时候,写的请求被拒绝,读的请求可以,其实读者写者问题和生产者消费者模型唯一的区别是,消费者会拿走数据,而读者不会
上述函数主要是用于创建读写锁
第一个函数是让读写锁以读者的身份加锁,第二个函数是让读写锁以写者的身份加锁
无论是读者锁还是写者锁,当要释放这个锁的时候,就需要用上述接口释放锁
读者写者模型里读者是非常多的,但此时写者是无法写数据的,就很容易造成写者的饥饿问题
cpp
int read_count=0;
//读者加锁
lock(rlock);
read_count++;
if(read_count==1)
lock(wlock);
unlock(rlock);
//进行读取
lock(rlock);
read_count--;
if(read_count==0)
unlock(wlock)
unlock(rlock)
//写者加锁
lock(wlock);
//写入数据
unlock(wlock)
线程同步
同步指的是在保证数据安全的前提下,让线程访问资源需要按照一定的顺序,之前在加锁的时候,由于每一个线程的抢夺锁的能力并不一样,造成抢票系统几乎只有一个线程在抢票的结果,所以我们需要用同步的概念来解决这个饥饿问题,线程走了下次还来称为互斥,线程来了排队称为同步
解决方案
可以做一个task_struct*的等待队列,当一个线程请求临界资源失败之后,就排到队尾,当前一个申请锁的线程执行结束之后,操作系统就会唤醒队头的线程,然后让这个线程进入临界区进行执行,唤醒这个线程的变量被称为条件变量,临界资源需要锁的存在,条件变量必须依赖锁
条件变量函数
产生与销毁

这个接口其实和互斥锁的接口的使用方法是类似的
等待

当这个线程申请临界资源失败了的时候,线程自身是知道自己申请失败的,线程就会自动要队尾进行排队等待
第一个参数指的是在这个参数上等待,第二个参数是互斥量
在调用这个函数的时候,必须是放在临界区里的,但如果是这样的话锁就无法被释放,所以这个接口在等待的时候会释放锁,在接收到信号的时候重新持有锁,然后再继续执行
唤醒

pthread_cond_broadcast用于唤醒所有的等待线程
pthread_cond_signal用于唤醒队头的第一个线程
应用
cpp
#include<pthread.h>
#include<unistd.h>
#include<iostream>
using namespace std;
#define NUM 5
int cnt=0;
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
void* count(void* args)
{
long long i=(long long)args;
while(1)
{
pthread_mutex_lock(&lock);
pthread_cond_wait(&cond,&lock);
cout<<"i am thread "<<i<<",cnt is "<<cnt++<<endl;
pthread_mutex_unlock(&lock);
}
}
int main()
{
for(long long i=0;i<NUM;i++)
{
pthread_t tid;
pthread_create(&tid,nullptr,count,(void*)i);
}
cout<<"main pthread control begin"<<endl;
while(1)
{
pthread_cond_signal(&cond);
sleep(1);
}
return 0;
}
pthread_cond_wait的过程要放在加锁和解锁中,因为只有在判断临界资源不满足条件的时候才要等待,临界资源也是有状态的,上述代码其实是没有对临界资源判断的,但抢票逻辑中有if判断票数是否大于0,如果不是就让线程去等待。在让线程等待的时候,它会自动释放锁,在让线程阻塞等待后,要用pthread_cond_signal或者pthread_cond_broadcast唤醒队列中的线程。
cp生产者消费者模型
可以解决生产者和消费者因为速度不一致导致的效率问题,进行一定程度的解耦,生产者和消费者应该都是线程承担的
生产者和生产者之间,消费者和消费者之间都是互斥关系,用于保证线程安全,生产者和消费者是互斥和同步关系,需要保证顺序性
blockingqueue

cpp
#pragma once
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<queue>
using namespace std;
pthread_mutex_t printlock=PTHREAD_MUTEX_INITIALIZER;
template<class T>
class area
{
private:
static const int defaultsize=10;
public:
area(int maxsize=defaultsize)
:max_size(maxsize)
{
lowwater=maxsize/3;
highwater=(maxsize*2)/3;
pthread_mutex_init(&lock,nullptr);
pthread_cond_init(&c_cond,nullptr);
pthread_cond_init(&p_cond,nullptr);
}
void push(const T& value)
{
pthread_mutex_lock(&lock);
if(q.size()==max_size)
{
//阻塞等待
pthread_cond_wait(&p_cond,&lock);
}
q.push(value);
pthread_mutex_lock(&printlock);
cout<<"productor prodece a good ,good is "<<value<<endl;
pthread_mutex_unlock(&printlock);
if(q.size()>highwater)
pthread_cond_signal(&c_cond);
pthread_mutex_unlock(&lock);
}
T pop()
{
pthread_mutex_lock(&lock);
if(q.size()==0)
{
pthread_cond_wait(&c_cond,&lock);
}
T val=q.front();
q.pop();
pthread_mutex_lock(&printlock);
cout<<"consumer get a good ,good is "<<val<<endl;
pthread_mutex_unlock(&printlock);
if(q.size()<lowwater)
{
pthread_cond_signal(&p_cond);
}
pthread_mutex_unlock(&lock);
return val;
}
~area()
{
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&c_cond);
pthread_cond_destroy(&p_cond);
}
private:
queue<T> q;
int max_size;//队列 的最大长度
pthread_mutex_t lock;//加锁
//生产者和消费者必须
pthread_cond_t c_cond;//解决同步问题
pthread_cond_t p_cond;//解决同步问题
int lowwater;
int highwater;
};
cpp
#include"cp.hpp"
void* Comsumer(void* args)
{
area<int>* local=static_cast<area<int>*>(args);
while(1)
{
int val=local->pop();
//sleep(1);
}
}
void* Productor(void* args)
{
area<int>* local =static_cast<area<int>*>(args);
int tmp=0;
while(1)
{
local->push(tmp);
tmp++;
//sleep(1);
}
}
int main()
{
//创建场所
area<int>* local=new area<int>();
//创建生产者,消费者线程
pthread_t c,p;
pthread_create(&c,nullptr,Comsumer,local);
pthread_create(&p,nullptr,Productor,local);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
delete local;
return 0;
}

上述代码的锁其实是为了约束生产者和生产者,消费者和消费者,生产者和消费者的关系,而两个条件变量主要是为了约束生产者和消费者的关系
但这并不具备高效性,除非生产者在把资源放入共享区之前需要处理数据,而这个时间消费者刚好在消费数据,这样生产者和消费者线程就可以并发执行,不会互相干扰,才能体现高效性,比如说下面的计算任务,生产者获得data1,data2和op是需要花费时间的,在生产者获取数据的时候消费者可以从队列里获得任务并执行
cpp
#pragma once
#include<iostream>
#include<vector>
#include<ctime>
#include<cstdlib>
using namespace std;
enum{
normal=0,
divzero,
modzero,
operator_error
};
class Task
{
public:
Task(int d1,int d2,char op)
:data1(d1)
,data2(d2)
,operate(op)
{}
void count()
{
switch(operate)
{
case '+':
ret=data1+data2;
exitcode=normal;
break;
case '-':
ret=data1-data2;
exitcode=normal;
break;
case '*':
ret=data1*data2;
exitcode=normal;
break;
case '/':
if(data2==0)
{
exitcode=divzero;
}
else
{
ret=data1/data2;
exitcode=normal;
}
break;
case '%':
if(data2==0)
{
exitcode=modzero;
}
else
{
ret=data1%data2;
exitcode=normal;
}
break;
default:
exitcode=operator_error;
break;
}
}
void consumerprint()
{
string ans=to_string(data1)+operate+to_string(data2)+"="+to_string(ret);
cout<<ans<<endl;
string exiterror="exitcode is "+to_string(exitcode);
cout<<exiterror<<endl;
}
void productorprint()
{
string t=to_string(data1)+operate+to_string(data2);
cout<<t<<endl;
}
private:
int data1;
int data2;
char operate;
int ret;
int exitcode;
};

误唤醒
当线程wait的时候,误唤醒的时候
我们知道,这个函数会在等待的时候释放锁,然后让其他符合规则的线程先执行,然后在被唤醒的时候重新持有锁,但如果这个线程被误唤醒了呢?
上述的所有代码都是只有一个生产者和一个消费者的,那如果是多个生产者和多个消费者呢,当有多个消费者线程在执行,假设采用pthread_cond_broadcast集体唤醒了5个消费者线程,但实际上这五个消费者线程必须先竞争一把锁,有一个线程竞争到了这把锁,从而正常执行,而剩下四个线程并没有竞争到锁,就造成了误唤醒/伪唤醒的情况,但这个锁不一定是被消费者抢到的,而是被这四个被唤醒的线程之一竞争到的,这个线程就会往下继续执行,再次生产数据,就可能会导致队列里的数据数量大于maxsize的情况
如果要避免伪唤醒的情况,就把判断逻辑的if改为while,再次判断一次,就能解决这个问题,这样修改,我们就能把上面一个生产者一个消费者的cp模型转化为多个生产者多个消费者的cp模型
基于环形队列的生产消费模型
用数组实现这个环形队列,浪费一个格子用于区分空和满,当生产者和消费者在同一个格子的时候,表示这个队列是空,但生产者的下一个格子是消费者的时候,表示这个队列是满,实现只要不在同一个数组下标,就可以实现生产的时候也在消费,消费的时候也在生产,也就是说,指向同一个位置的时候不能同时访问,所以需要信号量的作用
我们可以定义两个信号量,一个信号量表示空间资源是多少,一个表示数据资源是多少
cpp
#pragma once
#include<queue>
#include<vector>
#include<semaphore.h>
#include<pthread.h>
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
using namespace std;
#define NUM 10
template<class T>
class cp_queue
{
private:
public:
cp_queue()
:maxsize(NUM)
,runqueue(maxsize)
,c_pos(0)
,p_pos(0)
{
sem_init(&space,0,maxsize);
sem_init(&data,0,0);
}
void push(T value)
{
sem_wait(&space);
cout<<"productor product a source ,source is "<<value<<endl;
//cout<<p_pos<<" "<<runqueue.size()<<endl;
runqueue[p_pos]=value;
//cout<<"push exit"<<endl;
sem_post(&data);
p_pos++;
p_pos%=maxsize;
}
void pop(T* pvalue)
{
sem_wait(&data);
*pvalue=runqueue[c_pos];
cout<<"comsumer comsume a source ,source is "<<(*pvalue)<<endl;
sem_post(&space);
c_pos++;
c_pos%=maxsize;
}
~cp_queue()
{
sem_destroy(&space);
sem_destroy(&data);
}
private:
int maxsize;
vector<T> runqueue;
//下标
int c_pos;
int p_pos;
//信号量
sem_t space;
sem_t data;
};

这是单个生产者和单个消费者之间的关系,消费者和生产者的速度不一样,结果也会不一样,比如说刚开始消费者速度很快,就会变成生产者生产一个,消费者马上消费一个,如果刚开始生产者速度很快,那么生产者很快把队列生产满,然后消费者消费一个,生产者就马上生产一个
那么如果我们想让这个环形队列支持多生产者,多消费者运行,首先我们要知道,环形队列只能运行一个生产者,一个消费者并发执行,不能让两个生产者或者两个消费者同时执行,所以我们需要对push和pop行为加锁,并且需要加两把锁,因为生产者和消费者是可以同步的
先申请信号量或者先申请锁都是可以的,信号量是不需要保护的,因为信号申请是原子的,每个线程都可以参与信号量的竞争,当一个线程竞争到信号量的时候,可以立刻去竞争锁,当这个线程竞争到锁,开始执行临界区代码的时候,其他线程还是可以竞争信号量资源的,然后当这个执行临界区代码的线程执行完后,某一个线程可以立刻持有锁,这就让争夺锁和争夺信号量几乎是并行的,如果反过来就没有这个效果,会导致申请信号量的线程永远只有一个,不利于并发度
线程池 池化技术
先申请一部分线程放在内存里,需要使用的时候直接取即可,这样可以减少系统调用的次数,其实线程池也类似于生产消费模型,外面是生产者,生产者把数据传入线程池,由线程池自己分配
cpp
#pragma once
#include<iostream>
#include<vector>
#include<queue>
#include<unistd.h>
using namespace std;
template<class T>
class pthread_pool
{
private:
static const int max_size=10;
public:
pthread_pool()
:maxsize(max_size)
,pthreads(maxsize)
{
//创建锁和条件变量
pthread_mutex_init(&lock,nullptr);
pthread_cond_init(&cond,nullptr);
//创建线程池
for(int i=0;i<maxsize;i++)
{
pthread_t tid;
pthread_create(&tid,nullptr,do_task,this);
pthreads.push_back(tid);
}
}
static void* do_task(void* args)//如果是普通函数的话会多一个隐藏参数this指针,不匹配pthread_create第三个参数
{
pthread_pool<T>* arg=static_cast<pthread_pool<T>*>(args);
//上锁
pthread_mutex_lock(&(arg->lock));
//判断队列里有没有数据
while(arg->tasks.size()==0)
{
//条件等待
pthread_cond_wait(&(arg->cond),&(arg->lock));
}
//处理
T task=arg->tasks.front();
arg->tasks.pop();
pthread_mutex_unlock(&(arg->lock));
task.count();
task.consumerprint();
}
void push(T task)
{
pthread_mutex_lock(&lock);
tasks.push(task);
task.productorprint();
//通知
pthread_cond_signal(&cond);
pthread_mutex_unlock(&lock);
}
~pthread_pool()
{
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
}
private:
int maxsize;
vector<pthread_t> pthreads;
queue<T> tasks;
pthread_mutex_t lock;
pthread_cond_t cond;
};