线程(thread)及线程间同步(互斥锁、信号量、条件信号)

文章目录

    • 一 线程
      • (一) 线程的概念
        • 1.背景
        • 2.什么是线程
        • 3.线程与进程的比较
        • 4.进程与线程的区别
      • (二)线程的操作
        • 1.创建 pthread_create()
        • 2.退出 pthread_exit()
        • 3.等待 pthread_ join()
        • 4.获取ID pthread_ self()
    • 二 互斥锁
      • (一)互斥锁的概念
        • 1.临界区
        • 2.互斥锁
      • (二)互斥锁的操作
        • 1.创建 pthread_mutex_init()
        • 2.销毁 pthread_mutex_destroy()
        • 3.上锁 pthread_mutex_lock()
        • 4.解锁 pthread_mutex_unlock()
        • 5.示例
    • 三 信号量
      • (一)信号量的概念
      • (二)信号量的操作
        • 1.创建 sem_init()
        • 2.销毁 sem_destroy()
        • 3.P操作 sem_wait()
        • 4.V操作 sem_post()
    • 四 条件信号
      • (一)条件信号的概念
      • (二)条件信号的操作
        • 1.创建 pthread_cond_init()
        • 2.销毁 pthread_cond_destroy()
        • 3.等待 pthread_cond_wait()
        • 4.发送 pthread_cond_signal()
        • 5.示例
    • 五 参考资料

一 线程

(一) 线程的概念

1.背景

  • 创建进程的过程会带来很大的系统开销。
  • 为了完成进程间的数据交换,需要特殊的 IPC(Inter Process Communication,进程间通信)技术。
  • 进程的上下文切换开销很大。(这是使用进程时的最大开销)

为了在一定程度上克服多进程的上述缺点,人们引入了线程(Thread)。这是为了将进程的各种缺点降至最低限度(不能直接消除)而设计出的一种“轻量级进程”(Light Weight Process,LWP)。

2.什么是线程

线程是进程的一个实体,是CPU调度和分派的基本单位。它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属于一个进程的其他的线程共享进程拥有的全部资源。

3.线程与进程的比较

每个进程的内存空间都由保存变量的 “数据区”、使用malloc等函数动态分配的堆区(Heap)、函数运行时使用的栈区(Stack)构成,每个进程都有这种独立的内存空间。但如果以获得多个代码执行流为主要目的,只需分离栈区域,通过这种方式可以获得如下优势:

  • 上下文切换时不需要切换数据区和堆区。
  • 可以利用数据区和堆区交换数据,无需特殊技术
  • 线程的创建和上下文切换比进程的创建和上下文切换速度更快。
  • 进程:在操作系统构成单独执行流的单位。
  • 线程:在进程构成单独执行流的单位。
  • 进程是操作系统进行资源分配和调度的基本单位,是程序执行的最小单位;而线程是CPU执行和调度的基本单位。

4.进程与线程的区别

进程和线程的主要区别在于它们是不同的操作系统资源管理方式。

  1. 进程有独立的地址空间。
  2. 线程有自己的栈和局部变量,但线程之间没有单独的地址空间,共享数据区和堆区。
  3. 线程的划分尺度小于进程,使得多线程程序的并发性高。
  4. 多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
  5. 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
  6. 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  7. 多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。

(二)线程的操作

1.创建 pthread_create()

不能使用perror函数处理错误信息。
处理错误信息函数:strerror(ret)

#include 
 
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
/*
功能:创建一个新的线程
参数:
			  pthread_t *thread:以出参的方式获得新创建线程的ID
	 const pthread_attr_t *attr:用于定制各种不同的线程属性,NULL为创建默认属性的线程
void *(*start_routine) (void *):在单独执行流中执行的函数地址值(函数指针)
					  void *arg:通过第三个参数传递调用函数时包含传递参数信息的变量地址值
返回值:
		成功:0
		失败:错误编号
*/

2.退出 pthread_exit()


#include 
 
