多线程和线程同步

news2024/12/28 17:47:06

文章目录

  • 进程和线程
  • 线程的操作
    • 线程创建
    • 线程退出
    • 线程回收
    • 线程分离
    • 线程取消和ID比较
  • 线程同步
    • 互斥锁
    • 死锁
    • 读写锁
    • 条件变量
    • 信号量

进程和线程

线程是轻量级的进程,在Linux环境下线程的本质还是进程。
在计算机上运行的程序是一组指令及指令参数的组合,指令按照既定的逻辑控制计算机的运行。
操作系统以进程为单位分配资源,进程是操作系统资源分配的最小单位,进程是操作系统调度执行的最小单位
进程和线程的区别:
1.进程有自己独立的地址空间,多个线程共用同一个地址空间。
在这里插入图片描述
线程更加节省系统资源,而且效率更高。在同一个地址空间中,每个线程独享的资源有:自己的栈区和寄存器,多个线程共享的资源有:代码段、堆区、全局数据区、打开的文件等。
2.线程是程序的最小执行单位,进程是操作系统中最小的资源分配单位。
每个进程对应一个虚拟的地址空间,一个进程只能抢一个CPU时间片。一个地址空间中可以划分出多个线程,在有效的资源基础上,能够抢更多的CPU时间片。多线程中抢占时间片是随机的,并不是按照顺序进行的。
3.关于CPU的调度和切换,线程的上下文切换要比进程快得多。
上下文切换指的是进程或者线程在分时复用CPU时间片时,在切换之前会将上一个任务的状态进行保存,下次切换回这个任务的时候,从寄存器中加载这个状态继续运行。任务从保存到再次加载的这个过程就是一次上下文切换。
4.线程更加廉价,启动速度更快,退出也快,对系统资源的冲击小。
在处理多任务程序的时候,使用多线程比使用多进程要有优势,但线程并不是越多越好。在进行文件I/O操作的时候,其对CPU的使用率不高,此时线程等于2倍的CPU核心数时效率最高。而在处理复杂算法的时候,线程的个数等于CPU核心数时,效率是最高的。
5.一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉,所以多进程要比多线程健壮。


线程的操作

线程创建

线程的创建使用的是pthread_create函数,该函数的原型如下。

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

thread用来保存新创建的线程ID;attr为线程属性,设置为NULL表示所有属性设置为默认值;新创建的线程从start_routine()函数开始运行;arg是传递给start_routine()函数的参数。线程创建成功函数就返回0,创建失败将返回一个错误号。
含有pthread_create函数时,在编译的时候需要加上-lpthread,使用-l选项指定链接库pthread,因为pthread不在gcc的默认链接库中,所以需要手动指定。

线程退出

多线程程序中,如果想要让线程退出,但是不会导致虚拟地址空间的释放,就可以调用线程库中的线程退出函数pthread_exit(),该函数原型如下。

void pthread_exit(void *retval);  //retval是线程退出时携带的数据,当前子线程的主线程会得到该数据,不需要使用时就设置为NULL。

调用该函数后,当前线程马上就退出了,但是不会影响到其他线程的正常运行,其在主线程和子线程中都可以使用。
下面是一个线程创建的例子。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void* ThreadFun(void *arg)
{
	for(int i=0;i<5;i++)
		printf("子线程:i=%d ",i);
	printf("子线程:%ld\n",pthread_self());
	if(arg != NULL)
		printf("arg = %s\n",(char*)arg);   //参数不为空就打印
	return NULL;
}
int main()
{
	char *c = "abcdefg";
	pthread_t tid;
	pthread_create(&tid,NULL,ThreadFun,NULL);   //不传参
	for(int i=0;i<5;i++)
		printf("主线程:i=%d ",i);
	printf("主线程:%ld\n",pthread_self());
	pthread_create(&tid,NULL,ThreadFun,(void*)c);   //传参
	printf("主线程2:%ld\n",pthread_self());
	pthread_exit(NULL);
	//sleep(1);    //保证主线程等到子线程执行完毕后再退出
	return 0;
}

将上面程序编译后运行,结果如下图所示。
在这里插入图片描述
可以看到,同一个程序执行两次的结果不一样,这是因为时间片的抢占是随机的。
如果程序中既不加sleep()函数,也不加pthread_exit()函数,那么第二个子线程可能还没来得及执行,主线程的地址空间就被释放了,具体的执行结果如下图所示。
在这里插入图片描述

线程回收

线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函数是pthread_join(),该函数的原型如下。

int pthread_join(pthread_t thread, void **retval);

thread是要回收的子线程的ID,retval是一个二级指针,其指向一级指针的地址,该地址中存储的是线程退出函数pthread_exit()传递出来的数据,如果不需要该参数,将该参数设置为NULL。线程回收成功返回0,回收失败则返回一个错误号。
pthread_join()函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出,函数解除阻塞进行资源的回收,函数调用一次只能回收一个子线程,如果有多个子线程则需要循环进行回收。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

