目录
线程概念
线程控制
创建线程
线程等待
线程终止
线程分离
LWP
线程互斥
互斥量接口
死锁
线程同步
条件变量
条件变量接口
生产者和消费者模型
线程:是在进程内部运行的一个执行分支(执行流),属于进程的一部分,粒度比进程更细和轻量化。
Linux中没有专门为线程设计的TCB(Thread Control Block),而是用进程的PCB(Process Control Block)来模拟线程。
进程是承担分配系统资源的基本实体
线程是CPU调度的基本单位,承担进程资源的一部分的基本实体
即进程划分资源给线程。
Linux因为是用进程来模拟的线程,所以并没有给我们提供操作线程的接口,而是提供在同一个地址空间创建PCB的方法,分配资源给指定的PCB的接口。
创建线程、释放线程、等待线程和分离线程等都是由系统级别的工程师在用户层对Linux轻量级进程接口进行封装,给我们打包成库,让用户直接使用库接口,即用户层的原生线程库(pthread库)。
一般而言,线程也是需要被等待的,如果不等待可能会造成类似“僵尸进程”的问题。
线程终止分为3种情况,1、代码跑完结果对2、代码跑完结果不对3、代码异常。
对于前两种情况,线程等待是可以处理的。但是第3三种情况,线程是不需要处理的,因为异常的时候内核会向进程发送信号,而信号的处理是进程的业务。
示例如下:
void* thread_run(void* args){
int num = *(int*)args;
while(1){
printf("我是新线程[%d], 我创建的线程ID是:%lu\n", num, pthread_self());
sleep(2);
break;
}
return (void*)111;//函数的返回值
}
#define NUM 1
int main(){
pthread_t tid[NUM];
for(int i =0; i< NUM; i++){
pthread_create( tid + i, NULL, thread_run, (void*)&i);
sleep(1);
}
printf("wait sub thread ...\n");
sleep(5);
void* status = NULL;
int ret =0;
for(int i =0; i < NUM; i++){
ret = pthread_join(tid[i], &status);//获取线程退出信息
}
printf("ret: %d, status %d\n", ret, (int)status);
printf("wait success!\n");
sleep(1);
return 0;
}
运行结果如下
1、函数中的return(a、main函数return的时候代表进程和主线程退出。b、其他线程函数return的时候,只表示当前线程退出。)
2、线程可以通过pthread_exit终止自己(exit是终止进程)
3、取消目标线程
当线程终止之后,可以选择线程等待。但也有另一种方式,就是线程分离。即,线程分离之后,分离的线程不再需要被等待,运行结束之后,自行释放。(同属一个进程,但是确实陌生人)
下边的代码使用pthread_create创建线程,并不断打印线程id。
#include
#include
#include
void* thread_run(void* args){
const char* id = (const char*)args;
while(1){
printf("I am %s thread, %d\n", id, getpid());
sleep(1);
}
}
int main(){
pthread_t tid;
pthread_create(&tid, NULL, thread_run, (void*)"thread 1");
while(1){
printf("I am main thread, %d\n", getpid());
sleep(1);
}
return 0;
}
使用ps -aL命令查看进程与线程情况
我们可以观察到一个LWP的属性, 这里需要详细说明以下线程id和LWP的关系。
我们常使用的线程id和LWP并不是一样的。线程id是用户层使用的一个虚拟地址,而LWP是内核中管理线程的属性。
上边的主线程的PID和新线程的PID都是相同的,只有LWP不同,这也说明了Linux下使用进程模拟的线程的说法,线程是一种轻量级的进程,是CPU调度的最小单元。
我们使用的线程库(pthread)是一个由系统级的工程师在用户层对Linux轻量级进程接口进行封装,给我们打包成的库,是第三方库,但也是最接近系统的库。也能理解成是一种文件,是文件在运行的时候就会被加载进内存,和虚拟地址空间进行映射,而我们平常所说的线程id就是一个虚拟地址空间的地址。
其次,每个线程都会产生运行时的临时数据,都要存在一个自己的私有栈结构来保存这些临时数据。但是linux并没有写线程的TCB,而只提供了分配资源给指定的PCB的接口。因此产生了pthread线程库,而每个线程的私有栈结构也都是由pthread第三方库来维护的。线程的私有栈结构中就有内核中维护其线程的LWP属性,当我们拥有了线程id就能轻松访问到对应线程的私有栈,而通过私有栈中维护着若干线程属性,就包括LWP。
线程id和LWP的关系我们可以类比成C语言中文件操作的FILE结构体和fd的关系。FILE中绝对封装了fd,因为它需要向文件写入或者读取内容,需要访问磁盘。
在任意时刻,只允许一个执行流访问某段代码(某部分资源)就称为互斥
一件事情,要么执行要么不执行,没有中间态称为原子性。
多线程执行流共享的资源称为临界资源
每个线程内部,访问临界资源的代码,称为临界区
也许这两句话太过于抽象,我们呢可以执行下边的代码完成让几个创建的线程去抢票的逻辑。
#include
#include
#include
#include
// 抢票逻辑,10000张票,5个线程抢
class Ticket{
public:
Ticket():tickets(1000){
pthread_mutex_init(&mtx, nullptr);
}
bool GetTicket(){
bool ret = true;
if(tickets > 0){
usleep(1000);
printf("我是[%ld],我要抢的票是 %d\n", pthread_self(), tickets--);
printf("");
}
else{
printf("票已售完!\n");
ret = false;
}
return ret;
}
~Ticket(){
pthread_mutex_destroy(&mtx);
}
private:
int tickets;
pthread_mutex_t mtx;
};
void* ThreadRun(void* args){
Ticket* t = (Ticket*)args;
std::string tname = (char*)args;
while(true){
if(!t->GetTicket()){
break;
}
else{
continue;
}
}
}
int main(){
Ticket* t = new Ticket();
pthread_t tid[5];
for(int i =0;i<5;i++){
int* id = new int(i);
pthread_create(tid + i, nullptr, ThreadRun, (void*)t);
}
for(int i =0; i< 5; i++){
pthread_join(tid[i], nullptr);
}
return 0;
}
运行结果如下:
我们可以观察到,打印的结果居然还出现了负数,这并不会是我们预期的结果。
上边的tickets出现问题的核心在于tickets--,给出下图。
tickets的动作并不是原子性的(非安全的),首先tickets--是一个运算动作,需要将内存中的tickets数据加载进CPU中的寄存器,运算完成后,再将运算结果返回至tickets中。这样tickets--的过程就有两步。但是我们都知道,进程是被切换的,而进程的临时数据都是保存在自己的私有栈结构上的。比如进程A拿到tickets资源,运算至999的时候就被切换了,其临时数据保存在A的私有栈结构上。然后进程B进来拿到tickets资源进行运算,然后运算至1的时候才被切换,其临时数据保存在B的私有栈结构上。然后再切换A进程,A进程则是将自己的临时数据拿出来继续运算,这时的tickets的数据是999。如果是这样的话,就会导致进程B抢到的998张票失效的问题。
这种问题就是因为临界资源(这里就是tickets)在某一时间段中,被两个或者多个执行流访问了。
要做到在任意时刻,只允许一个执行流访问某段代码(某部分资源)需要一把锁。Linux提供的这把锁称为互斥量。
互斥量的初始化和销毁
其中pthread_mutex_init()的第二个参数一般可以设为null值。
互斥量加锁和解锁
对于加锁和解锁的进一步理解,加锁是为了保证临界资源的安全性,在任意时刻只能有一个线程访问临界资源。但是我们加的锁是如何保证安全性的呢?即如何保证原子性(在汇编级别只有1行代码)的?Linux提供了swap或exchange指令,使用一条汇编完成内存和寄存器内数据交换。
在加锁和解锁之间的临界区的时候,进程也可能被切换走,在进程被切走的时候,临时数据会被保存在自己的私有栈结构中,这里的临时数据也包括自己竞争成功的锁数据。也就是在此期间,其他进程休想竞争锁成功,因为锁资源也随着该进程被切走了。只有当该进程访问完临界资源之后,再释放锁,才有可能被其它进程竞争成功。
利用互斥锁我们可以改造一下之前的买票代码:
#include
#include
#include
#include
// 抢票逻辑,10000张票,5个线程抢
class Ticket{
public:
Ticket():tickets(1000){
pthread_mutex_init(&mtx, nullptr);
}
bool GetTicket(){
bool ret = true;
pthread_mutex_lock(&mtx);//加锁
if(tickets > 0){
usleep(1000);
printf("我是[%ld],我要抢的票是 %d\n", pthread_self(), tickets--);
printf("");
}
else{
printf("票已售完!\n");
ret = false;
}
pthread_mutex_unlock(&mtx);//解锁
return ret;
}
~Ticket(){
pthread_mutex_destroy(&mtx);
}
private:
int tickets;
pthread_mutex_t mtx;
};
void* ThreadRun(void* args){
Ticket* t = (Ticket*)args;
std::string tname = (char*)args;
while(true){
if(!t->GetTicket()){
break;
}
else{
continue;
}
}
}
int main(){
Ticket* t = new Ticket();
pthread_t tid[5];
for(int i =0;i<5;i++){
int* id = new int(i);
pthread_create(tid + i, nullptr, ThreadRun, (void*)t);
}
for(int i =0; i< 5; i++){
pthread_join(tid[i], nullptr);
}
return 0;
}
执行结果
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。
死锁的四个必要条件
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
初始化和销毁
pthread_cond_init()的第二个参数一般可以设置为null值。
进行等待
唤醒等待
示例:下边代码是由boss进程唤醒workers进程
#include
#include
#include
#include
pthread_mutex_t mtx;
pthread_cond_t cond;
//ctrl thread 控制work线程,让他定期运行
void *ctrl(void *args)
{
std::string name = (char*)args;
while(true){
//pthread_cond_signal: 唤醒在条件变量下等待的一个线程,哪一个??
//在cond 等待队列里等待的第一个线程
std::cout << "master say : begin work" << std::endl;
//pthread_cond_signal(&cond);
//唤醒所有线程
pthread_cond_broadcast(&cond);
sleep(5);
}
}
void *work(void *args)
{
int number = *(int*)args;
delete (int*)args;
while(true){
pthread_cond_wait(&cond, &mtx);
std::cout << "worker: " << number << " is working ..." << std::endl;
}
}
int main()
{
#define NUM 3
pthread_mutex_init(&mtx, nullptr);
pthread_cond_init(&cond, nullptr);
pthread_t master;
pthread_t worker[NUM];
pthread_create(&master, nullptr, ctrl, (void*)"boss");//boss进程
for(int i = 0; i < NUM; i++){
int *number = new int(i);
pthread_create(worker+i, nullptr, work, (void*)number);//workers进程
}
for(int i = 0; i < NUM; i++){
pthread_join(worker[i], nullptr);
}
pthread_join(master, nullptr);
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&cond);
return 0;
}
运行结果:
条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须 要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件 变量上的线程。
1、生产者消费者模型将生产环节和消费环节进行了“解耦”,使得生产过程和消费过程得以并行,互不影响。
2、提高了效率
生产者&生产者------------竞争(互斥)关系
消费者&消费者------------竞争(互斥)关系
消费者&生产者------------同步、互斥关系
生产者和消费者是不同的执行流。
生产者和消费者之间存在一段缓冲区。
示例:
BlockQueue.hpp(缓冲区)
#pragma once
#include
#include
#include
namespace ns_blockqueue
{
const int default_cap = 5;
template
class BlockQueue
{
private:
std::queue bq_; //我们的阻塞队列
int cap_; //队列的元素上限
pthread_mutex_t mtx_; //保护临界资源的锁
//1. 当生产满了的时候,就应该不要生产了(不要竞争锁了),而应该让消费者来消费
//2. 当消费空了,就不应该消费(不要竞争锁了),应该让生产者来进行生产
pthread_cond_t is_full_; //bq_满的, 消费者在改条件变量下等待
pthread_cond_t is_empty_; //bq_空的,生产者在改条件变量下等待
private:
bool IsFull()
{
return bq_.size() == cap_;
}
bool IsEmpty()
{
return bq_.size() == 0;
}
void LockQueue()
{
pthread_mutex_lock(&mtx_);
}
void UnlockQueue()
{
pthread_mutex_unlock(&mtx_);
}
void ProducterWait()
{
//pthread_cond_wait
//1. 调用的时候,会首先自动释放mtx_!,然后再挂起自己
//2. 返回的时候,会首先自动竞争锁,获取到锁之后,才能返回!
pthread_cond_wait(&is_empty_, &mtx_);
}
void ConsumerWait()
{
pthread_cond_wait(&is_full_, &mtx_);
}
void WakeupComsumer()
{
pthread_cond_signal(&is_full_);
}
void WakeupProducter()
{
pthread_cond_signal(&is_empty_);
}
public:
BlockQueue(int cap = default_cap):cap_(cap)
{
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_);
}
public:
//const &:输入
//*: 输出
//&: 输入输出
void Push(const T &in)
{
LockQueue();
//临界区
if(IsFull()){ //bug?
//等待的,把线程挂起,我们当前是持有锁的!!!
ProducterWait();
}
//向队列中放数据,生产函数
bq_.push(in);
if(bq_.size() > cap_/2 ) WakeupComsumer();
UnlockQueue();
//WakeupComsumer();
}
void Pop(T *out)
{
LockQueue();
//从队列中拿数据,消费函数函数
if(IsEmpty()){ //bug?
//无法消费
ConsumerWait();
}
*out = bq_.front();
bq_.pop();
if(bq_.size() < cap_/2 ) WakeupProducter();
UnlockQueue();
}
};
}
CPtest.cc文件
#include "BlockQueue.hpp"
#include
#include
#include
using namespace ns_blockqueue;
void *consumer(void *args)
{
BlockQueue *bq = (BlockQueue*)args;
while(true){
sleep(2);
int data = 0;
bq->Pop(&data);
std::cout << "消费者消费了一个数据: " << data << std::endl;
}
}
void *producter(void *args)
{
BlockQueue *bq = (BlockQueue*)args;
while(true){
// sleep(2);
//1. 制造数据,生产者的数据(task)从哪里来??
int data = rand()%20 + 1;
std::cout << "生产者生产数据: " << data << std::endl;
bq->Push(data);
}
}
int main()
{
srand((long long)time(nullptr));
BlockQueue *bq = new BlockQueue();
pthread_t c,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;
}
运行结果: