Linux征途——多线程

线程是Linux最为重要的内容,熟练掌握线程是系统编程的最重要的一环。

目录

线程的概念

线程的特点

多进程和多线程对比

线程的控制

线程创建

pid、tgid、tid

线程终止

线程等待

线程分离

线程安全

互斥的实现:互斥锁

同步的实现:条件变量

生产者消费者模型

posix标准信号量

信号量实现生产者消费者

线程池


线程的概念

在传统操作系统中PCB是一个进程,但由于Linux下的线程是以进程的pcb模拟的,所以Linux下的pcb实际是一个个线程。因为这些pcb相较于传统的pcb更加轻量化,所以Linux下的线程也叫轻量化的进程。一个进程中至少有一个线程(进程产生一定会有程序要运行)所以进程中至少有一个PCB。CPU调度的是PCB,线程即是CPU调度的基本单位。而进程是一个线程组,是资源分配的基本单位。

线程的特点

           由于进程的线程运行在同一个虚拟地址空间:共享进程代码段和数据段

  •  共享的资源:
  1.  文件描述符表
  2.  每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  3.  当前⼯作目录
  4.  ⽤户id和组id
  •  独立的资源
  1. 独立的数据
  2. 独立的寄存器
  3. 独立的errno
  4. 独立的信号屏蔽字
  5. 线程id

多进程和多线程对比

  • 线程的优点:源于(使用同一个虚拟地址空间)
  1. 线程的销毁和创建成本更低-----》进程创建页表等
  2. 线程间通信更加方便----》同一个进程
  3. 线程的调度成本低----》进程调度需要切换页表,线程只用切换pcb
  4. 线程的执行粒度更加细致-----》细致到处理函数
  • 线程的缺点:
  1. 缺乏控制,因此编码需要注意的问题很多
  2. 稳定性低/健壮性差

线程的控制

  • 线程创建

操作系统没有提供直接创建线程的系统调用接口,所以大佬们封装一套线程控制接口----封装了线程库供用户使用
我们创建的线程是一个用户态线程-----

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;失败返回错误码

pthread_t pthread_self(void);
 返回调用线程的线程id

 

  • pid、tgid、tid

运行上述程序。在另一个终端上运行  ps -eLf |head -1 && ps -eLf |grep Create |grep -v grep(Create是我的执行程序名字)x下面只是显示了一部分。ps是显示进程信息

UID:启动这些进程的用户。
PID:进程的进程ID。
PPID:父进程的进程号
LWP:线程号,也可以说是轻量级进程号
C:进程生命周期中的CPU利用率。
NLWP:有多少个线程
STIME:进程启动时的系统时间。
TTY:进程启动时的终端设备。
TIME:运行进程需要的累计CPU时间。
CMD:启动的程序名称。

上图可以看到PID是相同的,说明只有一个进程,但是LWP有两个,说明是两个线程,NLWP是线程的个数。
现在来说说三个id的历史。

  • pid

因为线程是进程模拟实现的,所以以后的pid就是线程id,当然可以理解位轻量级进程id

  • tgid

那进程的id就要用另一个名称,就是tgid。为什么是tgid,因为现在的进程是线程组,所以tgid就是thread group id。

  • tid

在代码中我们无法获取这个LWP,但是有一个tid是可以获取的,它的类型是pthread_t ,tid是一个地址,但也唯一标识一个线程,所以也是线程id,以后我们在代码中通过这个tid来操作线程。后面对线程控制都是用的tid这个线程id。

  • 接口

getpid()-----返回进程,也就是线程组的id,这个id是tgid
gettid()------返回线程id,这个id是pid。这个接口一般不使用
pthread_self(void)-----返回调用线程的id,这个id是tid

  • 线程终止

void pthread_exit(void *value_ptr);
退出调用线程,并且返回一个value_ptr
exit(0); exit退出的是整个进程(所有线程都会退出)

这里有个注意的地方,就是return
在普通线程中return 是退出线程,在main中return直接退出进程



 
线程打印一次之后,就退出了。由于我不需要返回值去干嘛,所以返回值置空。

int pthread_cancel(pthread_t thread);
thread:线程ID
返回值:成功返回0;失败返回错误码

pthread_exit()--------退出调用线程
pthread_cancle()----取消指定线程



线程还是执行一次后退出,注意如果在线程中退出自己,那么程序会卡住,因为这是非法操作。

  • 线程等待

等待指定线程退出,线程退出未处理也会形成僵尸进程。(前面没有等待,所以也是僵尸线程、线程是不会被显示,因为我们查看进程信息的时候主要显示的就是主线程的信息,所以这里能看到)

