Linux-应用编程-学习笔记(19):线程全解

Linux-应用编程-学习笔记(19):线程全解

前言:线程是解决进程间通信的一个非常好的方法,它保留了进程技术实现多任务的特性,是内核调度的最小单元。多线程在多核心CPU上面更有优势。

一、线程引入

1. 用线程来解决进程的劣势

我们知道进程技术发明的主要目的是用来实现多任务系统需求(多任务的需求是客观的)。
进程技术的主要功能是为了实现CPU的分时复用,从而实现宏观上的并行。但是要知道进程之间的调度(切换)是需要耗费一定成本的。比如A进程执行到某一步去切换B进程,首先需要保存A进程此时的断点,然后去执行B进程,等到回来再恢复断点继续执行……这其中断点得保存与恢复都是要付出代价的。并且进程间的通信由于进程隔离的缘故,会比较麻烦。
因此我们可以通过线程的方式来实现进程这些劣势的弥补,线程技术不仅保留了进程技术实现多任务的特性,还在线程间切换和线程间通信上提升了效率,并且在多核心CPU上面更有优势

2. 线程原理简介

线程是参与内核调度的最小单元,一个进程中可以有多个线程(一个线程相当于一个分支任务),线程是依附于进程存在的(先得有了一个进程,才能有里面的线程),操作系统在调度时候调度的是进程里面的线程
Linux-应用编程-学习笔记(19):线程全解_第1张图片
线程可以像进程一样可被OS调度,继承了进程能够可以被单独调用的特性。
同一进程的多个线程之间很容易高效率通信(同一个进程中的多个线程就好像同一个文件中的多个函数一样),进程中线程与线程的通信就相当于函数与主函数之间的通信,可以使用全局变量即可。
在多核心CPU(对称多处理器架构SMP)架构下效率最大化。可以保证多线程里面的多个线程运行在不同的CPU核心上,实现一种并行同时运行的状态。

3. 线程常见函数

线程的创建与回收:
(1)pthread_create :主线程用来创造子线程的
(2)pthread_join:主线程用来等待(阻塞)回收子线程
(3)pthread_detach: 主线程用来分离子线程,分离后主线程不必再去回收子线程(子线程自己管自己)

线程的取消:
(1)pthread_cancel: 一般都是主线程调用该函数去取消(让它赶紧死)子线程
(2)pthread_setcancelstate :子线程设置自己是否允许被取消(如果设置宏为不可被取消,那么cancel不管用)
(3)pthread_setcanceltype:取消的时候的模式(允许取消时候才会生效)

线程函数的退出:
(1)pthread_exit:标准的子线程退出方式(子线程不能调用exit返回,因为你属于进程的一部分,调用exit直接把进程整个都退出了)
(2)pthread_cleanup_push:解决破坏锁的问题(具体分析见下面)
(3)pthread_cleanup_pop:接应push函数,将内部保存的信息从栈中取出来(0表示拿出来不执行,1表示拿出来执行)
Linux-应用编程-学习笔记(19):线程全解_第2张图片
解释一下上述过程:
首先如果线程正常运行没有被取消,那么cnt在进行线程操作之前被加1(上锁),线程运行结束后cnt减1(解锁),其中cnt的变化为0->1->0,解锁后的cnt可以供其他线程使用。
如果线程在没有结束时就被取消了,那么cnt没有经历减1(解锁),则其他线程将无法进入(锁坏掉)。所以我们可以使用pthread_cleanup_push函数,在线程执行前先将解锁函数(function)提前保存在栈中,如果线程被取消,那么能够通过栈中的解锁函数来保证锁不会坏掉。

二、线程同步方法

通过一个任务来实现线程间的同步。
任务:用户从终端输入任意字符然后统计个数显示,输入end则结束。

1. 使用信号量进行同步

线程同步就是主线程和子线程之间的配合。想要的效果是平时子线程时阻塞住的状态,主线程获取用户输入的字符,如果不等于“end”,那么就去激活子线程,将输入的内容打印出来。
使用信号量的方式就是通过定义一个cnt,使其为0,在线程执行前给cnt++(上锁),线程结束后给cnt–(解锁)。这里使用封装好的库函数即可。