typedef struct
{
	int age;
	char* name;
}Test;

Test t;   //1.全局变量

void* ThreadFun(void *arg)
{
	printf("子线程:%ld\n",pthread_self());
	//static Test t;   //2.静态变量
	t.age = 10;
	t.name = "zhangsan";
	pthread_exit(&t);   //t是结构体变量,此时应当取其地址
	return NULL;
}

int main()
{
	pthread_t tid;
	pthread_create(&tid,NULL,ThreadFun,NULL); 
	printf("主线程:%ld\n",pthread_self());
	void *p;
	pthread_join(tid,&p);
	Test *pt = (Test *)p;
	printf("age = %d  name = %s\n",pt->age,pt->name);
	return 0;
}

函数体中定义的临时变量是存放在栈区的,因此在定义结构体变量的时候应该定义为全局变量或者静态变量,这样就能确保得到的数据是正确的。
在这里插入图片描述
上面的实现过程还有一种方法,将结构体变量定义在主函数中,然后将变量的地址通过pthread_create()的第四个参数进行传递,然后在函数体中将该类型进行强制转换,赋值后通过pthread_exit()函数退出子线程,主线程中通过pthread_join()函数接收,第二个参数设置为空。
具体的代码如下。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

typedef struct
{
	int age;
	char* name;
}Test;

void* ThreadFun(void *arg)
{
	printf("子线程:%ld\n",pthread_self());
	Test *t = (Test *)arg;
	t->age = 10;
	t->name = "zhangsan";
	pthread_exit(t);   //t是指针变量,直接传
	return NULL;
}

int main()
{
	Test t;
	pthread_t tid;
	pthread_create(&tid,NULL,ThreadFun,&t);   //将结构体变量的地址放在第四个参数位置
	printf("主线程:%ld\n",pthread_self());
	pthread_join(tid,NULL);
	printf("age = %d  name = %s\n",t.age,t.name);
	return 0;
}

线程分离

程序中的主线程一般都有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,使用函数pthread_join(),但是如果子线程不退出,主线程就会一直被阻塞,主线程中的任务就不能被执行了。
线程库函数中提供了线程分离函数pthread_detach()函数,该函数原型如下。

int pthread_detach(pthread_t thread);    //thread是要分离的线程ID

调用这个函数后,指定的子线程就会和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用pthread_join()函数就回收不到子线程的资源了。
在上面程序的基础上,在主函数中加入线程分离函数pthread_detach()。

int main()
{
	Test t;
	pthread_t tid;
	pthread_create(&tid,NULL,ThreadFun,&t); 
	printf("主线程:%ld\n",pthread_self());
	pthread_detach(tid);
	pthread_join(tid,NULL);
	printf("age = %d  name = %s\n",t.age,t.name);
	pthread_exit(NULL);
	return 0;
}

再次运行程序,可以看到,主线程就回收不到子线程的资源了。
在这里插入图片描述

线程取消和ID比较

线程取消指的是在某些特定的情况下,在一个线程中杀死另一个线程,使用的函数是pthread_cancel(),该函数的原型如下。

int pthread_cancel(pthread_t thread);  //thread是要杀死的线程ID

杀死一个线程需要分两步:第一步是在线程A中调用线程取消函数,指定要杀死的线程B;第二步是在线程B中进行一次系统调用,即从用户区切换到内核区。这两步都完成,线程B才能被取消。
线程ID的比较使用的函数是pthread_equal()函数,该函数的原型如下。

int pthread_equal(pthread_t t1,pthread_t t2);  //相等返回非0值,不相等返回0

在Linux中,线程ID是一个无符号的长整型,因此可以直接使用比较操作符比较两个线程的ID,但是线程库是可以跨平台使用的,在有些平台上可能不是一个单纯的整型,这种情况下比较两个线程的ID就需要用到线程比较函数。


线程同步

创建两个线程访问全局变量,代码如下。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define MAX 50

int number = 0;   //全局变量,共享资源
void* CountNum1(void *arg)
{
	for(int i=0;i<MAX;i++)
	{
		int cur = number;
		cur++;
		usleep(10);
		number = cur;
		printf("Thread A, id = %lu, number = %d\n",pthread_self(),number);
	}
	return NULL;
}

void* CountNum2(void *arg)
{
	for(int i=0;i<MAX;i++)
	{
		int cur = number;
		cur++;
		number = cur;
		printf("Thread B, id = %lu, number = %d\n",pthread_self(),number);
		usleep(5);
	}
	return NULL;
}

int main()
{
	pthread_t tid_a,tid_b;
	pthread_create(&tid_a,NULL,CountNum1,NULL); 
	pthread_create(&tid_b,NULL,CountNum2,NULL); 
	pthread_join(tid_a,NULL);
	pthread_join(tid_b,NULL);
	return 0;
}

上面代码运行后,其中部分运行结果如下图所示。
在这里插入图片描述
可以看到线程A和线程B在访问全局变量的时候,会有重复,这是因为线程在抢占到了CPU时间片后会从物理内存中读取数据,物理内存和CPU寄存器之间一般设有三级缓存。
在这里插入图片描述
上一个线程在失去时间片之后,还没来得及将最新一次的数据写回物理内存,这样切换到另一个线程计数时,就会发生计数重复。
对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步,常用的线程同步方式有:互斥锁、读写锁、条件变量、信号量。共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也称为临界资源。

互斥锁

互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块,被锁定的代码块只能被锁定它的线程解锁。所有的线程只能顺序执行,不能并行处理,这样多线程访问共享资源数据混乱的问题就可以被解决了,付出的代价是执行效率的降低,因为默认临界区多个线程是可以并行处理的,现在只能串行处理。
互斥锁的类型为pthread_mutex_t,在创建的锁对象中保存了当前锁的状态信息,锁定或者打开,如果是锁定状态,其中还记录了给这把锁加锁的线程ID。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥变量加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程才能被解除阻塞。一般情况下,每一个共享资源对应一把互斥锁,锁的个数和线程的个数无关。
互斥锁的操作函数原型如下。

pthread_mutex_t mutex;
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);  //初始化互斥锁,attr一般设置为NULL
//restrict是一个关键字,用来修饰指针,只有这个关键字修饰的指针可以访问指向的内存地址
int pthread_mutex_destroy(pthread_mutex_t *mutex);  //释放互斥锁资源
int pthread_mutex_lock(pthread_mutex_t *mutex);  //上锁

pthread_mutex_lock()函数被调用,首先会判断参数mutex互斥锁中的状态是不是锁定状态,如果没有被锁定,当前线程就可以加锁成功,而且这个锁种会记录是哪个线程加锁成功了。如果被锁定了,其他线程加锁就失败了,这些线程都会阻塞在这把锁上。当这把锁被解开之后,这些阻塞在锁上的线程就解除阻塞了,并且这些线程是通过竞争的方式对这把锁加锁的,没抢到锁的线程会继续阻塞。

int pthread_mutex_trylock(pthread_mutex_t *mutex);  //尝试加锁

调用pthread_mutex_trylock()函数对互斥锁加锁有两种情况,锁是打开的就加锁成功,如果锁变量是被锁住的,调用这个函数加锁的线程不会被阻塞,而是直接返回错误信号。

int pthread_mutex_unlock(pthread_mutex_t *mutex);  //解锁

互斥锁的应用例子如下。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define MAX 50

int number = 0;   //全局变量,共享资源
pthread_mutex_t mutex;   //创建互斥锁变量
void* CountNum1(void *arg)
{
	for(int i=0;i<MAX;i++)
	{
		pthread_mutex_lock(&mutex);   //加互斥锁
		//临界区开始
		int cur = number;
		cur++;
		usleep(10);
		number = cur;
		printf("Thread A, id = %lu, number = %d\n",pthread_self(),number);
		//临界区结束
		pthread_mutex_unlock(&mutex);   //解锁
	}
	return NULL;
}

void* CountNum2(void *arg)
{
	for(int i=0;i<MAX;i++)
	{
		pthread_mutex_lock(&mutex);   //加互斥锁
		//临界区开始
		int cur = number;
		cur++;
		number = cur;
		printf("Thread B, id = %lu, number = %d\n",pthread_self(),number);
		//临界区结束
		pthread_mutex_unlock(&mutex);   //解锁
		usleep(5);
	}
	return NULL;
}

int main()
{
	pthread_t tid_a,tid_b;
	pthread_mutex_init(&mutex,NULL);   //在创建子线程之前进行互斥锁的初始化
	pthread_create(&tid_a,NULL,CountNum1,NULL); 
	pthread_create(&tid_b,NULL,CountNum2,NULL); 
	pthread_join(tid_a,NULL);
	pthread_join(tid_b,NULL);
	pthread_mutex_destroy(&mutex);   //销毁互斥锁
	return 0;
}

上面程序编译后的运行结果如下图所示。
在这里插入图片描述
可以看到,这个时候全局变量不再重复了,是逐个增加变化的。

死锁

当多个线程访问共享资源时,就需要加锁,如果锁使用不当,就会造成死锁现象,后果是所有的线程都被阻塞,并且线程的阻塞是无法解开的,因为可以解锁的线程也被阻塞了。
造成死锁的原因:加锁之后忘记解锁;程序虽然有解锁代码,但是在解锁之前return了;重复加锁造成死锁;两个函数中分别有加锁和解锁代码,但是在一个函数的临界区调用了另一个函数;有两个临界资源,两个线程各自对一个资源上锁,但是该资源中要访问另一个资源。
死锁产生之后,程序就会阻塞,只能强制退出,运行结果如下图所示。
在这里插入图片描述
避免死锁的方法:检查代码,防止多次上锁;对共享资源访问完毕后一定要解锁,或者加锁的时候使用trylock()函数,这样即使有线程阻塞,尝试加锁的线程也不会被阻塞;程序中有多把锁的情况下,如果条件允许,可以控制对锁的访问顺序,在对其他互斥锁加锁之前,先释放当前线程的互斥锁;可以引入一些专门用于死锁检测的模块。

读写锁

读写锁是互斥锁的升级版,在做读操作的时候可以提高程序的执行效率,如果所有的线程都是做读操作,那么读是并行的,但是使用互斥锁,读操作也是串行的。
读写锁是一把锁,既可以锁定读操作,也可以锁定写操作,如果使用读写锁锁定了读操作,那么需要先解锁才能去锁定写操作,反之亦然。读写锁的类型为pthread_rwlock_t,锁种记录了锁的状态(锁定/打开)、锁定的操作(读操作/写操作)、锁定该锁的线程。
读写锁的特点
1、使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的;
2、使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的,此时和互斥锁一样;
3、使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问者两个临界区,访问写锁临界区的线程继续运行,而访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高
如果程序中所有的线程都对共享资源做写操作,使用读写锁和互斥锁是一样的,没有体现优势;如果程序中的线程对共享资源有写也有读操作,并且对共享资源读的操作多,那么读写锁相比于互斥锁更有优势。
读写锁的相关函数原型如下。

pthread_rwlock_t rwlock;
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);    //初始化读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);     //释放读写锁的系统资源
//rwlock是读写锁的地址,attr是读写锁的属性,一般设置为NULL

读锁操作的相关函数原型如下。

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);    //加读锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);    //尝试加读锁

调用pthread_rwlock_rdlock()函数加读锁,如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;如果读写锁已经锁定了写操作,调用这个函数的线程就会被阻塞。调用pthread_rwlock_tryrdlock()函数加读锁时,如果碰到读写锁已经锁定写操作的情况,加锁失败,但是该线程不会被阻塞。
写锁操作的相关函数原型如下。

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);    //加写锁
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);    //尝试加写锁

调用pthread_rwlock_wrlock()函数加写锁,如果读写锁是打开的,那么加锁成功;如果读写锁已经锁定了读操作或写操作,调用这个函数的线程就会被阻塞。调用pthread_rwlock_trywrlock()函数加写锁时,如果碰到读写锁已经锁定写操作的情况,加锁失败,但是该线程不会被阻塞。
不管是读锁还是写锁,解锁的函数都是一样的,解锁的函数原型如下。

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);    //解锁

读写锁使用案例:8个线程同时操作同一个全局变量,3个线程不定时的写这个全局变量,5个线程不定时的读这个全局变量。
该读写锁案例的代码如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#define MAX 50

int number = 0;   //全局变量,共享资源
pthread_rwlock_t rwlock;   //创建读写锁变量
void* read_num(void *arg)
{
	for(int i=0;i<MAX;i++)
	{
		pthread_rwlock_rdlock(&rwlock);   //加读锁
		//临界区开始
		printf("Read number, thread_id = %lu, number = %d\n",pthread_self(),number);
		//临界区结束
		pthread_rwlock_unlock(&rwlock);   //解锁
		usleep(5);
	}
	return NULL;
}

void* write_num(void *arg)
{
	for(int i=0;i<MAX;i++)
	{
		pthread_rwlock_wrlock(&rwlock);   //加写锁
		//临界区开始
		int cur = number;
		cur++;
		number = cur;
		printf("Write number, thread_id = %lu, number = %d\n",pthread_self(),number);
		//临界区结束
		pthread_rwlock_unlock(&rwlock);   //解锁
		usleep(rand()%5);
	}
	return NULL;
}

int main()
{
	pthread_t tid_rd[5],tid_wr[3];
	pthread_rwlock_init(&rwlock,NULL);   //在创建子线程之前进行读写锁的初始化
	for(int i=0;i<3;i++)
	{
		pthread_create(&tid_wr[i],NULL,write_num,NULL);   //写线程创建
	}
	for(int i=0;i<5;i++)
	{
		pthread_create(&tid_rd[i],NULL,read_num,NULL);    //读线程创建
	}
	for(int i=0;i<5;i++)
	{
		pthread_join(tid_rd[i],NULL);   //读线程回收
	}
	for(int i=0;i<3;i++)
	{
		pthread_join(tid_wr[i],NULL);   //写线程回收
	}
	pthread_rwlock_destroy(&rwlock);   //销毁读写锁
	return 0;
}

上面程序编译后的执行结果如下图所示。
在这里插入图片描述
有三个不同的线程在写数,有五个不同的线程在读数。

条件变量