一个线程被创建,默认有一个属性joinable,处于这个属性的线程必须等待,因为该类线程退出后,不会自动回收资源,会造成内存泄漏

pthread_join(pthread_t thread, void **value_ptr);
thread:线程ID
value_ptr:它指向⼀个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

 

  • 线程分离

为了解决上面的问题,这里我们将将线程的join able属性修改为detach属性,处于这个属性的线程,退出后将自动回收资源,但是不能被等待,否则报错,但是不会崩溃
int pthread_detach(pthread_t thread)
线程分离可以在线程任意时间分离,包括分离自己

可以直接在入口函数中直接分离。

线程安全

  •     发生:多个执行流对数据的竞争操作,这是不安全的。对比前面的重入函数是一个道理。
  •     解决------ 同步和互斥
  •     加粗的接口是常用的

        同步:对临界资源的时序可控;例:用户对缓冲区输入数据,CPU从缓冲区拿数据。有则拿,无则无操作。
        互斥:对临界资源的唯一访问;例:缓冲区只允许有一个读或者写操作。

  • 互斥的实现:互斥锁

互斥锁变量:pthread_mutex_t mutex
由于线程创建之后,谁先运行不一定,所以创建线程之前初始化。操作步骤如下:

  • 1、初始化

静态初始化:pthread_mutex_t mutex = READ_MUTEX_INITIALIZER:直接初始化
动态初始化:int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
 mutex:要初始化的互斥量
 attr:通常NULL

  •  2、加锁/解锁

 加锁:int pthread_mutex_lock(pthread_mutex_t *mutex);能加则加,加不了,阻塞
           int pthread_mutex_trylock(pthread_mutex_t *mutex);尝试枷锁,加不上,报错退出
解锁:int  pthread_mutex_unlock(pthread_mutex_t *mutex);有加锁必解锁
                返回值:成功返回0,失败返回错误号

  • 3、销毁

 锁的销毁:int pthread_mutex_destroy(pthread_mutex_t *mutex)
注意:
使⽤PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
不要销毁⼀个已经加锁的互斥量
 已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁

产生死锁必要条件 :(必要条件)前提两个或两个以上的锁

  1. 互斥条件:有互斥才有互斥锁,我操作时别人不能操作
  2. 不可剥夺条件:我的锁别人不能解
  3. 请求与保持条件:拥有自己的锁,请求别的锁,未得到别的锁,不解自己的锁
  4. 环路等待条件:A锁,请求自己的锁,自己请求A锁

 死锁的预防:破坏必要条件
死锁的避免:检测(银行家算法)

  • 同步的实现:条件变量

条件变量:pthread_cond_t  条件满足则唤醒,条件不满足则等待

  • 初始化:

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
cond:要初始化的条件变量
attr:通常NULL

  • 等待:

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
cond:要在这个条件变量上等待
mutex:互斥量

这里必须说一下为什么要加互斥锁。因为这个wait的内部并不是原子操作,而是先进行了该线程互斥量的解锁,然后线程被挂起等待,当有其它线程唤醒这个线程的时候,他就被唤醒然后进行互斥量加锁。pthread_cond_wait(cv, mutex) 实际做的事情就是下面蓝色框内几个事情。后面我们对生产者消费者模型还原,就知道了。

  • 唤醒:

 int pthread_cond_signal(pthread_cond_t *cond);

int pthread_cond_broadcast(pthread_cond_t *cond);

  • 销毁:

int pthread_cond_destroy(pthread_cond_t *cond)

  • 生产者消费者模型

先来了解一下生产者消费者模型吧。

#include<iostream>
#include<queue>
#include<pthread.h>
using namespace std;