sem_init:初始化未命名的信号量。
sem_post:发送解锁信号给指向的信号量。
sem_wait:锁定指向的信号量。
sem_destroy:销毁定义的信号量。

#include 
#include 
#include 
#include 
#include 

char buf[200] = {0};
//定义一个sem_t类型的变量
sem_t sem;
unsigned int flag = 0;

// 子线程程序,作用是统计buf中的字符个数并打印
void *func(void *arg)
{
	// 子线程首先应该有个循环
	// 循环中阻塞在等待主线程激活的时候,子线程被激活后就去获取buf中的字符长度,然后打印;完成后再次被阻塞
	//阻塞住,等到发送信号才执行
	sem_wait(&sem);
	while (flag == 0)
	{	
		printf("本次输入了%d个字符\n", strlen(buf));
		memset(buf, 0, sizeof(buf));
		sem_wait(&sem);
	}
	//退出线程
	pthread_exit(NULL);
}

int main(void)
{
	int ret = -1;
	pthread_t th = -1;
	//初始化一个信号量
	sem_init(&sem, 0, 0);
	//创建一个线程
	ret = pthread_create(&th, NULL, func, NULL);
	if (ret != 0)
	{
		printf("pthread_create error.\n");
		exit(-1);
	}
	
	printf("输入一个字符串,以回车结束\n");
	while (scanf("%s", buf))	//如果有输入内容进入循环
	{
		// 去比较用户输入的是不是end,如果是则退出,如果不是则继续		
		if (!strncmp(buf, "end", 3))
		{
			printf("程序结束\n");
			flag = 1;
			//发送信号
			sem_post(&sem);	
			break;
		}
		// 主线程在收到用户收入的字符串,并且确认不是end后就去发信号激活子线程来计数。
		// 子线程被阻塞,主线程可以激活,这就是线程的同步问题。
		// 信号量就可以用来实现这个线程同步
		sem_post(&sem);	
	}

	// 回收子线程
	printf("等待回收子线程\n");
	ret = pthread_join(th, NULL);
	if (ret != 0)
	{
		printf("pthread_join error.\n");
		exit(-1);
	}
	printf("子线程回收成功\n");
	//销毁信号量
	sem_destroy(&sem);
	
	return 0;
}

Linux-应用编程-学习笔记(19):线程全解_第3张图片

2. 使用互斥锁进行同步

互斥锁又称互斥量。互斥锁和信号量的关系:可以认为互斥锁是一种特殊的信号量。信号量可以一直加(能够实现排队),而互斥锁只能是0或者1。
互斥锁一般用来实现关键段的保护,流程一般为上锁->执行->解锁。

pthread_mutex_init:使用attr指定的属性初始化由互斥锁引用的互斥锁。
pthread_mutex_lock:用来锁定互斥锁。
pthread_mutex_unlock:解除互斥锁。
pthread_mutex_destroy:销毁互斥锁。

#include 
#include 
#include 
#include 

char buf[200] = {0};
//定义一个互斥锁的变量
pthread_mutex_t mutex;
unsigned int flag = 0;

// 子线程程序,作用是统计buf中的字符个数并打印
void *func(void *arg)
{
	// 子线程首先应该有个循环
	// 循环中阻塞在等待主线程激活的时候,子线程被激活后就去获取buf中的字符
	// 长度,然后打印;完成后再次被阻塞
	
	sleep(1);
	while (flag == 0)
	{	
		pthread_mutex_lock(&mutex);
		printf("本次输入了%d个字符\n", strlen(buf));
		memset(buf, 0, sizeof(buf));
		pthread_mutex_unlock(&mutex);
		sleep(1);
	}	
	pthread_exit(NULL);
}