条件变量的主要作用不是用来处理线程同步的,而是进行线程阻塞的。如果在多线程程序中只使用条件变量无法实现线程的同步,必须要配合互斥锁来使用。
条件变量和互斥锁都能阻塞线程,但是二者的效果是不一样的。假设有A-Z这样的26个线程,这些线程共同访问同一把互斥锁,如果线程A加锁成功,那么其余B-Z线程访问互斥锁都阻塞,所有的线程只能顺序访问临界区。条件变量只有在满足指定条件时才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,这种情况下会出现共享资源中数据的混乱。
条件变量用于处理类似于生产者消费者模型,并且配合着互斥锁来使用。条件变量的类型为pthread_cond_t,被条件变量阻塞的线程信息会被记录在pthread_cond_t类型的变量中,其在解除阻塞的时候会使用。
条件变量相关的操作函数原型如下。

pthread_cond_t cond;
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);   //初始化
//cond是条件变量的地址,attr是条件变量的属性,一般设置为空
int pthread_cond_destroy(pthread_cond_t *cond);   //销毁
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);   //线程阻塞函数,线程调用该函数就会被阻塞

pthread_cond_wait()函数原型在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁的功能是进行线程同步,让线程顺序进入临界区,避免出现共享资源的数据混乱。使用该函数阻塞线程时候,如果线程已经对互斥锁mutex上锁,那么这把锁会打开,这样做是为了避免死锁;当线程解除阻塞的时候,函数内部会帮这个线程将mutex互斥锁锁上,然后继续向下访问临界区。

struct timespec
{
	time_t tv_sec;   //秒
	long tv_nsec;   //纳秒
};   //结构体中记录的时间是从1971.01.01开始到某个时间点的时间

int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);   //abstime表示阻塞时长

pthread_cond_timedwait()函数的作用是将线程阻塞一定的时间,阻塞到时后,线程就解除阻塞了。

time_t cur_time = time(NULL);
struct timespec tmsp;
tmsp.tv_nsec = 0;
tmsp.tv_sec = cur_time + 100;    //表示线程阻塞100秒

唤醒函数原型如下。

int pthread_cond_signal(pthread_cond_t *cond);    //至少唤醒一个被阻塞的线程
int pthread_cond_broadcast(pthread_cond_t *cond);  //唤醒所有被阻塞的线程

条件变量案例:生产者和消费者模型,5个生产者和5个消费者,生产者往链表头部添加节点,消费者删除链表头部节点。
该条件变量的实现的代码如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

struct Node   //定义链表
{
	int number;
	struct Node *next;
}; 
struct Node *head = NULL;  //链表头结点
int max_cap = 5;
pthread_mutex_t mutex;   //创建互斥锁变量
pthread_cond_t cond_p,cond_c;   //创建条件变量
void* producer(void *arg)
{
	while(1)
	{
		pthread_mutex_lock(&mutex);
		//临界区开始
		while(max_cap == 0)
		{
			pthread_cond_wait(&cond_p,&mutex);   //链表满了,阻塞生产者
			printf("生产已满,阻塞的生产者ID : %ld\n",pthread_self());
		}
		struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));   //创建新节点
		newNode->number = rand() % 100;   //初始化新节点的属性
		newNode->next = head;   
		head = newNode;    //移动头结点的位置
		printf("producer id : %ld, number = %d\n",pthread_self(),newNode->number);
		max_cap--;
		//临界区结束
		pthread_mutex_unlock(&mutex);
		pthread_cond_signal(&cond_c);    //生产出产品,唤醒消费者
		//sleep(1);   //休眠
	}
	return NULL;
}

void* consumer(void *arg)
{
	while(1)
	{
		pthread_mutex_lock(&mutex);
		//临界区开始
		while(head == NULL)
		{
			pthread_cond_wait(&cond_c,&mutex);   //链表为空,阻塞消费者
			printf("链表为空,阻塞的消费者ID : %ld\n",pthread_self());
		}
		struct Node *node = head;
		printf("consumer id : %ld, number = %d\n",pthread_self(),node->number);
		head = head->next;   //链表头结点后移
		free(node);  //释放取出来的头结点
		max_cap++;
		//临界区结束
		pthread_mutex_unlock(&mutex);
		pthread_cond_signal(&cond_p);    //消费了产品,唤醒生产者
		//sleep(3); 
	}
	return NULL;
}

int main()
{
	pthread_t tid_p[5],tid_c[5];
	pthread_mutex_init(&mutex,NULL);  //初始化互斥锁
	pthread_cond_init(&cond_p,NULL);   //初始化条件变量
	pthread_cond_init(&cond_c,NULL);   //初始化条件变量
	for(int i=0;i<5;i++)
	{
		pthread_create(&tid_p[i],NULL,producer,NULL);  //创建生产者线程
	}
	for(int i=0;i<5;i++)
	{
		pthread_create(&tid_c[i],NULL,consumer,NULL);   //创建消费者线程
	}
	for(int i=0;i<5;i++)
	{
		pthread_join(tid_p[i],NULL);   //线程回收
		pthread_join(tid_c[i],NULL);
	}	
	pthread_mutex_destroy(&mutex);   //销毁互斥锁
	pthread_cond_destroy(&cond_p);   //销毁条件变量
	pthread_cond_destroy(&cond_c);   //销毁条件变量
	return 0;
}