class BolckQueue
{
	public:
		//构造函数
		BolckQueue()
			:capacity_(10)
		{
			pthread_mutex_init(&mutex_,NULL);
			pthread_cond_init(&product_,NULL);
			pthread_cond_init(&customer_,NULL);

		}
		//析构函数
		~BolckQueue()
		{
			pthread_mutex_destroy(&mutex_);
			pthread_cond_destroy(&product_);
			pthread_cond_destroy(&customer_);
		}
		//队列的接口
		void Push(int val)
		{
			QueueBlock();//先对队列进行加锁
			while(QueueIsFull())//如果队列是满的就让生产者等待
			{
				ProductWait();
                    //现在就来说说这个等待
                    //如果队列是满的,生产者线程就对临界资源解锁,让消费者取临界资源取数据
                    //等临界资源有空间了,消费者线程对生产者线程进行唤醒
                    //生产者线程就对临界资源加锁,上面就是wait操作,然后跳出循环,开始递送数据
                
			}
			queue_.push(val);
			QueueUnblock();//队列解锁
			CustomerWake();//对消费者唤醒
		}
		int Pop()
		{
			QueueBlock();//队列加锁
			while(QueueIsEmpty())
			{
				CustomerWait();//如果队列为空,消费者等待
			}
			int val = queue_.front();
			queue_.pop();
			QueueUnblock();
			ProductWake();//消费者拿出后,唤醒生产者
			return val;
		}
		//成员私有接口
	private:
		void QueueBlock()
		{
			pthread_mutex_lock(&mutex_);
		}
		void QueueUnblock()
		{
			pthread_mutex_unlock(&mutex_);
		}
		bool QueueIsFull()
		{
			return queue_.size() == capacity_;
		}
		bool QueueIsEmpty()
		{
			return queue_.empty();
		}
		void ProductWait()
		{
			pthread_cond_wait(&product_,&mutex_);		
		}
		void ProductWake()
		{
			pthread_cond_signal(&product_);
		}
		void CustomerWait()
		{
			pthread_cond_wait(&customer_,&mutex_);		
		}
		void CustomerWake()
		{
			pthread_cond_signal(&customer_);
		}

		//队列成员变量
		queue<int> queue_;
		int capacity_;//队列的容量
		pthread_cond_t product_;//生产者条件变量
		pthread_cond_t customer_;//消费者条件变量
		pthread_mutex_t mutex_;//互斥量
};

void *customer(void* arg)
{
	BolckQueue* c_q = (BolckQueue*)arg;
	while(1)
	{
		int data;
		data = c_q->Pop();
		cout << "取出的数据是:" << data << endl;
	}
	return NULL;
}
void *productor(void* arg)
{
	BolckQueue* p_q = (BolckQueue*)arg;
	int data = 0;
	while(1)
	{
		p_q->Push(data++);
		cout << "输入数据:" << data << endl;
	}
	return NULL;
}

#define NUM 4
int main()
{
	int time = 0;
	BolckQueue q_test;
	pthread_t c_tid[NUM];
	pthread_t p_tid[NUM];
	while(time < NUM)
	{
		if(pthread_create(&p_tid[time],NULL,productor,(void*)&q_test) != 0 )
		{
			cout << "create error" << endl;
			return -1;
		}
		++time;
	}
	time = 0;
	while(time < NUM)
	{
		if(pthread_create(&c_tid[time],NULL,customer,(void*)&q_test) != 0 )
		{
			cout << "create error" << endl;
			return -1;
		}
		++time;
	}
	time = 0;
	while(time < NUM)
	{
		pthread_join(c_tid[time],NULL);//每个线程我们都需要等待,防止僵尸线程
		++time;
	}
	time = 0;
	while(time < NUM)
	{
		pthread_join(p_tid[time],NULL);
		++time;
	}
	return 0;
}
  • posix标准信号量

  • 概念

信号量的本质是具有等待队列的计数器,主要用于是按进程/线程的同步,当然也可以实现互斥(互斥时信号量只能是0和1)

  • 信号量原理
  1. 初始一个资源计数
  2.  当获取资源时,先判断资源数
  3.          若 >0 ,表示有资源,计数 -1,直接返回,获取资源
  4.          若<=0,表四没有资源,则阻塞等待
  5. 当资源使用完毕,计数+1,唤醒等待队列上的进程/线程
  • 接口介绍

初始化:int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:信号量
pshared: 0表⽰示线程间共享,非零表⽰示进程间共享    
value:信号量初始值

销毁信号量:int sem_destroy(sem_t *sem);
 sem:信号量

等待信号量: int sem_wait(sem_t *sem);
会将信号量的值减1,若小于0则等待,大于0就使用。

唤醒信号量:int sem_post(sem_t *sem);

表⽰示资源使⽤用完毕。将信号量值加1。

  • 信号量实现生产者消费者

下面根据信号量的同步和互斥功能来实现生产者和消费者模型