void pthread_exit(void *retval);
/*
功能:线程主动退出函数
参数:
	void *retval:出时,需要传递给pthread_join的数据,不传数据用 NULL
返回值:
		成功:0
		失败:错误编号
*/
//作为保留
int pthread_cancel(pthread_t thread);//线程取消函数
int pthread_detach(pthread_t thread);//分离线程函数

3.等待 pthread_ join()


#include 

int pthread_join(pthread_t thread, void **retval);
/*
功能:等待一个线程的结束
参数:
	 pthread_t thread:需要等待的线程ID
		void **retval:指向 pthread_exit返回的数据
返回值:
		成功:0
		失败:错误编号
*/

4.获取ID pthread_ self()

pthread_t pthread_self (void)/*
功能:获取当前线程id
参数:
		无
返回值:
		线程ID 无符号长整型,输出格式为 "%lu"
*/

二 互斥锁

(一)互斥锁的概念

1.临界区

临界资源: 是指在同一个时刻只允许有限个任务可以访问或修改的资源,通常包括硬件资源(CPU、内存、外设等)和软件资源(共享代码段、共享结构、变量等)

临界区(Critical Section)
  在任意时刻只允许一个线程对共享资源进行访问的区域。这个临界区可能是代码块、或是共享内存。
  例如,多个线程同时访问同一个全局变量,如果都是读取操作,则不会出现问题。 如果一个线程负责改变此变量的值,而其他线程负责同时读取变量的值,则不能保证读取到的数据是经过写线程修改后的。为了确保读线程读取到的是经过修改后的值,就必须在向全局变量写入数据时禁止其他线程对其的任何访问,直至赋值过程结束后再解除对其他线程的访问限制。
  从代码角度讲,全局变量就是共享资源,访问全局变量的语句或语句块就是临界区;从内存角度讲,全局变量所在内存空间中的数据就是共享资源,而内存空间的大小就是临界区。

2.互斥锁

当一个进程中存在两个及以上(一个进程本来就有一个线程)的线程时,线程间会互相争夺共享资源,导致单个线程中的执行秩序会被打乱。所以需要用到互斥量来进行秩序控制,保证单个线程中的程序先执行完毕。
互斥量也叫互斥锁,本人觉得互斥锁更加的贴切,因为互斥量是锁定共享资源(也可以是一段代码),等共享资源被处理完毕后(执行完一段代码),然后去解锁。而另一个线程必须等到这个锁解开了,才能执行自己的代码。注意互斥锁不能控制多线程的执行顺序,也就是我们加了锁之后,并不知道先执行那个线程,我们只能知道,程序执行到有互斥锁的线程后,会把被锁定的代码先执行完毕。

互斥锁的相关API都放在了 ,编译时需要加动态库 -lpthread

(二)互斥锁的操作

1.创建 pthread_mutex_init()

pthread_mutex_t mutex;//定义互斥锁的索引
//使用的时候,应该把 mutex定义为全局变量,这样所有的线程都能使用
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
/*
功能:创建一个互斥锁
参数:
		 	 pthread_mutex_t *mutex:互斥量的索引(名称)指针类型
	const pthread_mutexattr_t *attr:互斥量的属性,通常为 NULL
返回值:
		成功:返回0
		失败:返回错误编码
*/

2.销毁 pthread_mutex_destroy()

int pthread_mutex_destroy(pthread_mutex_t *mutex);
/*
功能:销毁一个互斥锁,归还自己的一切资源
参数:
		 pthread_mutex_t *mutex:互斥量的索引(名称)指针类型
返回值:
		成功:返回0
		失败:返回错误编码
所有线程执行完毕后销毁互斥锁
*/

3.上锁 pthread_mutex_lock()

 int pthread_mutex_lock(pthread_mutex_t *mutex);
 /*
功能:锁定某个临界区
参数:
		 pthread_mutex_t *mutex:互斥量的索引(名称)指针类型
返回值:
		成功:返回0
		失败:返回错误编码
*/

4.解锁 pthread_mutex_unlock()

 int pthread_mutex_unlock(pthread_mutex_t *mutex);
 /*
功能:解锁某个临界区
参数:
		 pthread_mutex_t *mutex:互斥量的索引(名称)指针类型
返回值:
		成功:返回0
		失败:返回错误编码
*/