int main(void)
{
	int ret = -1;
	pthread_t th = -1;
	//创建一个互斥锁
	pthread_mutex_init(&mutex, NULL);
	//创建一个子线程
	ret = pthread_create(&th, NULL, func, NULL);
	if (ret != 0)
	{
		printf("pthread_create error.\n");
		exit(-1);
	}
	
	printf("输入一个字符串,以回车结束\n");
	while (1)
	{
		//互斥锁上锁
		pthread_mutex_lock(&mutex);
		scanf("%s", buf);
		//互斥锁解锁
		pthread_mutex_unlock(&mutex);
		// 去比较用户输入的是不是end,如果是则退出,如果不是则继续		
		if (!strncmp(buf, "end", 3))
		{
			printf("程序结束\n");
			flag = 1;

			break;
		}
		//这里sleep的作用是确保主子线程不会抢锁
		sleep(1);
	}

	
	// 回收子线程
	printf("等待回收子线程\n");
	ret = pthread_join(th, NULL);
	if (ret != 0)
	{
		printf("pthread_join error.\n");
		exit(-1);
	}
	printf("子线程回收成功\n");
	
	pthread_mutex_destroy(&mutex);
	
	return 0;
}

这里只是一个简单的互斥锁使用示例,正常情况下互斥锁用来保护一些关键的位置,这些位置有着明显的先后顺序,所以不至于用sleep来控制抢锁。

3. 使用条件变量进行同步

条件变量是线程同步的一种特有的方法,也是效率比较高的一种方法。当线程不满足条件时,就一直等待这个条件(阻塞),另一个提供条件的线程当满足发送条件时,就会给这个线程发送一个条件从而激活这个阻塞住的线程,被激活之后就会继续运行。
条件变量在使用时可以与互斥锁配合使用

pthread_cond_init:初始化cond所引用的一个条件变量。
pthread_cond_destroy:销毁被引用的条件变量。
pthread_cond_wait:该函数应在条件变量上阻塞。 它们应使用被调用线程锁定的互斥锁或未定义的行为结果来调用。
pthread_cond_signal/pthread_cond_broadcast:这些函数应解除阻塞在条件变量上阻塞的线程。

#include 
#include 
#include 
#include 

char buf[200] = {0};
//定义互斥锁变量
pthread_mutex_t mutex;
//定义条件变量
pthread_cond_t cond;
unsigned int flag = 0;

// 子线程程序,作用是统计buf中的字符个数并打印
void *func(void *arg)
{
	// 子线程首先应该有个循环
	// 循环中阻塞在等待主线程激活的时候,子线程被激活后就去获取buf中的字符
	// 长度,然后打印;完成后再次被阻塞
	
	while (flag == 0)
	{	
		//上锁
		pthread_mutex_lock(&mutex);
		//等待条件变量满足才能继续执行
		pthread_cond_wait(&cond, &mutex);
		printf("本次输入了%d个字符\n", strlen(buf));
		memset(buf, 0, sizeof(buf));
		//解锁
		pthread_mutex_unlock(&mutex);
	}
	//退出子线程
	pthread_exit(NULL);
}

int main(void)
{
	int ret = -1;
	pthread_t th = -1;
	//初始化一个互斥锁
	pthread_mutex_init(&mutex, NULL);
	//初始化一个条件变量
	pthread_cond_init(&cond, NULL);
	//创建一个子线程
	ret = pthread_create(&th, NULL, func, NULL);
	if (ret != 0)
	{
		printf("pthread_create error.\n");
		exit(-1);
	}
	
	printf("输入一个字符串,以回车结束\n");
	while (1)
	{
		scanf("%s", buf);
		//发送条件变量满足信号
		pthread_cond_signal(&cond);
		// 去比较用户输入的是不是end,如果是则退出,如果不是则继续		
		if (!strncmp(buf, "end", 3))
		{
			printf("程序结束\n");
			flag = 1;
			
			break;
		}
		// 主线程在收到用户收入的字符串,并且确认不是end后
		// 就去发信号激活子线程来计数。
		// 子线程被阻塞,主线程可以激活,这就是线程的同步问题。
		// 信号量就可以用来实现这个线程同步
	}
	// 回收子线程
	printf("等待回收子线程\n");
	ret = pthread_join(th, NULL);
	if (ret != 0)
	{
		printf("pthread_join error.\n");
		exit(-1);
	}
	printf("子线程回收成功\n");
	//销毁互斥锁
	pthread_mutex_destroy(&mutex);
	//销毁条件变量
	pthread_cond_destroy(&cond);
	return 0;
}

Linux-应用编程-学习笔记(19):线程全解_第4张图片

你可能感兴趣的:(Linux嵌入式)