#include<iostream>
#include<vector>
#include<pthread.h>
#include<unistd.h>
#include<semaphore.h>
using namespace std;
class RingQueue
{
	public:
	//构造函数
	RingQueue()
	:capacity_(10)
	,queue_(10)
	,cus_step_(0)
	,pro_step_(0)
	{
		sem_init(&free_space_,0,capacity_);
		sem_init(&data_space_,0,0);
		sem_init(&mutex_,0,1);//初始化1,1表示有资源,0表示没有资源。就实现了互斥
	}
	~RingQueue()
	{
		sem_destroy(&free_space_);
		sem_destroy(&data_space_);
		sem_destroy(&mutex_);
	}
	void Push(int val)
	{
		ProductorWait();//是否有空间进行生产
		QueueBlock();//队列加锁
		queue_[pro_step_] = val;//添加数据
		pro_step_ = (pro_step_ + 1) % capacity_;
		QueueUnblock();
		CustomerWake();
	}
	int Pop()
	{
		CustomerWait();
		QueueBlock();//队列加锁
		int data = queue_[cus_step_];
		cus_step_ = (cus_step_ + 1) %capacity_;
		QueueUnblock();
		ProductorWake();
		return data;
	}
	private:
	//私有成员接口
	void QueueBlock()
	{
		//pthread_mutex_lock(&mutex_);
		sem_wait(&mutex_);
	}
	void QueueUnblock()
	{
		//pthread_mutex_unlock(&mutex_);
		sem_post(&mutex_);
	}
	void CustomerWait()
	{
		sem_wait(&data_space_);
	}
	void CustomerWake()
	{
		sem_post(&data_space_);
	}
	void ProductorWait()
	{
		sem_wait(&free_space_);
	}
	void ProductorWake()
	{
		sem_post(&free_space_);
	}
	//成员变量
	vector<int> queue_;
	int capacity_;
	int cus_step_;
	int pro_step_;
	sem_t mutex_;
	sem_t free_space_;
	sem_t data_space_;
};
void* customer(void* arg)
{
	RingQueue* c_q = (RingQueue*)arg;
	while(1)
	{
		sleep(1);
		int data = c_q->Pop();
		cout << "取出的数据是:" << data << endl;
	}
	return NULL;
}
void* productor(void* arg)
{
	RingQueue* p_q = (RingQueue*)arg;
	int data = 0;
	while(1)
	{
		sleep(1);
		p_q->Push(data++);
		cout << "+++++输入的数据是:" << data << endl;
	}
	return NULL;
}

#define NUM 5
int main()
{
	int time = 0;
	RingQueue q_test;
	pthread_t c_tid[NUM];
	pthread_t p_tid[NUM];
	while(time < NUM)
	{
		if(pthread_create(&c_tid[time],NULL,customer,(void*)&q_test) != 0)
		{
			cout << "create error" << endl;
		}
		++time;
	}
	time = 0;
	while(time < NUM)
	{
		if(pthread_create(&p_tid[time],NULL,productor,(void*)&q_test) != 0)
		{
			cout << "create error" << endl;
		}
		++time;
	}
	time = 0;
	while(time < NUM)
	{
		pthread_join(c_tid[time],NULL);
		++time;
	}
	time = 0;
	while(time < NUM)
	{
		pthread_join(p_tid[time],NULL);
		++time;
	}
	return 0;
}

线程池

  • 概念

线程池就是一堆线程(一定数量),这里会有一个任务队列,当有任务的时候,会有线程去给任务队列添加任务,处理任务时,线程从任务队列取任务。简单理解就是一堆线程,一个生产者消费者模型。

  • 作用

在双十一的时候,淘宝一瞬间会有很多的请求链接,分分钟百万上下,这个时候如果服务器直接创建一百万个线程,服务器可能就凉了。

  1. 避免大量线程被创建和销毁
  2. 避免显示一时间创建太多,资源耗尽,程序崩溃
  3. 解耦合
  4. 支持忙闲不均,支持并发
  • 实现
  1. 最大数量限制----线程数量
  2. 当前线程池的线程数量
  3. 线程安全的任务队列
  4. 消费者生产者
#include<iostream>
#include<queue>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<semaphore.h>
using namespace std;