上面程序编译后的执行结果如下图所示。
在这里插入图片描述
需要注意的是,线程阻塞后会将互斥锁打开,否则生产者没办法接着生产,就会产生死锁。只有阻塞的线程将互斥锁打开,生产者才能继续生产然后唤醒消费者,消费者线程被唤醒后会再次加锁向下执行。

信号量

信号量用在多线程多任务同步,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不 一定是锁定某一个资源,而是流程上的概念,比如有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步 骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。
信号量(信号灯)与互斥锁和条件变量的主要不同在于”灯”的概念,灯亮则意味着资源可用,灯灭则意味着不可用。信号量主要 阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。
使用信号量需要用到信号量的头文件,信号的类型为sem_t类型。

#include <semaphore.h>
sem_t sem;

信号量操作函数的原型如下。

int sem_init(sem_t *sem,int pshared,unsigned int value);   //初始化信号量
int sem_destroy(sem_t *sem);   //销毁

其中,sem是信号量变量的地址;pshared如果设置为0表示线程同步,非0则表示进程同步;value用于初始化当前信号量拥有的资源数,是一个大于等于0的数,如果为0,线程就会被阻塞。

int sem_wait(sem_t *sem);    //调用该函数一次,资源数量-1,资源减至0时,线程被阻塞
int sem_trywait(sem_t *sem);    //资源为0时,线程不会被阻塞
int sem_post(sem_t *sem);   //调用该函数一次,资源数量+1
int sem_getvalue(sem_t *sem,int *sval);  //查看信号量中整型数的当前值,值存放在sval指针对应的内存中

生产者在给自己的资源数量做减1操作后,要给消费者的资源数加1,同理,消费者在给自己的资源数做减1操作后,也要给生产者的资源数加1。
使用信号量且只有一个资源可用的情况下,可以不使用互斥锁,相应的代码如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>   //信号量头文件

struct Node   //定义链表
{
	int number;
	struct Node *next;
}; 
struct Node *head = NULL;  //链表头结点
sem_t sem_p,sem_c;   //创建信号量变量

void* producer(void *arg)
{
	while(1)
	{
		sem_wait(&sem_p);   //生产者资源减1
		struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));   //创建新节点
		newNode->number = rand() % 100;   //初始化新节点的属性
		newNode->next = head;   
		head = newNode;    //移动头结点的位置
		printf("producer id : %ld, number = %d\n",pthread_self(),newNode->number);
		sem_post(&sem_c);   //消费者资源加1
		sleep(rand()%3);    //休眠
	}
	return NULL;
}

void* consumer(void *arg)
{
	while(1)
	{
		sem_wait(&sem_c);   //消费者资源减1
		struct Node *node = head;
		printf("consumer id : %ld, number = %d\n",pthread_self(),node->number);
		head = head->next;   //链表头结点后移
		free(node);  //释放取出来的头结点
		sem_post(&sem_p);   //生产者资源加1
		sleep(rand()%3); 
	}
	return NULL;
}

int main()
{
	sem_init(&sem_p,0,1);   //生产者信号量初始化,只有一个资源
	sem_init(&sem_c,0,0);   //消费者信号量初始化
	pthread_t tid_p[5],tid_c[5];
	for(int i=0;i<5;i++)
	{
		pthread_create(&tid_p[i],NULL,producer,NULL);  //创建生产者线程
	}
	for(int i=0;i<5;i++)
	{
		pthread_create(&tid_c[i],NULL,consumer,NULL);   //创建消费者线程
	}
	for(int i=0;i<5;i++)
	{
		pthread_join(tid_p[i],NULL);   //线程回收
		pthread_join(tid_c[i],NULL);
	}	
	sem_destroy(&sem_c);
	sem_destroy(&sem_p);
	return 0;
}

上面程序编译后的执行结果如下图所示。
在这里插入图片描述
可以看到,输出结果严格按照生产一个、消费一个的顺序进行着。
使用信号量但有一个以上资源可用的情况下,需要使用互斥锁,相应的代码如下。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>   //信号量头文件

struct Node   //定义链表
{
	int number;
	struct Node *next;
}; 
struct Node *head = NULL;  //链表头结点
sem_t sem_p,sem_c;   //创建信号量变量
pthread_mutex_t mutex;
void* producer(void *arg)
{
	while(1)
	{
		sem_wait(&sem_p);   //生产者资源减1
		pthread_mutex_lock(&mutex);   //互斥锁加锁一定在信号量之后
		struct Node *newNode = (struct Node*)malloc(sizeof(struct Node));   //创建新节点
		newNode->number = rand() % 100;   //初始化新节点的属性
		newNode->next = head;   
		head = newNode;    //移动头结点的位置
		printf("producer id : %ld, number = %d\n",pthread_self(),newNode->number);
		pthread_mutex_unlock(&mutex);   //解锁的顺序可以随意
		sem_post(&sem_c);   //消费者资源加1
		sleep(rand()%3);    //休眠
	}
	return NULL;
}