5.示例

#include 
pthread_mutex_t mutex;//定义互斥锁的索引

void *Pthread_T1(void *arg)
{
	pthread_mutex_lock(&mutex);
	.......//执行一些代码
	pthread_mutex_unlock(&mutex);
	pthread_exit(NULL);
}
int main()
{
	int ret;
	pthread_t pthread_T1;
	int T1_ret=pthread_create(&pthread_T1,NULL,(void *)Pthread_T1,NULL);//创建一个子线程
	ret=pthread_mutex_init(&mutex,NULL);//创建互斥锁
	
	............
	pthread_mutex_destroy(&mutex);
	return 0;
}

三 信号量

(一)信号量的概念

线程的信号量与进程间通信中使用的信号量的概念是一样,它是一种特殊的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作。如果一个程序中有多个线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行。

互斥量与信号量的主要区别:

互斥量用于线程的互斥。
信号量用于线程的同步。

互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它 性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。

同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

  • 互斥量值只能为0/1。信号量值可以为非负整数。
  • 也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量时,也可以完成一个资源的互斥访问。
  • 互斥量的加锁和解锁必须由同一线程分别对应使用。信号量可以由一个线程释放,另一个线程得到。

(二)信号量的操作

1.创建 sem_init()

#include 
int sem_init(sem_t *sem,int pshared,unsigned int value);
 /*
功能:创建信号量
参数:
			sem_t *sem:指向信号量结构的一个指针,创建时应定义为全局变量
		   int pshared:
						 0:只能为当前进程的所有线程共享
		   			   非0:信号量在进程间共享
		unsigned int value:信号量的初始值
返回值:
		成功:返回0
		失败:返回错误编码
*/

2.销毁 sem_destroy()

int sem_destroy(sem_t *sem);
 /*
功能:销毁信号量,归还自己的一切资源
参数:
		sem_t *sem:sem_init函数初始化后的信号量结构体的指针
返回值:
		成功:返回0
		失败:返回错误编码
*/

3.P操作 sem_wait()

int sem_wait(sem_t *sem);
 /*
功能:使信号量的值减1,如果预计执行后的结果<0,则阻塞等待
参数:
		sem_t *sem:sem_init函数初始化后的信号量结构体的指针
返回值:
		成功:返回0
		失败:返回错误编码
*/

说明

  • sem_wait函数也是一个原子操作,它的作用是从信号量的值减去一个“1”,但它永远会先等待该信号量为一个非零值才开始做减法。也就是说,如果你对一个值为2的信号量调用sem_wait(),线程将会继续执行,介信号量的值将减到1。如果对一个值为0的信号量调用sem_wait(),这个函数就会地等待直到有其它线程增加了这个值使它不再是0为止。如果有两个线程都在sem_wait()中等待同一个信号量变成非零值,那么当它被第三个线程增加一个“1”时,等待线程中只有一个能够对信号量做减法并继续执行,另一个还将处于等待状态。
  • 信号量这种“只用一个函数就能原子化地测试和设置”的能力下正是它的价值所在。还有另外一个信号量函数sem_trywait,它是sem_wait的非阻塞搭档。

4.V操作 sem_post()

int sem_post(sem_t *sem); 
 /*
功能:使信号量的值加1
参数:
		sem_t *sem:sem_init函数初始化后的信号量结构体的指针
返回值:
		成功:返回0
		失败:返回错误编码
*/

说明
sem_post函数的作用是给信号量的值加上一个“1”,它是一个“原子操作”,即同时对同一个信号量做加“1”操作的两个线程是不会冲突的;而同时对同一个文件进行读、加和写操作的两个程序就有可能会引起冲突。信号量的值永远会正确地加一个“2”--因为有两个线程试图改变它。

四 条件信号