typedef bool (*mysolution)(int data);//定义一个函数指针,
//返回值是布尔,参数类型是T
class Task
{
public:
	Task()
	{}
	void SetTask(int& data,mysolution func)
	{
		data_ = data;
		func_ = func;
	}
	~Task()
	{}
	//处理函数
	bool Run()
	{	
		func_(data_);
		return true;
	}

private:
	int data_;//要处理的数据
	mysolution func_;//要处理的函数
};
#define THRMAX 5	//线程的最大数,队列容量也进行10个容量
class Threadpool
{
public:
	//队成员进行初始化
	Threadpool(int thrMax = THRMAX)
	:thrMax_(5)
	,thrcur_(5)
	,capacity_(10)
	{
		quiet_flag = false;
		pthread_mutex_init(&mutex,NULL);
		pthread_cond_init(&cond_con,NULL);
		pthread_cond_init(&cond_pro,NULL);
	}
	~Threadpool()
	{
		pthread_cond_destroy(&cond_con);
		pthread_cond_destroy(&cond_pro);
		pthread_mutex_destroy(&mutex);
	}
	//线程入口函数,传入的是线程池类的对象地址
	static void* ThrStart(void* arg)//这里之所以静态,是由于类成员函数有一个this指针
	{						//而线程创建里的函数只能有一个参数
		Threadpool* tp = (Threadpool*)arg;//获取对象
		while(1)
		{
			tp->QueueBlock();//对任务队列进行加锁
			while(tp->QueueIsEmpty())
			{
				tp->CustomerWait();//如果队列没有任务,消费者线程进行等待
			}
			Task task;
			tp->Pop(task);
			tp->ProductorWake();//唤醒生产者线程
			tp->QueueUnblock();//任务队列进行解锁
			task.Run();//处理数据,这个处理必须在解锁之后,不然一直处理数据,
				//别的线程无法再获取数据
		}
		return NULL;
	}
	//线程初始化,线程初始化不应该放在构造里
	//1、启动太慢
	//2、未知错误
	bool ThreadInit()	
	{
		pthread_t tid;
		for(int i = 0; i < 5; ++i)
		{//这里不需要数组,因为在后面不会用到tid所以,后面分离之后就不需要了
			int res = pthread_create(&tid,NULL,ThrStart,(void*)this);
			if(res != 0)
			{
				cout << "CREATE ERROR" << endl;
				return false;
			}
			pthread_detach(tid);//分离之后不用等待
		}
		return true;
	}
	//向任务队列添加数据
	bool PushTask(Task& task)
	{
		QueueBlock();//对任务队列进行加锁
		if(quiet_flag == true)
		{
			QueueUnblock();
			return false;
		}
		while(QueueIsFull())
		{
			ProductorWait();//生产者线程判断队列是否满了
		}
		Push(task);//添加任务
		CustomerWake();
		QueueUnblock();
		return true;
	}
	void ThreadQuiet()
	{
		QueueBlock();
		quiet_flag = true;
		QueueUnblock();
		while(thrcur_ > 0)
		{
			CustomerWakeAll();
			usleep(1000);
		}
	}
//成员接口
private:
	void QueueBlock()
	{ 
	     pthread_mutex_lock(&mutex);
	}
	void QueueUnblock()
	{
		pthread_mutex_unlock(&mutex);
	}
	void CustomerWait()
	{
		if(quiet_flag == true)
		{
			--thrcur_;
			pthread_mutex_unlock(&mutex);
			cout << pthread_self() << "已经退出"<<endl;
			pthread_exit(NULL);
		}
	    pthread_cond_wait(&cond_con,&mutex);
    }
	void CustomerWake()
	{
		pthread_cond_signal(&cond_con);
	}
	void CustomerWakeAll()
	{
		pthread_cond_broadcast(&cond_con);
	}
	void ProductorWait()
	{
		pthread_cond_wait(&cond_pro,&mutex);
	}
	void ProductorWake()
	{
		pthread_cond_signal(&cond_pro);
	}
	bool QueueIsEmpty()
	{
		return queue_.empty();
	}
	bool QueueIsFull()
	{
		return (queue_.size() == capacity_);
	}
	void Push(Task& task)
	{
		queue_.push(task);
	}
	void Pop(Task& task)
	{
		task = queue_.front();
		queue_.pop();
	}
//成员变量,用信号量来实现线程安全	
private:
	int thrMax_;//最大线程数量
	int thrcur_;//当前线程数量
	int capacity_;//队列容量	
	queue<Task> queue_;//任务队列
	
	bool quiet_flag;//线程退出的标志
	
	pthread_mutex_t mutex;//互斥量
	pthread_cond_t cond_con;//信号量实现同步
	pthread_cond_t cond_pro;
};
bool TaskHandler(int data)
{
	cout << "处理线程是:" << pthread_self() << endl;
	cout << "处理数据是:" << data << endl;
	sleep(1);
	return true;
}
#define TASKNUM 10
int main()
{
	Threadpool testpool;//创建一个线程池对象
	testpool.ThreadInit();//线程池初始化,10个线程,一个安全的任务队列
	Task task[TASKNUM];//创建10个任务对象
	for(int i = 0; i < TASKNUM; ++i)
	{
		task[i].SetTask(i,TaskHandler);
		testpool.PushTask(task[i]);
	}
	testpool.ThreadQuiet();
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值