什么是线程
在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
一切进程至少都有一个执行线程
线程在进程内部运行,本质是在进程地址空间内运行
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
课本:线程是比进程更加轻量化的一种执行流/线程是在进程内部执行的一种执行流
我的理解:线程是CPU调度的基本单位/进程是承担系统资源的基本实体
以上是Linux中线程的实现方案
之前对于进程的理解:是内部仅有一个执行流的进程
现今对于进程的理解:是内部含有多个执行流的进程
实际上Linux中没有真正的线程,是用进程的PCB来模拟的线程,如果OS真的支持线程,也就必须管理线程(先描述,再组织),会有TCB(和进程的PCB类似)..... 和进程的创建一样麻烦,故而可以基于进程已有的一切,来模拟线程
线程创建更加简单,只需要创建PCB就好,一切是基于进程基础上的
线程在进程的地址空间中运行
POSIX线程库
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
要使用这些函数库,要通过引入头文件<pthread.h>
链接这些线程函数库时要使用编译器命令的“-lpthread”选项
创建线程
功能:创建一个新的线程
原型
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变量的开销更小
代码:
Makefile
testThread:testThread.cc
g++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:
rm -f testThread
testThread.cc
// 新线程执行
void *ThreadRoutine(void *arg)
{
const char *threadname = (const char *)arg;
while (true)
{
cout<< "I am a new thread: "<<threadname<<", pid: "<< getpid() <<endl;
sleep(1);
}
}
int main()
{
//已经有进程了
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void *)"thread 1");
sleep(3);
pthread_t tid1;
pthread_create(&tid1, nullptr, ThreadRoutine, (void *)"thread 2");
sleep(3);
pthread_t tid2;
pthread_create(&tid2, nullptr, ThreadRoutine, (void *)"thread 3");
sleep(3);
pthread_t tid3;
pthread_create(&tid3, nullptr, ThreadRoutine, (void *)"thread 4");
sleep(3);
//主线程
while(true)
{
cout << "I am main thread"<< ", pid: " << getpid() <<endl;
sleep(1);
}
return 0;
}
当在访问某一行代码时,有极大的概率会访问这行代码附近的代码,称之为局部性原理-->给预加载机制(可执行程序从磁盘加载到物理内存),提供理论基础
CPU内部有cache(数据的缓存)用于存储某一行正在执行的代码的一些附近代码(极大概率马上会被执行),这样就不用总是来回从内存中访问加载数据到CPU内,而是直接在CPU内部做事
我们把保存在cache内部的一部分代码和数据称之为热数据
线程切换为什么效率高?
因为:1 寄存器少(线程执行流执行进程代码的一部分,形成的上下文数据较少-->用于保存数据的寄存器少) 2 不需要重新更新cache(线程指向的是同一个地址空间)
分给进程的时间片也要被内部的线程进行瓜分,因为时间片也是资源
线程的优点
1.创建一个新线程的代价要比创建一个新进程小得多
2.与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
3.线程占用的资源要比进程少很多
4.能充分利用多处理器的可并行数量
5.在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
线程的缺点
性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型 线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了 不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
线程异常
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
线程用途
合理的使用多线程,能提高CPU密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是 多线程运行的一种表现)
进程和线程
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
线程ID 、一组寄存器 、栈 、errno 、信号屏蔽字、调度优先级
进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
文件描述符表、每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)、当前工作目录、用户id和组id
线程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,本质就是一个进程地址空间上的一个地址
重谈地址空间
文件系统IO的基本单位大小:4KB(page size)
线程私有:CPU寄存器的属于该线程自己的上下文数据 每个线程都要有自己独立的栈结构
const int threadnum = 5;//要创建的线程的个数
using func_t = function<void()>;//与typedef function<void()> func_t 作用相同
void Print()//新线程额外执行的任务
{
std::cout << "我是线程执行的大任务的一部分" << std::endl;
}
class ThreadData
{
public:
ThreadData(const std::string &name, const uint64_t &ctime, func_t f)
: threadname(name),
createtime(ctime),
func(f)
{}
public:
std::string threadname;//新线程的名字
uint64_t createtime;//新线程的创建时间
func_t func;//新线程要执行的函数
};
// 新线程执行
void *ThreadRountine(void *args)
{
int a = 10;
ThreadData *td = static_cast<ThreadData *>(args);
while (true)
{
cout << "new thread"
<< " thread name: " << td->threadname << " create time: " << td->createtime << endl;
td->func();
if(td->threadname == "thread-4")//让线程4触发异常,验证线程的健壮性问题-->一个线程出错终止,所有线程都终止
{
cout << td->threadname << " 触发了异常!" << endl;
a /= 0; // 故意制作异常
}
sleep(1);
}
}
//创建多线程
int main()
{
vector<pthread_t> pthreads;
for (size_t i = 0; i < threadnum; i++)
{
char threadname[64];
snprintf(threadname, sizeof(threadname), "%s-%lu", "thread", i);
pthread_t tid;
ThreadData *td = new ThreadData(threadname, (uint64_t)time(nullptr), Print);
pthread_create(&tid, nullptr, ThreadRountine, td);
pthreads.push_back(tid);
sleep(1);
}
cout << "thread id: ";
for(const auto &tid: pthreads)
{
cout << tid << ",";
}
cout << endl;
while (true)
{
cout << "main thread" << endl;
sleep(3);
}
return 0;
}
pthread_self
string ToHex(pthread_t tid)
{
char id[64];
snprintf(id,sizeof(id),"0x%x",tid);
return id;
}
void* threadRoutine(void*arg)
{
usleep(1000);
string name = static_cast<const char*>(arg);
while(true)
{
cout<<"new threaad is running,thread name: "<<name<<" thread id: "<<ToHex(pthread_self())<<endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,threadRoutine,(void*)"thread-1");
while(true)
{
cout<<"main thread, sub thread id: "<<ToHex(tid)<<" main thread id: "<<ToHex(pthread_self())<<endl;
sleep(1);
}
return 0;
}
thread id本质是一个地址
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit
2. 线程可以调用pthread_ exit终止自己
3. 一个线程可以调用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;失败返回错误码
线程的终止,可以直接return,也可以调用pthread_exit()
线程等待
为什么需要线程等待?
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内
创建新的线程不会复用刚才退出线程的地址空间
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的 终止状态是不同的,总结如下:
1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值
2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED。
3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数
线程默认要被等待
线程退出,如果没有被等待,会导致类似进程的僵尸问题
string ToHex(pthread_t tid)
{
char id[64];
snprintf(id,sizeof(id),"0x%x",tid);
return id;
}
void* threadRoutine(void*arg)
{
usleep(1000);
string name = static_cast<const char*>(arg);
int cnt = 5;
while(cnt--)
{
cout<<"new threaad is running,thread name: "<<name<<" thread id: "<<ToHex(pthread_self())<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,threadRoutine,(void*)"thread-1");
cout<<"main thread, "<<" main thread id: "<<ToHex(pthread_self())<<endl;
sleep(10);
int n = pthread_join(tid,nullptr);
cout<<"main thread done"<<" n: "<<n<<endl;
sleep(5);
return 0;
}
如果我们要得到新线程的返回值,即我们要得到void*,为了得到void*,需要传入void**
string ToHex(pthread_t tid)
{
char id[64];
snprintf(id,sizeof(id),"0x%x",tid);
return id;
}
void* threadRoutine(void*arg)
{
usleep(1000);
string name = static_cast<const char*>(arg);
int cnt = 5;
while(cnt--)
{
cout<<"new threaad is running,thread name: "<<name<<" thread id: "<<ToHex(pthread_self())<<endl;
sleep(1);
}
return (void*)"thread-1 done";
//pthread_exit((void*)"thread-1 done");//这样返回也可以
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,threadRoutine,(void*)"thread-1");
cout<<"main thread, "<<" main thread id: "<<ToHex(pthread_self())<<endl;
void* ret = nullptr;
int n = pthread_join(tid,&ret);
cout<<"main thread done"<<" n: "<<n<<endl;
cout<<"main thread get new thread return: "<<(const char*)ret<<endl;
return 0;
}
新线程能返回任意类型的值,当然也包括对象这种结构化数据
class ThreadReturn
{
public:
ThreadReturn(pthread_t id, const string &info, int code)
: id_(id),
info_(info),
code_(code)
{}
public:
pthread_t id_;
std::string info_;
int code_;
};
string ToHex(pthread_t tid)
{
char id[64];
snprintf(id,sizeof(id),"0x%x",tid);
return id;
}
void* threadRoutine(void*arg)
{
usleep(1000);
string name = static_cast<const char*>(arg);
int cnt = 5;
while(cnt--)
{
cout<<"new threaad is running,thread name: "<<name<<" thread id: "<<ToHex(pthread_self())<<endl;
sleep(1);
}
ThreadReturn*ret = new ThreadReturn(pthread_self(),"thread quit normal",10);
return ret;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,threadRoutine,(void*)"thread-1");
cout<<"main thread, "<<" main thread id: "<<ToHex(pthread_self())<<endl;
void* ret = nullptr;
int n = pthread_join(tid,&ret);
cout<<"main thread done"<<" n: "<<n<<endl;
ThreadReturn*r = static_cast<ThreadReturn*>(ret);
cout<<"main thread get new thread return info: "<<r->code_<<","<<ToHex(r->id_)<<","<<r->info_<<endl;
delete r
return 0;
}
分离线程
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放 资源,从而造成系统泄漏
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
joinable和分离是冲突的,一个线程不能既是joinable又是分离的
线程是可以被设置为分离状态的,一旦该线程被分离,那么主线程就不再关心该线程,该线程退出会由系统回收,线程模式默认是joinable
设置成分离状态需要用到:pthread_detach
新线程设置成分离状态way1:
void* threadRoutine(void*arg)
{
pthread_detach(pthread_self());//该线程设置成分离状态
int cnt = 5;
while(cnt--)
{
cout<<"new threaad is running..."<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,threadRoutine,(void*)"thread-1");
sleep(1);
int n = pthread_join(tid,nullptr);
cout<<"main thread done"<<" n: "<<n<<endl;
return 0;
}
新线程一旦设置成分离状态,主线程就不会等待新线程了,所以使用pthread_join去等待会失败
新线程设置成分离状态way2:
void* threadRoutine(void*arg)
{
int cnt = 5;
while(cnt--)
{
cout<<"new threaad is running..."<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,threadRoutine,(void*)"thread-1");
sleep(1);
pthread_detach(tid);//将新线程设置成分离状态
int n = pthread_join(tid,nullptr);
cout<<"main thread done"<<" n: "<<n<<endl;
return 0;
}
线程终止方法之一:即把该线程取消掉,使用pthread_cancel
void* threadRoutine(void*arg)
{
int cnt = 5;
while(true)
{
cout<<"new threaad is running..."<<endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,threadRoutine,(void*)"thread-1");
sleep(5);
int n = pthread_cancel(tid);//取消新线程
cout<<"new thread cancel done, "<<" n: "<<n<<endl;
void*ret = nullptr;
n = pthread_join(tid,&ret);//主线程等待新线程
cout<<"main thread done, "<<" n: "<<n<<" thread return: "<<(int64_t)ret<<endl;
return 0;
}
线程被取消,返回值是-1:
#define PTHREAD_CANCELED ((void *) -1)
分离新线程的情况下:
int main()
{
pthread_t tid;
pthread_create(&tid,nullptr,threadRoutine,(void*)"thread-1");
sleep(5);
pthread_detach(tid);//分离新线程
int n = pthread_cancel(tid);//取消新线程
cout<<"new thread cancel done, "<<" n: "<<n<<endl;
void*ret = nullptr;
n = pthread_join(tid,&ret);//主线程等待新线程
cout<<"main thread done, "<<" n: "<<n<<" thread return: "<<(int64_t)ret<<endl;
return 0;
}
线程如果是被分离的,该线程可以被取消,但是不能被join
我们以上使用的关于线程的接口调用,全部都不是由系统直接提供的,而是原生线程库pthread提供的,因为在Linux操作系统里,并不存在真正的线程,是用进程来模拟的线程,所以linux系统里也就不会提供任何创建线程的系统调用,最多会提供创建轻量级进程的系统调用,可是作为使用者,我们只认线程这个概念,所以必须在用户和系统之间添加一层软件层pthread库(在底层去调用创建轻量级进程的接口,返回之后给用户提供操作线程的接口,比如创建,终止)
如果在用户层创建5个线程,那么在系统中就要有5个轻量级进程(LWP)与之对应,轻量级进程天然由操作系统来管理,但是用户级线程也可能会存在很多呀,它由谁来管理呢?答案是pthread库!,管理思路是先描述再组织,所以必须有struct tcb,只不过struct tcb并不在系统中呈现,而是在pthread库中实现,这个tcb一定要直接或者间接要与LWP的id值有关系
虽然地址空间,线程之间是共享的,但是线程要有独立属性:1 上下文数据 2 栈空间
如何理解pthread库来管理线程?
首先libpthread.so动态库需要加载映射到进程的地址空间中(映射到共享区),然后才能被当前进程访问到,才能使用库中的方法,线程库是共享的,所以内部要管理整个系统,多个用户启动的所有线程
站在语言角度理解pthread
C++11内部的多线程,本质就是对原生线程库的封装
Makefile
mythread:mypthread.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f mythread
mypthread.cc
void myrun()
{
while(true)
{
cout << "I am a thread" << endl;
sleep(1);
}
}
int main()
{
thread t(myrun);
t.join();
return 0;
}
全局变量,本身就是被所有线程共享的
int g_val = 100;
void *threadRoutine(void *args)
{
string name = static_cast<const char *>(args);
while (true)
{
sleep(1);
cout << name << ", g_val: " << g_val << " ,&g_val: " << &g_val << "\n"<<endl;
g_val++;
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");
while (true)
{
sleep(1);
cout << "main thread, g_val: " << g_val << " ,&g_val: " << &g_val << "\n"<<endl;
}
pthread_join(tid, nullptr);
}
__thread int g_val = 100;//线程的局部存储,每个线程各自将该变量存到自己的线性局部存储内,各有一份
__thread pid_t lwp = 0;//__thread适用于内置类型,对于容器如string 不支持
pid_t gettid()
{
return syscall(SYS_gettid);
}
void *threadRoutine(void *args)
{
string name = static_cast<const char *>(args);
lwp = gettid(); // 调用系统调用 SYS_gettid 获取当前线程的 TID
while (true)
{
sleep(1);
cout << name << ", g_val: " << g_val << " ,&g_val: " << &g_val << "\n"<< endl;
cout <<"new thread: " << lwp << endl;
g_val++;
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRoutine, (void *)"thread1");
lwp = gettid(); // 调用系统调用 SYS_gettid 获取当前线程的 TID
cout <<"main thread: " << lwp << endl;
while (true)
{
sleep(1);
cout << "main thread, g_val: " << g_val << " ,&g_val: " << &g_val << "\n"<< endl;
}
pthread_join(tid, nullptr);
}
线程中也可以进行fork
现在我们来简单封装一下线程:
Makefile
test_thread:main.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f test_thread
Thread.hpp
template<class T>
using func_t = std::function<void(T)>;
template<class T>
class Thread
{
public:
Thread(const std::string &threadname, func_t<T> func, T data)
:_tid(0)
, _threadname(threadname)
, _isrunning(false)
, _func(func)
, _data(data)
{}
static void *ThreadRoutine(void *args) //pthread_create的第三个参数:void *(*start_routine) (void *),类内方法有一个隐藏的参数:Thread*this,不符合,所以设计成静态方法
{
Thread *ts = static_cast<Thread *>(args);
ts->_func(ts->_data);
return nullptr;
}
bool Start()
{
int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);//将this作为ThreadRoutine的参数传入
if(n == 0)
{
_isrunning = true;
return true;
}
else return false;
}
bool Join()
{
if(!_isrunning) return true;
int n = pthread_join(_tid, nullptr);
if(n == 0)
{
_isrunning = false;
return true;
}
return false;
}
std::string ThreadName()
{
return _threadname;
}
bool IsRunning()
{
return _isrunning;
}
~Thread()
{}
private:
pthread_t _tid;//线程id
std::string _threadname;//线程名
bool _isrunning;//线程是否允许
func_t<T> _func;//线程要执行的函数
T _data;//线程要执行函数的参数
};
main.cc
std::string GetThreadName()
{
static int number = 1;
char name[64];
snprintf(name, sizeof(name), "Thread-%d", number++);
return name;
}
void Print(int num)
{
while (num)
{
std::cout << "hello world: " << num-- << std::endl;
sleep(1);
}
}
int main()
{
Thread<int> t(GetThreadName(), Print, 10);
std::cout<<t.IsRunning()<<std::endl;
std::cout<<t.ThreadName()<<std::endl;
t.Start();
std::cout<<t.IsRunning()<<std::endl;
t.Join();
std::cout<<t.IsRunning()<<std::endl;
return 0;
}
简单朴素的示例代码:多线程抢票
int ticket=10000;//全局的共享资源
void GetTicket(std::string name)
{
while(true)
{
if(ticket>0)
{
//充当抢票花费的时间
usleep(1000);
printf("%s get a ticket: %d\n",name.c_str(),ticket);
ticket--;
}
else
{
break;
}
//实际情况,还有后续动作...
}
}
int main()
{
std::string name1 = GetThreadName();
Thread<std::string> t1(name1,GetTicket,name1);
std::string name2 = GetThreadName();
Thread<std::string> t2(name2,GetTicket,name2);
std::string name3 = GetThreadName();
Thread<std::string> t3(name3,GetTicket,name3);
std::string name4 = GetThreadName();
Thread<std::string> t4(name4,GetTicket,name4);
t1.Start();
t2.Start();
t3.Start();
t4.Start();
t1.Join();
t2.Join();
t3.Join();
t4.Join();
return 0;
}
进程线程间的互斥相关背景概念
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
互斥量mutex
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个 线程,其他线程无法获得这种变量
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之 间的交互
多个线程并发的操作共享变量,会带来一些问题。
公共资源要被保护起来,否则就会出现数据不一致的情况
其中有一种保护策略是:任何一个时刻,只允许一个线程正在访问共享资源 --临界资源(也就是被保护起来的资源)
进程中访问临界资源的代码--临界区(被保护的区域)
多线程并发访问,因为时间片切换,会对数据的更新发生互相干扰,导致出现数据不一致问题。
我们需要知道-- 操作并不是原子操作,而是对应三条汇编指令:
load :将共享变量ticket从内存加载到寄存器中
update : 更新寄存器里面的值,执行-1操作
tore :将新值,从寄存器写回共享变量ticket的内存地址
++操作类似
为了防止出现数据不一致问题,我们要做到以下几点:
代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临 界区。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量
加锁:
1 我们要尽可能给少的代码块加锁
2 一般加锁,都是给临界区加锁
申请锁本身是安全的,原子的
规则是都必须先申请锁
根据互斥的定义,任何时刻,只允许一个线程申请锁成功,多个线程申请锁失败,失败的线程在mutex上进行阻塞,本质也就是等待
一个线程在临界区中访问临界资源的时候可不可能发生切换?
答案:可能,完全允许
互斥量的接口
初始化互斥量
初始化互斥量有两种方法:
方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict
attr);
参数:
mutex:要初始化的互斥量
attr:NULL
销毁互斥量
销毁互斥量需要注意:
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex); --原子的
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调用 pthread_ lock 时,可能会遇到以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量, 那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁
改进代码:
int ticket=10000;//全局的共享资源
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//锁有了,定义并被初始化
void GetTicket(std::string name)
{
while(true)
{
pthread_mutex_lock(&mutex);
if(ticket>0)
{
//充当抢票花费的时间
usleep(1000);
printf("%s get a ticket: %d\n",name.c_str(),ticket);
ticket--;
pthread_mutex_unlock(&mutex);
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
//实际情况,还有后续动作...
}
}
int ticket=10000;//全局的共享资源
void GetTicket(pthread_mutex_t*mutex)
{
while(true)
{
pthread_mutex_lock(mutex);
if(ticket>0)
{
//充当抢票花费的时间
usleep(1000);
printf("get a ticket: %d\n",ticket);
ticket--;
pthread_mutex_unlock(mutex);
}
else
{
pthread_mutex_unlock(mutex);
break;
}
//实际情况,还有后续动作...
}
}
int main()
{
pthread_mutex_t mutex;//局部使用锁
pthread_mutex_init(&mutex,nullptr);
std::string name1 = GetThreadName();
Thread<pthread_mutex_t*> t1(name1,GetTicket,&mutex);
std::string name2 = GetThreadName();
Thread<pthread_mutex_t*> t2(name2,GetTicket,&mutex);
std::string name3 = GetThreadName();
Thread<pthread_mutex_t*> t3(name3,GetTicket,&mutex);
std::string name4 = GetThreadName();
Thread<pthread_mutex_t*> t4(name4,GetTicket,&mutex);
t1.Start();
t2.Start();
t3.Start();
t4.Start();
t1.Join();
t2.Join();
t3.Join();
t4.Join();
pthread_mutex_destroy(&mutex);
return 0;
}
对锁进行封装:
LockGuard.hpp
#pragma once
#include <pthread.h>
// 不定义锁,默认认为外部会给我们传入锁对象
class Mutex
{
public:
Mutex(pthread_mutex_t *lock)
:_lock(lock)
{}
void Lock()
{
pthread_mutex_lock(_lock);
}
void Unlock()
{
pthread_mutex_unlock(_lock);
}
~Mutex()
{}
private:
pthread_mutex_t *_lock;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t *lock)
: _mutex(lock)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex _mutex;
};
main.cc
全局锁
int ticket = 10000; // 全局的共享资源
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void GetTicket(std::string name)
{
while (true)
{
{
LockGuard lockguard(&mutex); // 构建时自动加锁,析构时自动解锁
if (ticket > 0)
{
// 充当抢票花费的时间
usleep(1000);
printf("%s get a ticket: %d\n", name.c_str(), ticket);
ticket--;
}
else
{
break;
}
// 实际情况,还有后续动作...
}
}
}
int main()
{
std::string name1 = GetThreadName();
Thread<std::string> t1(name1, GetTicket, name1);
std::string name2 = GetThreadName();
Thread<std::string> t2(name2, GetTicket, name2);
std::string name3 = GetThreadName();
Thread<std::string> t3(name3, GetTicket, name3);
std::string name4 = GetThreadName();
Thread<std::string> t4(name4, GetTicket, name4);
t1.Start();
t2.Start();
t3.Start();
t4.Start();
t1.Join();
t2.Join();
t3.Join();
t4.Join();
return 0;
}
局部锁
class ThreadData
{
public:
ThreadData(const std::string &name, pthread_mutex_t *lock)
: threadname(name)
, pmutex(lock)
{}
public:
std::string threadname;
pthread_mutex_t *pmutex;
};
int ticket = 10000; // 全局的共享资源
void GetTicket(ThreadData*td)
{
while (true)
{
{
LockGuard lockguard(td->pmutex); // 构建时自动加锁,析构时自动解锁
if (ticket > 0)
{
// 充当抢票花费的时间
usleep(1000);
printf("%s get a ticket: %d\n", td->threadname.c_str(), ticket);
ticket--;
}
else
{
break;
}
// 实际情况,还有后续动作...
}
}
}
int main()
{
pthread_mutex_t mutex;
pthread_mutex_init(&mutex,nullptr);
std::string name1 = GetThreadName();
ThreadData*td = new ThreadData(name1,&mutex);
Thread<ThreadData*> t1(name1, GetTicket, td);
std::string name2 = GetThreadName();
ThreadData*td2 = new ThreadData(name2,&mutex);
Thread<ThreadData*> t2(name2, GetTicket, td2);
std::string name3 = GetThreadName();
ThreadData*td3 = new ThreadData(name3,&mutex);
Thread<ThreadData*> t3(name3, GetTicket, td3);
std::string name4 = GetThreadName();
ThreadData*td4 = new ThreadData(name4,&mutex);
Thread<ThreadData*> t4(name4, GetTicket, td4);
t1.Start();
t2.Start();
t3.Start();
t4.Start();
t1.Join();
t2.Join();
t3.Join();
t4.Join();
pthread_mutex_destroy(&mutex);
delete td;
delete td2;
delete td3;
delete td4;
return 0;
}
个别系统中,抢票代码会出现很多的票都被同一个线程抢完了
多线程运行,同一份资源,有线程长时间无法拥有,称之为饥饿问题,要解决饥饿问题,要让线程执行的时候,具备一定的顺序性--同步
线程加锁的本质:简单理解锁是数字1
struct
{
//.....
int mutex = 1;
}
关于加锁的原则:谁加锁,谁解锁
可重入VS线程安全:
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们 称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重 入函数,否则,是不可重入函数
可重入还是不可重入,描述的是函数的特点,线程安全与否,描述的是线程的特征
常见的线程不安全的情况:
不保护共享变量的函数、函数状态随着被调用,状态发生变化的函数、返回指向静态变量指针的函数、调用线程不安全函数的函数
常见的线程安全的情况:
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况:
调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
常见可重入的情况:
不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系:
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
可重入与线程安全区别:
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生 死锁,因此是不可重入的。
死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态
我们是为了保护资源,才使用锁
死锁四个必要条件
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
破坏死锁的四个必要条件、加锁顺序一致、避免锁未释放的场景、资源一次性分配
死锁产生有4个必要条件(必须都满足,才死锁)-->解决/避免死锁-->破坏4个必要条件中的一个或者多个
破坏循环等待条件:建议按照同样的次序申请锁,尽量把锁资源按照顺序一次就给申请线程了
线程同步
条件变量
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。 例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量
同步概念与竞态条件
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。
互斥能保证资源的安全,同步能够较为充分高效地使用资源
条件变量函数
初始化
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); //一次唤醒一个线程
为什么 pthread_cond_wait 需要互斥量?
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件 变量上的线程。 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据
条件变量使用规范
等待条件代码
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);
下面来使用一下:
Makefile
testCond:testCond.cc
g++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:
rm testCond
testCond.cc
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *threadRoutine(void *args)
{
std::string name = static_cast<const char*>(args);
while(true)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);//???
std::cout<<"I am a new thread : "<<name<<std::endl;
pthread_mutex_unlock(&mutex);
}
}
//主线程
int main()
{
pthread_t t1,t2,t3;
pthread_create(&t1,nullptr,threadRoutine,(void*)"thread-1");
pthread_create(&t2,nullptr,threadRoutine,(void*)"thread-2");
pthread_create(&t3,nullptr,threadRoutine,(void*)"thread-3");
sleep(5);//5s开始进行让cond成立,唤醒一个线程
while(true)
{
pthread_cond_signal(&cond);//唤醒一个线程
//pthread_cond_broadcast(&cond);//或者唤醒全部线程
sleep(1);
}
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
return 0;
}
也可以唤醒全部线程
pthread_cond_broadcast(&cond);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int tickets = 1000;
void *threadRoutine(void *args)
{
std::string name = static_cast<const char*>(args);
while(true)
{
//单纯的互斥,能保证数据安全,不一定合理或者有效
pthread_mutex_lock(&mutex);
if(tickets>0)
{
std::cout << name<< ", get a ticket: " << tickets-- << std::endl; // 模拟抢票
usleep(1000);
}
else
{
std::cout << "没有票了," << name << std::endl; // 每一个线程在大量的申请锁和释放锁
//1 线程在进行等待的时候,会自动释放锁
//2 线程被唤醒的时候,是在临界区内唤醒的,当线程被唤醒,线程在pthread_cond_wait返回的时候,要重新申请并持有锁
//3 当线程被唤醒的时候,重新申请并持有锁的本质是也要参与锁的竞争的
pthread_cond_wait(&cond,&mutex);//???
}
pthread_mutex_unlock(&mutex);
}
}
//主线程
int main()
{
pthread_t t1,t2,t3;
pthread_create(&t1,nullptr,threadRoutine,(void*)"thread-1");
pthread_create(&t2,nullptr,threadRoutine,(void*)"thread-2");
pthread_create(&t3,nullptr,threadRoutine,(void*)"thread-3");
sleep(5);//5s开始进行让cond成立
while(true)
{
sleep(6);
pthread_mutex_lock(&mutex);
tickets+=100;//放票
pthread_mutex_unlock(&mutex);
pthread_cond_broadcast(&cond);
}
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
return 0;
}
生产者消费者模型
为何要使用生产者消费者模型?
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的
生产者消费者模型优点
解耦 、支持并发、 支持忙闲不均
基于BlockingQueue的生产者消费者模型
BlockingQueue
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
阻塞队列代码:
Makefile
testblockqueue:Main.cc
g++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:
rm testblockqueue
BlockQueue.hpp
const int defaultcap = 5;
template<class T>
class BlockQueue
{
public:
BlockQueue(int cap = defaultcap)
:_capacity(cap)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_p_cond, nullptr);
pthread_cond_init(&_c_cond, nullptr);
}
bool IsFull()
{
return _q.size() == _capacity;
}
bool IsEmpty()
{
return _q.size() == 0;
}
void Push(const T &in) // 生产者的
{
pthread_mutex_lock(&_mutex);
while(IsFull())//写成while,会有判断-->代码具有鲁棒性
{
//阻塞等待
pthread_cond_wait(&_p_cond, &_mutex);
}
_q.push(in);
pthread_cond_signal(&_c_cond);
pthread_mutex_unlock(&_mutex);
}
void Pop(T *out) // 消费者的
{
pthread_mutex_lock(&_mutex);
while(IsEmpty())
{
//阻塞等待
pthread_cond_wait(&_c_cond, &_mutex);//此函数存在伪唤醒的情况:如果消费线程全部一次性被唤醒(broadcast),那么消费线程之间就会互相竞争锁,如果一次只生产一个,那么当第一个消费线程申请锁成功且消费,释放锁,再有第二个消费线程申请锁成功,也去消费,就会出错,因为没有资源消费了,所以不能写成if(IsEmpty()),而要写成while(IsEmpty()),这样不管是哪种唤醒,都会去while循环处判断一下,满足条件才继续
}
*out = _q.front();
_q.pop();
pthread_cond_signal(&_p_cond);
pthread_mutex_unlock(&_mutex);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_p_cond);
pthread_cond_destroy(&_c_cond);
}
private:
std::queue<T> _q;
int _capacity; // _q.size() == _capacity->满了,则不能再生产,_q.size() == 0->空,不能再消费了
pthread_mutex_t _mutex;
pthread_cond_t _p_cond; // 给生产者的条件变量
pthread_cond_t _c_cond; // 给消费者的条件变量
};
Main.cc
void *consumer(void *args)
{
BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
while(true)
{
//1.消费数据
int data = 0;
bq->Pop(&data);
//2.进行处理
std::cout << "consumer data: "<<data<<std::endl;
}
return nullptr;
}
void *productor(void *args)
{
BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);//生产者和消费者同时看到了同一份资源
while(true)
{
//1.有数据
int data = rand()%10+1;
//2.进行生产
bq->Push(data);
std::cout<<"productor data: "<<data<<std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
srand((uint16_t)time(nullptr) ^ getpid() ^ pthread_self()); // 只是为了形成更随机的数据
BlockQueue<int>*bq = new BlockQueue<int>();
pthread_t c,p;//消费者和生产者(都是线程)
pthread_create(&c,nullptr,consumer,bq);
pthread_create(&p,nullptr,productor,bq);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
return 0;
}
生产者不仅仅可以传递整数数据给消费者,当然也可以分派任务给消费者
生产消费者模型里面交换的可以是基本数据,也可以是类对象
下面是分派简单的计算任务:
LockGuard.hpp
对锁封装一下
// 不定义锁,默认认为外部会给我们传入锁对象
class Mutex
{
public:
Mutex(pthread_mutex_t *lock)
:_lock(lock)
{}
void Lock()
{
pthread_mutex_lock(_lock);
}
void Unlock()
{
pthread_mutex_unlock(_lock);
}
~Mutex()
{}
private:
pthread_mutex_t *_lock;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t *lock)
: _mutex(lock)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex _mutex;
};
BlockQueue.hpp
const int defaultcap = 5;
template<class T>
class BlockQueue
{
public:
BlockQueue(int cap = defaultcap)
:_capacity(cap)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_p_cond, nullptr);
pthread_cond_init(&_c_cond, nullptr);
}
bool IsFull()
{
return _q.size() == _capacity;
}
bool IsEmpty()
{
return _q.size() == 0;
}
void Push(const T &in) // 生产者的
{
LockGuard lockguard(&_mutex);//构建时自动申请锁,析构时自动释放锁//申请锁释放锁的方式2
//pthread_mutex_lock(&_mutex);//申请锁释放锁的方式1
while(IsFull())
{
//阻塞等待
pthread_cond_wait(&_p_cond, &_mutex);
}
_q.push(in);
pthread_cond_signal(&_c_cond);
//pthread_mutex_unlock(&_mutex);//申请锁释放锁的方式1
}
void Pop(T *out) // 消费者的
{
LockGuard lockguard(&_mutex);
//pthread_mutex_lock(&_mutex);
while(IsEmpty())
{
//阻塞等待
pthread_cond_wait(&_c_cond, &_mutex);
}
*out = _q.front();
_q.pop();
pthread_cond_signal(&_p_cond);
//pthread_mutex_unlock(&_mutex);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_p_cond);
pthread_cond_destroy(&_c_cond);
}
private:
std::queue<T> _q;
int _capacity; // _q.size() == _capacity->满了,则不能再生产,_q.size() == 0->空,不能再消费了
pthread_mutex_t _mutex;
pthread_cond_t _p_cond; // 给生产者的条件变量
pthread_cond_t _c_cond; // 给消费者的条件变量
};
Task.hpp
const int defaultvalue = 0;
enum
{
ok = 0,
div_zero,
mod_zero,
unknow
};
const std::string opers = "+-*/%)(&^";
class Task
{
public:
Task()
{}
Task(int x, int y, char op)
: data_x(x)
, data_y(y)
, oper(op)
, result(defaultvalue)
, code(ok)
{}
void Run()
{
switch (oper)
{
case '+':
result = data_x + data_y;
break;
case '-':
result = data_x - data_y;
break;
case '*':
result = data_x * data_y;
break;
case '/':
{
if (data_y == 0)
code = div_zero;
else
result = data_x / data_y;
}
break;
case '%':
{
if (data_y == 0)
code = mod_zero;
else
result = data_x % data_y;
}
break;
default:
code = unknow;
break;
}
}
void operator()()
{
Run();
}
std::string PrintTask()
{
std::string s;
s = std::to_string(data_x);
s += oper;
s += std::to_string(data_y);
s += "=?";
return s;
}
std::string PrintResult()
{
std::string s;
s = std::to_string(data_x);
s += oper;
s += std::to_string(data_y);
s += "=";
s += std::to_string(result);
s += " [";
s += std::to_string(code);
s += "]";
return s;
}
~Task()
{}
private:
int data_x;
int data_y;
char oper; // + - * / %
int result;
int code; // 结果码,0: 结果可信 !0: 结果不可信,如1,2,3,4
};
Main.cc
void *consumer(void *args)
{
BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);
while(true)
{
sleep(1);
Task t;
//1.消费数据
bq->Pop(&t);
//2.进行处理
t();
std::cout << "consumer data: " << t.PrintResult() << std::endl;
}
return nullptr;
}
void *productor(void *args)
{
BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(args);//生产者和消费者同时看到了同一份资源
while(true)
{
//1.有数据
int data1 = rand() % 10;
usleep(rand()%123);
int data2 = rand() % 10;
usleep(rand()%123);
char oper = opers[rand() % (opers.size())];
Task t(data1, data2, oper);
std::cout << "productor task: " << t.PrintTask() << std::endl;
//2.进行生产
bq->Push(t);
}
return nullptr;
}
int main()
{
srand((uint16_t)time(nullptr) ^ getpid() ^ pthread_self()); // 只是为了形成更随机的数据
BlockQueue<Task> *bq = new BlockQueue<Task>();
pthread_t c,p;//消费者和生产者(都是线程)
pthread_create(&c,nullptr,consumer,bq);
pthread_create(&p,nullptr,productor,bq);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
return 0;
}