void* consumer(void *arg)
{
	while(1)
	{
		sem_wait(&sem_c);   //消费者资源减1
		pthread_mutex_lock(&mutex);   //互斥锁加锁一定在信号量之后
		struct Node *node = head;
		printf("consumer id : %ld, number = %d\n",pthread_self(),node->number);
		head = head->next;   //链表头结点后移
		free(node);  //释放取出来的头结点
		pthread_mutex_unlock(&mutex);   //解锁的顺序可以随意
		sem_post(&sem_p);   //生产者资源加1
		sleep(rand()%3); 
	}
	return NULL;
}

int main()
{
	sem_init(&sem_p,0,5);   //生产者信号量初始化,设置5个资源
	sem_init(&sem_c,0,0);   //消费者信号量初始化
	pthread_mutex_init(&mutex,NULL);
	pthread_t tid_p[5],tid_c[5];
	for(int i=0;i<5;i++)
	{
		pthread_create(&tid_p[i],NULL,producer,NULL);  //创建生产者线程
	}
	for(int i=0;i<5;i++)
	{
		pthread_create(&tid_c[i],NULL,consumer,NULL);   //创建消费者线程
	}
	for(int i=0;i<5;i++)
	{
		pthread_join(tid_p[i],NULL);   //线程回收
		pthread_join(tid_c[i],NULL);
	}	
	pthread_mutex_destroy(&mutex);
	sem_destroy(&sem_c);
	sem_destroy(&sem_p);
	return 0;
}

上面程序编译后的执行结果如下图所示。
在这里插入图片描述
当把资源的数量设置为5以后,不再是生产者生产一个,消费者消费一个了,生产者有可能连续生产,消费者也有可能连续消费。
特别需要强调的是,互斥锁加锁一定要在信号量的sem_wait()之后,这样才能避免死锁。
如果生产者生产已满,且生产者抢到时间片先执行,这个时候如果互斥加锁在前,那么执行了该操作后,在下一步的sem_wait()已不能继续向下执行,因为生产已满!这个时候再转到消费者,由于生产者没有对互斥锁解锁,因此消费者也不能继续向下执行,产生死锁。同理,如果消费者已经全部消费完产品,且抢到时间片先执行,同样会发生死锁。因此,互斥加锁的操作必须在后。


本文参考视频:
多线程和线程同步-C/C++

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1540930.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Web前端-JS

JavaScript&#xff0c;简称js&#xff1a;负责网页的行为&#xff08;交互效果&#xff09;。是一门跨平台&#xff0c;面向对象的脚本语言&#xff08;编写出来的语言不需要编译&#xff0c;通过浏览器的解释就可以运行&#xff09; JS引入方式 1.内嵌样式 这样打开页面就会…

毕业答辩PPT模板涵盖多种风格,包括母版的设计及主题色的设计

毕业答辩PPT模板涵盖多种风格&#xff0c;包括母版的设计及主题色的设计 前言一两个页面的展示研究内容主题概述主题内容一&#xff1a;主要面向三点研究内容主题内容二&#xff1a;主要面向两点研究内容主题内容三&#xff1a;主要面向包含应用开发的研究 前言 之前做了有关开…

Oracle Data Guard部署

Oracle的主备DG搭建 1. 修改主机名,同步时间 主库IP&#xff1a;192.168.100.137 备库IP&#xff1a;192.168.100.138配置主机名(主库) Hostname zygjpdb vim /etc/hosts 192.168.100.137 zygjpdb 192.168.100.138 zygjsdbvim /etc/sysconfig/network HOSTNAMEzygjpdb ------…

电脑如何关闭自启动应用?cmd一招解决问题

很多小伙伴说电脑刚开机就卡的和定格动画似的&#xff0c;cmd一招解决问题&#xff1a; CtrlR打开cmd,输入&#xff1a;msconfig 进入到这个界面&#xff1a; 点击启动&#xff1a; 打开任务管理器&#xff0c;禁用不要的自启动应用就ok了

机器学习算法那些事 | 使用Transformer模型进行时间序列预测实战

本文来源公众号“机器学习算法那些事”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;使用Transformer模型进行时间序列预测实战 时间序列预测是一个经久不衰的主题&#xff0c;受自然语言处理领域的成功启发&#xff0c;transfo…

C语言分支和循环