(一)条件信号的概念

  在进程里,如果没有条件信号去控制线程的话,我们无法确定进程会先执行哪个线程,但是使用条件信号的话,我们可以去控制线程的执行顺序。
  条件本身是由互斥量保护的。线程在改变条件状态前必须首先锁住互斥量,其他线程在获得互斥量之前不会察觉到这种改变,因为必须锁定互斥量以后才能计算条件。
  条件变量使用之前必须首先初始化,pthread_cond_t数据类型代表的条件变量可以用两种方式进行初始化,可以把常量PTHREAD_COND_INITIALIZER赋给静态分配的条件变量。

(二)条件信号的操作

1.创建 pthread_cond_init()

//使用的时候,应该把 *cond定义为全局变量,这样所有的线程都能使用
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
 /*
功能:创建条件信号
参数:
			  pthread_cond_t *cond:条件信号的索引(名称),是指针变量
	const pthread_condattr_t *attr:条件信号的属性,通常为 NULL
返回值:
		成功:返回0
		失败:返回错误编码
*/

2.销毁 pthread_cond_destroy()

int pthread_cond_destroy(pthread_cond_t *cond);
 /*
功能:销毁条件信号,释放占用的资源
参数:
	pthread_cond_t *cond:条件信号的索引(名称),使用pthread_cond_init()初始化后的变量
返回值:
		成功:返回0
		失败:返回错误编码
*/

3.等待 pthread_cond_wait()

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t * mutex);
 /*
功能:在指定互斥锁中等待条件信号
参数:
	pthread_cond_t *restrict cond:条件信号的索引,需要等待的条件信号
		  pthread_mutex_t * mutex:在mutex这个互斥锁里等待条件信号
返回值:
		成功:返回0
		失败:返回错误编码
*/

4.发送 pthread_cond_signal()

int pthread_cond_signal(pthread_cond_t *restrict cond,);
 /*
功能:发送条件信号
参数:
	pthread_cond_t *restrict cond:条件信号的索引,需要发送的条件信号
返回值:
		成功:返回0
		失败:返回错误编码
*/

5.示例

利用条件信号,先让T2线程执行,把shared_data加到20,然后T1进程把shared_data减到10:

#include 
#include 

int shared_data=0;
pthread_mutex_t mutex;
pthread_cond_t cond;//定义条件信号索引

void *Pthread_T1(void *arg)
{
		pthread_cond_wait(&cond,&mutex);//在mutex锁里等待信号cond
		printf("T1:\n");
		while(1){
			printf("T1:\n");
			shared_data--;
			printf("T1:shared_data=%d\n",shared_data);
			if(shared_data==10){
				pthread_exit(NULL);
			}
			sleep(1);
	}
}

void *Pthread_T2(void *arg)
{
		printf("T2:\n");
		pthread_mutex_lock(&mutex);
        while(1){        
				printf("T2:\n");
                shared_data+=2;
                printf("T2:shared_data=%d\n",shared_data);
                if(shared_data==20){
						pthread_cond_signal(&cond);//发送条件信号
						pthread_mutex_unlock(&mutex);
                        pthread_exit(NULL);
                }
				sleep(1);
        }
}

int main()
{	
	pthread_t T1;
	pthread_t T2;

	int ret1=pthread_create(&T1,NULL,(void *)Pthread_T1,NULL);
	int ret2=pthread_create(&T2,NULL,(void *)Pthread_T2,NULL);
	int mutex_ret=pthread_mutex_init(&mutex,NULL);//创建互斥锁
	int cond_ret=pthread_cond_init(&cond,NULL);	//创建条件信号

	int ret3=pthread_join(T1,NULL);
	int ret4=pthread_join(T2,NULL);
	
	pthread_mutex_destroy(&mutex);
	pthread_cond_destroy(&cond);//销毁条件信号
	return 0;
}

五 参考资料

高端IT培训 www.hqyj.com
https://blog.csdn.net/SupreV/article/details/78761365
https://blog.csdn.net/u010429831/article/details/122581134
https://blog.csdn.net/qq1140920745/article/details/110007124
https://blog.csdn.net/qq1140920745/article/details/110144556

你可能感兴趣的:(linux,嵌入式,c语言,c语言,linux,arm,单片机,物联网)