目录 一.分支 一.if 二.if else 三.if else嵌套 四.else if 五.switch语句 二.循环 一.while (do while&#xff09;break : 二.for函数&#xff1a; 三.goto语句: 四.猜数字: 一.分支 一.if if要条件为真才执行为假不执行而且if只能执行后面第一条如果要执行多条就…

Java基础之关键字instanceof(七)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

【C语言】linux内核pci_iomap

一、pci_iomap /** pci_iomap 是一个用于映射 PCI 设备的 BAR&#xff08;Base Address Register&#xff0c;基地址寄存器&#xff09;的函数。* 此函数返回指向内存映射 IO 的指针&#xff0c;用于直接访问 PCI 设备的内存或 I/O 空间。* * 参数:* dev - 指向pci_dev结构的指…

Android Jetpack Compose基础之组件的帧渲染

Android Jetpack Compose基础之组件的帧渲染 组合布局LayoutModifier示例 LayoutCompsable示例 绘制CanvasDrawModifierDrawModifier-drawWithContent示例 DrawModifier-drawBehind源码示例 DrawModifier-drawWithCache源码示例 拓展Modifier.graphicsLayer Android View 系统&…

0基础 三个月掌握C语言(13)-下

数据在内存中的存储 浮点数在内存中的存储 常见的浮点数&#xff1a;3.141592、1E10等 浮点数家族包括&#xff1a;float、double、long double类型 浮点数表示的范围&#xff1a;在float.h中定义 练习 关于&#xff08;float*)&n&#xff1a; &n&#xff1a;这是一…

基于SSM的宠物领养平台的设计与实现

基于SSM的宠物领养平台的设计与实现 获取源码——》公主号&#xff1a;计算机专业毕设大全 获取源码——》公主号&#xff1a;计算机专业毕设大全

Three.js 中的 OrbitControls 是一个用于控制相机围绕目标旋转以及缩放、平移等操作的控制器。

demo案例 Three.js 中的 OrbitControls 是一个用于控制相机围绕目标旋转以及缩放、平移等操作的控制器。下面是它的详细讲解&#xff1a; 构造函数: OrbitControls(object: Camera, domElement?: HTMLElement)object&#xff1a;THREE.Camera 实例&#xff0c;控制器将围绕…

LibFuzzer 基本使用

文章目录 前言环境搭建基础使用编写 fuzz target编译链接demo 测试 && 输出日志分析心脏滴血漏洞测试 提高代码覆盖率和测试速度指定种子语料库多核并行 Fuzz使用字典 参考 前言 相较于 AFL 来说&#xff0c;LibFuzzer 在单个进程内完成模糊测试&#xff0c;以此来避免…

Nacos部署(一)Linux部署Nacos2.3.x单机环境

&#x1f60a; 作者&#xff1a; 一恍过去 &#x1f496; 主页&#xff1a; https://blog.csdn.net/zhuocailing3390 &#x1f38a; 社区&#xff1a; Java技术栈交流 &#x1f389; 主题&#xff1a; Nacos部署&#xff08;一&#xff09;Linux部署Nacos2.3.x单机环境 ⏱️…

【NC20313】仪仗队

题目 仪仗队 欧拉函数&#xff0c;找规律 思路 这好像是一道非常简单的找规律问题&#xff0c;所以你从 1 1 1 开始枚举&#xff0c;计算出当 N i Ni Ni 时的结果 a n s i ans_i ansi​&#xff0c;所以你得出了以下结果&#xff1a; Nans10233549513621725837 令人失望…

招聘自媒体编辑岗位的人才测评方案

人才测评工具在招聘入职的方案&#xff0c;在线工具网根据自媒体岗位的特性和需求来分析&#xff0c;并制定自媒体主编的测评方案。 自媒体作为互联网时代的产物&#xff0c;自然也为我们带来了很多的福利&#xff0c;例如&#xff1a;海量的信息、快捷的传媒方式&#xff0c;那…

学习次模函数-第2章 定义

纵观本专著&#xff0c;我们认为及其幂集&#xff08;即&#xff0c; 所有子集的集合&#xff09;&#xff0c;其基数为。我们也考虑一个实值集函数&#xff0c;使得。 与凸函数的一般约定相反&#xff08;见附录A&#xff09;&#xff0c;我们不允许函数有无穷大的值。 次模分…

一文搞懂数据链路层

数据链路层 1. 简介2. MAC3. 以太网 1. 简介 &#xff08;1&#xff09;概念 链路(link)是一条无源的点到点的物理线路段&#xff0c;中间没有任何其他的交换结点。 数据链路(data link) 除了物理线路&#xff08;双绞线电缆、同轴电缆、光线等介质&#xff09;外&#xff0…

Java获取方法参数名称方案||SpringBoot配置顺序注解

一: Java获取方法参数名称的方法 普盲: getDeclaredMethods与getMethods的的区别 1、getMethods返回一个包含某些 Method 对象的数组&#xff0c;这些对象反映此 Class 对象所表示的类或接口的公共 member 方法。 2、getDeclaredMethods返回 Method 对象的一个数组&#xff0c…

STM32+ESP8266水墨屏天气时钟:简易多级菜单(数组查表法)

项目背景 本次的水墨屏幕项目需要做一个多级菜单的显示&#xff0c;所以写出来一起学习&#xff0c;本篇文章不单单适合于水墨屏&#xff0c;像0.96OLED屏幕也适用&#xff0c;区别就是修改显示函数。 设计思路 多级菜单的实现&#xff0c;一般有两种实现的方法 1.通过双向…