线程处理和线程同步


前言

本文梳理了有关于线程、线程处理和线程同步的一些知识点


一、线程

1.线程简介

线程指轻量级执行单元

(1)进程(Process)是正在执行的程序的实例。每个进程都有自己的地址空间、代码段、数据段和打开的文件描述符等资源。线程(Thread)是进程内的一个执行单元,它共享相同的地址空间和其他资源,包括文件描述符、信号处理等,但每个线程都有自己的栈空间。

(2)由于共享地址空间和数据段,同一进程的多线程之间进行数据交换比进程间通信方便很多,但也由此带来线程同步问题。

(3)同一进程的多线程共享大部分资源,除了每个线程独立的栈空间。这代表线程的创建、销毁、切换要比进程的创建、销毁、切换的资源消耗小很多,所以多线程比多进程更适合高并发

二、线程处理

1.线程的创建

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

线程号,设置线程属性(一般直接使用NUL,默认属性),一个指向函数的指针,定义了
新线程开始执行的入口必须接收一个void *类型参数 返回void *类型对象,前面函数的参数

成功返回0 失败返回非0

流程:

① 声明线程号pthread_t pid
② 准备好线程处理函数 void *out_thread(void *arg)
③ pthread_create(&pid,NULL,out_thread,NULL)创建线程
④ 主线程挂起等待子线程结束pthread_join(pid,NULL);
⑤ 释放内存 全局变量定义的内存 是共享的

create.c

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

#define BUF_LEN 1024

char *buf;	

//读线程需要执行的代码逻辑
void *input_thread(void *argv)
{
	int i = 0;
	while(1)
	{
		char c = fgetc(stdin);
		if(c && c != '\n')
		{
			buf[i++] = c;
		}

		//缓冲区索引溢出
		if(i >= BUF_LEN)
		{
			i = 0;
		}
	}
}

//写线程执行逻辑 
void *out_thread(void *argv)
{
	int i = 0;
	while(1)
	{
		if(buf[i])
		{
			fputc(buf[i],stdout);
			fputc('\n',stdout);
			buf[i++] = 0;

			if(i >= BUF_LEN)
			{
				i = 0;
			}
		}
		else
		{
			//如果当前还没有写入数据
			sleep(1);
		}
	}
}


//创建两个线程
// 1 读线程 读取控制台信息 写入到缓存
// 2 写线程 将缓存信息写入控制台
int main(int argc , char *argv[])
{
	//初始化buf
	buf = malloc(BUF_LEN);

	//声明线程 id     pthread_t == int 
	pthread_t pid_input;
	pthread_t pid_output;
	
	//创建读线程    线程描述符     属性      线程处理函数      线程传递的参数			
	pthread_create(&pid_input,NULL,input_thread,NULL);

	//创建写线程
	pthread_create(&pid_output,NULL,out_thread,NULL);

	//主线程等待读写线程结束
	pthread_join(pid_input,NULL);
	pthread_join(pid_output,NULL);

	//释放内存
	free(buf);
	return 0;
}

2.线程终止

线程终止方法:
① 线程函数执行return 语句
② 线程函数内部调用pthread_exit() 主动退出
③ 其他线程调用pthread_cancel()函数 其他线程请求放弃

1.pthread_exit(void *retval) , 调用后线程关闭,线程可以通过retval向其他线程传递信息,retval指向的区域不可以放在线程函数的栈内,其他线程如果需要这个返回值,需要调用pthread_join,本质上就是一个共享的变量

2.pthread_join(pthread_t , void *retval) 第一个参数是线程id,第二个参数用于接收线程结束后传递的返回值,一般设置为NULL

3.pthread_detach(pthread_t pid):用来标记不用回收,即不调用join不等待,系统会自动回收
如果join和detach都不调用 线程会一直占用资源,默认是join需要主线程回收,调用detach主线程就不用等,系统会自动回收

4.pthread_cancel(pthread_t pid) : 向目标线程发送取消请求,目标线程是否和何时响应取决于它的取消状态和取消类型

5.pthread_setcancelstate (int state, int *oldstate):设置线程取消状态
6.int pthread_setcanceltype(int type, int *oldtype); 设置线程取消类型

设置线程取消 主线程也要join 或者 detach

3.线程终止exit案例

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

//定义结构体接收线程结果   相当于用于跟主线程之间通信
typedef struct Result{
	char *p;
	int len;
}Result;

//如果一个进程的多个线程同时使用到标准的输入输出 会造成冲突问题 导致只有一个线程使用到标准的输入输出


//红玫瑰执行代码的函数
void *red_thread(void * arg)
{
	//初始化Result结构体 在堆区才可以传递所以要用malloc
	Result *result = malloc(sizeof(Result));
	//解析传递的参数
	char code = *((char*) arg);
	//声明存放读取消息的字符串
	char *ans = malloc(101);
	while(1)
	{
		fgets(ans,100,stdin);  读取标准输入
		if(ans[0] == code)
		{
			//接到了回复的消息
			free(ans);   
			char *redans = strdup("红玫瑰独自去了纽约\n");
			result->p = redans;
			result->len = strlen(redans);
			
			//结束线程 返回故事结果
			pthread_exit((void*)result);
		}
		else 
		{
			printf("红玫瑰还在等你\n");
		}
	}
	
}

//白玫瑰执行代码的函数
void *white_thread(void * arg)
{
	//初始化Result结构体 在堆区才可以传递所以要用malloc
	Result *result = malloc(sizeof(Result));
	//解析传递的参数
	char code = *((char*) arg);
	//声明存放读取消息的字符串
	char *ans = malloc(101);
	while(1)
	{
		fgets(ans,100,stdin);
		if(ans[0] == code)
		{
			//接到了回复的消息
			free(ans);
			char *redans = strdup("白玫瑰独自去了巴黎\n");
			result->p = redans;
			result->len = strlen(redans);
			
			//结束线程 返回故事结果
			pthread_exit((void*)result);
		}
		else 
		{
			printf("白玫瑰还在等你\n");
		}
	}
}


int main(int argc,char *argv[])
{

	
	//创建两个线程 -> 红玫瑰 白玫瑰
	pthread_t pid_red;
	pthread_t pid_white;
	
	char red_code = 'r';
	char white_code = 'w';

	Result *red_result = NULL;
	Result *white_result = NULL;

	pthread_create(&pid_red,NULL,red_thread,&red_code);

	pthread_create(&pid_white,NULL,white_thread,&white_code);

	//获取红玫瑰线程结果        
	pthread_join(pid_red,(void **)&red_result); 
	printf("红玫瑰故事的结局%d是:%s\n",red_result->len,red_result->p);

	//释放内存 
	free(red_result->p);    释放的其实是result->p指向的char *
	free(red_result);

	pthread_join(pid_white,(void **)&white_result); 
	printf("白玫瑰故事的结局%d是:%s\n",white_result->len,white_result->p);

	free(white_result->p);
	free(white_result);
	return 0;
}

4.detach不挂起终止案例

不等待线程终止 但也下达一个结束后回收相关线程的指令
真正使用的效果是不接收子线程返回的状态
主线程完成比创建的子线程慢

不会像join一样挂起等待

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

void *task(void *asg)
{
	printf("thread started\n");
	sleep(2);
	printf("thread finished\n");

	pthread_exit(NULL);
}

int main(int argc , char *argv[])
{
 	//创建线程
 	pthread_t tid;
 	pthread_create(&tid,NULL,task,NULL);

	//使用detach标记会等待线程完成之后回收相关资源
	pthread_detach(tid);

	//主线程运行完成比创建的子线程慢
	printf("main thread continus\n");

	sleep(3); //因为主线程不会在pthread_detach处等待 如果父线程先结束 会强制杀死子线程

	printf("man thread ending\n");
	
	return 0;
}

5.cancel的延迟取消案例

主线程主动的去取消创建的子线程

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

void *task(void *asg)
{
	printf("thread started\n");
	//默认取消类型是延迟
	printf("working...\n");
	sleep(1);

	//人为调用取消点函数 就是延迟到这个函数为止
	pthread_testcancel();
	printf("after canceled\n");

	pthread_exit(NULL);
}

int main(int argc , char *argv[])
{
 	//创建线程
 	pthread_t tid;
 	pthread_create(&tid,NULL,task,NULL);

	//取消子线程 只是发出一个停止的命令 也不等待
	if(pthread_cancel(tid) != 0)
	{
		perror("pthread_cancel");
	}

	void *res;

	//join会挂起 一直等待子线程结束             被取消/结束 通过res来判断
	pthread_join(tid,&res);

	if(res == PTHREAD_CANCELED)
	{
		printf("线程被取消\n");
	}
	else 
	{
		printf("线程还没有被取消 exit code %d",*(int*)res);
	}
	
	return 0;
}

6.cancel的异步取消和禁用取消案例

异步取消 发出取消信号的那一瞬间取消 而不再是等到人为调用取消点 函数

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

void *task(void *asg)
{
	printf("thread started\n");
	//默认取消类型是延迟							旧的方式的存储位置(旧的方式会填到写的地址值上)
	pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL);

	
	
	printf("working...\n");
	int i = 0;
	while(1)
	{
		printf("%d\n",i++);
	}
	printf("after canceled\n");
	pthread_exit(NULL);
}

int main(int argc , char *argv[])
{
 	//创建线程
 	pthread_t tid;
 	pthread_create(&tid,NULL,task,NULL);

	//取消子线程 只是发出一个停止的命令 也不等待
	if(pthread_cancel(tid) != 0)
	{
		perror("pthread_cancel");
	}

	void *res;

	//join会挂起 一直等待子线程结束             被取消/结束 通过res来判断
	pthread_join(tid,&res);

	if(res == PTHREAD_CANCELED)
	{
		printf("线程被取消\n");
	}
	else 
	{
		printf("线程还没有被取消 exit code %ld"long)res);
	}
	
	return 0;
}

禁用取消响应

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

void *task(void *asg)
{
	printf("thread started\n");
	//禁用取消响应
	pthread_setcancelstate(PTHREAD_CANCEL_DISABLE,NULL);
	//默认取消类型是延迟							旧的方式的存储位置(旧的方式会填到写的地址值上)
	pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS,NULL);

	
	
	printf("working...\n");

	sleep(2);
	pthread_exit(NULL);
}

int main(int argc , char *argv[])
{
 	//创建线程
 	pthread_t tid;
 	pthread_create(&tid,NULL,task,NULL);

	//取消子线程 只是发出一个停止的命令 也不等待
	if(pthread_cancel(tid) != 0)
	{
		perror("pthread_cancel");
	}

	void *res;

	//join会挂起 一直等待子线程结束             被取消/结束 通过res来判断
	pthread_join(tid,&res);

	if(res == PTHREAD_CANCELED)
	{
		printf("线程被取消\n");
	}
	else 
	{
		printf("线程还没有被取消 exit code %ld",(long)res);
	}
	
	return 0;
}

三、线程同步

1.竞态条件和锁

当多个线程并发访问和修改同一个共享资源时,如果没有适当的同步措施,就会遇到线程同步问题(白话说就是执行顺序问题),这种情况下程序的最终结果依赖于线程具体的执行时序,导致了竞态条件

竞态条件(race condition)是一种特定的线程同步问题指的是两个或者以上进程或者线程并发执行时,其最终的结果依赖于进程或者线程执行的精确时序。它会导致程序的行为和输出超出预期,因为共享资源的最终状态取决于线程执行的顺序和时机。为了确保程序执行结果的正确性和预期一致,需要通过适当的线程同步机制来避免竞态条件

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

#define THREAD_COUNT 20000

void *add_task(void *arg)
{
	int *p = arg;
	(*p)++;
	pthread_exit(NULL);
}

int main(int argc , char *argv[])
{
	
	int num = 0;
	pthread_t pid[THREAD_COUNT];
	for(int i = 0; i < THREAD_COUNT; i++)
	{
		pthread_create(pid + i,NULL,add_task,&num);
	}

	for(int i = 0; i < THREAD_COUNT; i++)
	{
		pthread_join(pid[i],NULL);
	}
	
	//打印累加结果
	printf("%d",num);
	return 0;
}

可以看到20000个线程对num的累加结果是不确定的,没有达到我们的预期值20000。这是因为线程之间出现了竞争,不同线程对于num的累加操作可能重叠,这就会导致多次累加操作可能只生效一次。

避免竞态条件:
① 避免多线程写入一个地址
② 加锁
{
常见锁机制:
互斥锁:保证同一时间只有一个线程可以执行临界区代码(白话就是只有一个线程先执行,其他线程要等)
读写锁:允许同时读,但写是互斥的,写与写互斥 写与读互斥
自旋锁:在获取锁之前,线程在循环中忙等待,适用于锁持有时间非常短的场景,一般是Linux内核使用
}

2.互斥锁

pthread_mutex_t
一种联合体类型的别名

用来控制对共享资源的访问,任何时刻只有一个线程可以持有锁,如果其他线程尝试获取一个已经被持有的锁,那线程将会被阻塞,直到锁被释放

流程:
① 创建锁并初始化pthread_mutex_init()

static pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

用于静态初始化互斥锁(mutex)。这个宏为互斥锁提供了一个初始状态,使其准备好被
锁定和解锁,而不需要在程序运行时显式调用初始化函数,一般就直接这样初始化而不是
用初始化函数,当我们使用PTHREAD_MUTEX_INITIALIZER初始化互斥锁时,实际上是
将互斥锁设置为默认属性和未锁定状态。这种初始化方式适用于简单的同步问题,我们可以通过以下代码初始化互斥锁。
static pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

② 获取锁pthread_mutex_lock(pthread_mutex_t *mutex)
③ 释放锁pthrea_mutex_unlock(pthread_mutex_t *mutex)
④ 用完销毁锁资源pthread_mutex_destory()
如果没有进行显示的销毁锁资源通常不会引起资源泄露问题。上述程序在所有线程执行完毕后直接结束,进程结束时,操作系统会回收该进程的所有资源,包括内存、打开的文件描述符和互斥锁等。

但是如果用了初始化函数,锁就是动态分配的就需要显式的销毁

自己实现互斥锁需要对原子进行操作 实现不了

将mutex加入我们刚刚的程序
为了保证计算结果的正确性,很显然,我们应阻塞式获取互斥锁,应调用的是pthread_mutex_lock函数。共享变量修改完成后,应该释放锁。

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

#define THREAD_COUNT 20000

//初始化 互斥锁
static pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;


void *add_task(void *arg)
{
	if(pthread_mutex_lock(&counter_mutex) == 0)
	{

		int *p = arg;
		(*p)++;
		pthread_mutex_unlock(&counter_mutex);
	}
	
	pthread_exit(NULL);
}

int main(int argc , char *argv[])
{
	

	int num = 0;
	pthread_t pid[THREAD_COUNT];

	for(int i = 0; i < THREAD_COUNT; i++)
	{
		pthread_create(pid + i,NULL,add_task,&num);
	}

	for(int i = 0; i < THREAD_COUNT; i++)
	{
		pthread_join(pid[i],NULL);
	}
	
	//打印累加结果
	printf("%d",num);
	return 0;
}

注意
上述代码中,互斥锁counter_mutex并未被显式销毁,但这通常不会引起资源泄露问题。上述程序在所有线程执行完毕后直接结束,进程结束时,操作系统会回收该进程的所有资源,包括内存、打开的文件描述符和互斥锁等。因此即便没有显式销毁互斥锁也不会有问题。

在某些情况下,确实需要显式销毁互斥锁资源。如果互斥锁是动态分配的(使用pthread_mutex_init函数初始化),或者互斥锁会被跨多个函数或文件使用,不再需要时必须显式销毁它。但对于静态初始化,并且在程序结束时不再被使用的互斥锁(上述程序中的counter_mutex),显式销毁不是必需的。

静态初始化保证了这个互斥锁的生命周期在这个进程中,进程结束互斥锁也会自动被回收销毁
动态分配时(也就是用初始化函数时)在堆中 需要手动释放
所以需要显示销毁 自己实现互斥锁需要对原子进行操作 实现不了

3.读写锁的基础机制

在读写锁的控制下,多个线程可以同时获得读锁,这些线程可以并发的读取共享资源,但他们的存在阻止了写锁的授予

写操作:如果至少有一个读操作持有读锁,写操作就无法获得写锁,写操作将会被阻塞,直到所有读锁都被释放

pthread_rwlock_t

1.int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

读写锁,读写锁的属性

为rwlock指向的读写锁分配所有需要的资源,并将锁初始化为未锁定状态。读写锁的属性
由attr参数指定,如果attr为NULL,则使用默认属性。当锁的属性为默认时,可以通过宏
PTHREAD_RWLOCK_INITIALIZER初始化,即
 * pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; 效果和调用当前方法
 并为attr传入NULL是一样的

2.int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

读写锁

销毁rwlock 指向的读写锁对象,并释放它所有的资源,当任何线程持有锁的时候销毁锁,
或者尝试销毁一个未初始化的锁时,结果是未定义的

3.int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

应用一个读锁到rwlock指向的读写锁上,并使调用线程获得读锁,如果写线程持有写锁,
读线程就无法获得读锁,会阻塞直到获得锁

4.int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

应用一个写锁到rwlock指向的读写锁上,并使调用线程获得写锁,如果任意线程持有读写
锁,则调用线程无法获得写锁,阻塞直到获得写锁

5.int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

释放锁

流程:
① 创建读写锁并且初始化
② 创建多个写线程,多个读线程
③ 读线程中 pthread_rwlock_rdlock(&rwlock) 获取读锁 执行完任务释放 unlock
④ 写线程中 pthread_rwlock_wrlock(&rwlock) 获取写锁 执行完任务释放 unlock
⑤ 主线程等待子线程运行完成 join
⑥ 销毁读写锁 pthread_rwlock_destroy(&rwlock)

这时读写操作是随机执行的

写操作不加锁测试:

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

//static pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; 静态初始化 不需要销毁


pthread_rwlock_t rwlock;
int shared_data = 0;

void *lock_reader(void *arg)
{
	//读写锁中的读是可以由多个线程共同读取的
	//获取读锁
	pthread_rwlock_rdlock(&rwlock);
	printf("当前是%s,shared_date为%d\n",(char*)arg,shared_data); 
	pthread_rwlock_unlock(&rwlock);
}


void *lock_writer(void *arg)
{
	int tmp = shared_data + 1;
	sleep(1);
	shared_data = tmp;
	printf("当前是%s,shared_date为%d\n",(char*)arg,shared_data);
}

int main(int argc , char *argv[])
{
	//显示的初始化读写锁
	pthread_rwlock_init(&rwlock,NULL);

	pthread_t writer1,writer2,reader1,reader2,reader3,reader4,reader5,reader6;

	//创建两个写线程
	pthread_create(&writer1,NULL,lock_writer,"writer1");
	pthread_create(&writer2,NULL,lock_writer,"writer2");

	//休眠等待
	sleep(3);

	pthread_create(&reader1,NULL,lock_reader,"reader1");
	pthread_create(&reader2,NULL,lock_reader,"reader2");
	pthread_create(&reader3,NULL,lock_reader,"reader3");
	pthread_create(&reader4,NULL,lock_reader,"reader4");
	pthread_create(&reader5,NULL,lock_reader,"reader5");
	pthread_create(&reader6,NULL,lock_reader,"reader6");

	//主线程等待创建的子线程运行完成
	pthread_join(writer1,NULL);
	pthread_join(writer2,NULL);
	pthread_join(reader1,NULL);
	pthread_join(reader2,NULL);
	pthread_join(reader3,NULL);
	pthread_join(reader4,NULL);
	pthread_join(reader5,NULL);
	pthread_join(reader6,NULL);

	
	//销毁读写锁
	pthread_rwlock_destroy(&rwlock);
	return 0;
}

写操作添加读写锁:

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

//static pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; 静态初始化 不需要销毁


pthread_rwlock_t rwlock;
int shared_data = 0;

void *lock_reader(void *arg)
{
	//读写锁中的读是可以由多个线程共同读取的
	//获取读锁
	pthread_rwlock_rdlock(&rwlock);
	printf("当前是%s,shared_date为%d\n",(char*)arg,shared_data); 
	pthread_rwlock_unlock(&rwlock);
}


void *lock_writer(void *arg)
{
	pthread_rwlock_wrlock(&rwlock);
	int tmp = shared_data + 1;
	sleep(1);
	shared_data = tmp;
	pthread_rwlock_unlock(&rwlock);
	printf("当前是%s,shared_date为%d\n",(char*)arg,shared_data);
}

int main(int argc , char *argv[])
{
	//显示的初始化读写锁
	pthread_rwlock_init(&rwlock,NULL);

	pthread_t writer1,writer2,reader1,reader2,reader3,reader4,reader5,reader6;

	//创建两个写线程
	pthread_create(&writer1,NULL,lock_writer,"writer1");
	pthread_create(&writer2,NULL,lock_writer,"writer2");

	//休眠等待
	sleep(3);

	pthread_create(&reader1,NULL,lock_reader,"reader1");
	pthread_create(&reader2,NULL,lock_reader,"reader2");
	pthread_create(&reader3,NULL,lock_reader,"reader3");
	pthread_create(&reader4,NULL,lock_reader,"reader4");
	pthread_create(&reader5,NULL,lock_reader,"reader5");
	pthread_create(&reader6,NULL,lock_reader,"reader6");

	//主线程等待创建的子线程运行完成
	pthread_join(writer1,NULL);
	pthread_join(writer2,NULL);
	pthread_join(reader1,NULL);
	pthread_join(reader2,NULL);
	pthread_join(reader3,NULL);
	pthread_join(reader4,NULL);
	pthread_join(reader5,NULL);
	pthread_join(reader6,NULL);

	
	//销毁读写锁
	pthread_rwlock_destroy(&rwlock);
	return 0;
}

4.写饥饿测试

多次运行后,我们发现,此时读操作总是连续执行的,且读操作休眠未结束时,写操作会被阻塞。与工作原理相符:
① 读操作可以并发执行,相互之间不必争抢锁,多个读操作可以同时获得读锁;
② 只要有一个线程持有读写锁,写操作就会被阻塞。
我们在读操作中加了1s休眠,只要有一个读线程获得锁,在1s内写操作是无法执行的,其它读操作就可以有充足的时间执行,因此读操作就会连续发生,写操作必须等待所有读操作执行完毕方可获得读写锁执行写操作。这就是使用读写锁时存在的潜在问题:写饥饿

① 问题描述
读写锁的写饥饿问题(Writer Starvation)是指在使用读写锁时,写线程可能无限期地等待获取写锁,因为读线程持续地获取读锁而不断地推迟写线程的执行。这种情况通常在读操作远多于写操作时出现。
② 解决方案
Linux提供了可以修改的属性pthread_rwlockattr_t,默认情况下,属性中指定的策略为“读优先”,当写操作阻塞时,读线程依然可以获得读锁,从而在读操作并发较高时导致写饥饿问题。我们可以尝试将策略更改为“写优先”,当写操作阻塞时,读线程无法获取锁,避免了写线程持有锁的时间持续延长,使得写线程获取锁的等待时间显著降低,从而避免写饥饿问题。

总结概括:
即更改写阻塞和读获取锁的优先级改变
原来是写阻塞时读可以获取锁改变后写阻塞时读不可以获取锁
写阻塞成为读阻塞的条件

解决写饥饿:
解决写饥饿:

pthread_rwlockattr_t

1.int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
读写锁属性对象指针

2.int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);
读写锁属性对象指针

3.int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
读写锁属性对象指针,设置锁类型(PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP :x写线程优先)

流程:
① 创建读写锁属性并初始化 ,显式调用
② 设置属性为写优先
③ 显示初始化读写锁,把读写锁属性写进去(第二个参数)
④ 销毁读写锁属性
⑤ …
⑥ 销毁读写锁

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

//static pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER; 静态初始化 不需要销毁


pthread_rwlock_t rwlock;
int shared_data = 0;

void *lock_reader(void *arg)
{
	//读写锁中的读是可以由多个线程共同读取的
	//获取读锁
	pthread_rwlock_rdlock(&rwlock);
	printf("当前是%s,shared_date为%d\n",(char*)arg,shared_data); 
	sleep(1);
	pthread_rwlock_unlock(&rwlock);
}


void *lock_writer(void *arg)
{
	pthread_rwlock_wrlock(&rwlock);
	int tmp = shared_data + 1;
	shared_data = tmp;
	pthread_rwlock_unlock(&rwlock);
	printf("当前是%s,shared_date为%d\n",(char*)arg,shared_data);
}

int main(int argc , char *argv[])
{
	//创建读写锁属性对象
	pthread_rwlockattr_t attr;
	pthread_rwlockattr_init(&attr);

	//修改参数设置写优先
	pthread_rwlockattr_setkind_np(&attr,PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
	
	//显示的初始化读写锁
	pthread_rwlock_init(&rwlock,&attr);
	pthread_rwlockattr_destroy(&attr);

	pthread_t writer1,writer2,reader1,reader2,reader3,reader4,reader5,reader6;

	//创建两个写线程
	pthread_create(&writer1,NULL,lock_writer,"writer1");


	pthread_create(&reader1,NULL,lock_reader,"reader1");
	pthread_create(&reader2,NULL,lock_reader,"reader2");
	pthread_create(&reader3,NULL,lock_reader,"reader3");

	//在读线程中间 添加一个获取写锁的写操作
	pthread_create(&writer2,NULL,lock_writer,"writer2");
	pthread_create(&reader4,NULL,lock_reader,"reader4");
	pthread_create(&reader5,NULL,lock_reader,"reader5");
	pthread_create(&reader6,NULL,lock_reader,"reader6");

	//主线程等待创建的子线程运行完成
	pthread_join(writer1,NULL);
	pthread_join(writer2,NULL);
	pthread_join(reader1,NULL);
	pthread_join(reader2,NULL);
	pthread_join(reader3,NULL);
	pthread_join(reader4,NULL);
	pthread_join(reader5,NULL);
	pthread_join(reader6,NULL);

	
	//销毁读写锁
	pthread_rwlock_destroy(&rwlock);
	return 0;
}

通过运行后结果可以发现,此时的连续六次读操作间夹杂了写操作,不再连续,写操作不必等待所有读操作完成才可以执行。不必长期等待,写饥饿问题已得到解决。

5.自旋锁

在Linux内核中,自旋锁是一种用于多处理器系统中的低级同步机制,主要用于保护非常短的代码段或数据结构,以避免多个处理器同时访问共享资源。自旋锁相对于其他锁的优点是它们在锁被占用时会持续检查锁的状态(即“自旋”),而不是让线程进入休眠。这使得自旋锁在等待时间非常短的情况下非常有效,因为它避免了线程上下文切换的开销。
自旋锁主要用于内核模块或驱动程序中,避免上下文切换的开销。不能在用户空间使用

自旋:不断循环

6.条件变量原理介绍

1)restrict关键字

restrict是一个C99标准引入的关键字,用于修饰指针,它的作用是告诉编译器,被修饰的指针是编译器所知的唯一一个可以在其作用域内用来访问指针所指向的对象的方法。这样一来,编译器可以放心地执行代码优化,因为不存在其他的别名(即其他指向同一内存区域的指针)会影响到这块内存的状态。 也就是只有一个名字能访问这块内存的内容
restrict声明了一种约定,主要目的是允许编译器在生成代码时做出优化假设,而不是在程序的不同部分间强制执行内存访问的规则。程序员需要确保遵守restrict的约定,编译器则依赖这个约定来进行优化。如果restrict约定被违反,可能导致未定义行为。
函数参数使用restrict修饰,相当于约定:函数执行期间,该参数指向的内存区域不会被其它指针修改。

2)线程间条件切换函数
如果需要两个线程协同工作,可以使用条件变量完成线程切换。查看文档可得:

1.int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

指向条件变量的指针(条件变量用于等待某个条件发生通过某一cond等待的线程需要通过同一cond的signal唤醒),与条件变量配合使用的互斥锁的指针。在调用pthread_cond_wait之前,线程必须已经获得了这个互斥锁。

调用该方法的线程必须持有互斥锁,调用该方法的线程会阻塞并且临时释放互斥锁,并等待其他线程调用pthread_cond_signal或者pthread_cond_broadcast唤醒,当被唤醒后线程会尝试重新获取读写锁

int 成功时返回0;失败时返回错误码,而非-1。错误码可能包括EINVAL、EPERM等,具体取决于错误的性质。

2.int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

添加超时机制 其他一样

3.int pthread_cond_signal(pthread_cond_t *cond);

指向条件变量的指针

唤醒因cond而阻塞的线程,如果多个线程因为cond则随机唤醒一个,如果没有线程在等待,则什么也不做

成功返回 0 失败返回错误码

4.int pthread_cond_broadcast(pthread_cond_t *cond);
唤醒所有正在等待条件变量cond的线程。如果没有线程在等待,这个函数什么也不做。

说明:
(1)使用条件变量时,通常涉及到一个或多个线程等待“条件变量”代表的条件成立,而另外一些线程在条件成立时触发条件变量。
(2)条件变量的使用必须与互斥锁配合,以保证对共享资源的访问是互斥的。
(3)条件变量提供了一种线程间的通信机制,允许线程以无竞争的方式等待特定条件的发生。

等条件变量代表的条件成立
配合互斥锁
是一种线程间通信机制

条件变量:
pthread_cond_t

条件变量允许线程挂起执行并释放已持有的互斥锁,等待某个条件变为真。条件变量总是需要与互斥锁一起使用,以避免出现竞态条件。

(2)用途
允许线程等待特定条件的发生。当条件尚未满足时,线程通过条件变量等待,直到其他线程修改条件并通知条件变量。
通知等待中的线程条件已改变,允许它们重新评估条件。
操作:
① 创建并初始化条件变量 pthread_cond_init
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 静态初始化

使用PTHREAD_COND_INITIALIZER静态初始化的条件变量通常不需要调用pthread_cond_destroy来销毁。但是,如果条件变量在程序执行期间被重新初始化(通过pthread_cond_init),那么在不再需要时应使用pthread_cond_destroy进行清理。
PTHREAD_COND_INITIALIZER只适用于静态或全局变量的初始化。对于动态分配的条件变量(例如,通过malloc分配的条件变量),应使用pthread_cond_init函数进行初始化。
PTHREAD_COND_INITIALIZER提供的是条件变量的默认属性。如果需要自定义条件变量的属性(例如,改变其pshared属性以支持进程间同步),则需要使用pthread_cond_init和pthread_condattr_t类型的属性对象。

② 在给定的互斥锁上等待条件变量。调用时线程将释放互斥锁并进入等待状态直到被唤醒 pthread_cond_wait
③ 定时等待 pthread_cond_timewait
④ 信号pthread_cond_signal:唤醒至少一个等待该条件变量的线程
⑤ 广播pthread_cond_broadcast:唤醒所有等待该条件变量的线程
⑥ 销毁pthread_cond_destroy():清理条件变量资源

流程:
① 初始化互斥锁
② 初始化条件变量要一个读一个写
③ 创建两个线程一个读一个写
写中:获取互斥锁,先向缓冲区中写数据,写完后发出可以读的信号,然后自己挂起等待
读中:获取互斥锁,判断缓冲区是否有数据,有数据则读出来,读完后发出可以写的信号,然后自己挂起等待,如果没得读(比如读先获取了锁),则直接发出可以写的信号,自身挂起释放锁,写就可以获得锁(不过这时不是被唤醒而是获得了锁)
④ 主线程挂起等待两个线程完成

条件变量功能展示:

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

#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int count = 0;

//初始化互斥锁                                     initialzer
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

//初始化条件变量
static pthread_cond_t can_write_cond = PTHREAD_COND_INITIALIZER;
static pthread_cond_t can_read_cond = PTHREAD_COND_INITIALIZER;


//期望功能是读或写的一方 一直进行读写操作 直到缓冲区读完或者写满 暂时释放锁


//从buffer中读数据
void *read_buffer(void *arg)
{
	//使用共同的变量 使用互斥锁 获取锁
	pthread_mutex_lock(&mutex);
	while(1)
	{

		while(count > 0)
		{
			printf("我收到白月光的幸运数字为%d\n",buffer[--count]);

		}

		//通知可以继续写
		pthread_cond_signal(&can_write_cond);

		//缓存中没有消息可读
		//暂停线程
		pthread_cond_wait(&can_read_cond,&mutex);

	}

	//最后释放锁
	pthread_mutex_unlock(&mutex);


}

//向buffer中写数据
void *write_buffer(void *arg)
{
	//使用共同的变量 使用互斥锁 获取锁
	pthread_mutex_lock(&mutex);
	int item = 1;
	while(1)
	{
		//如果缓冲区写满 使用条件变量暂停当前线程
		while(count < BUFFER_SIZE)
		{
			//缓冲区没满
			buffer[count++] = item++;
			printf("白月光发送了一个幸运数字%d\n",buffer[count-1]);
		}
		//先发出可以读的信号
		pthread_cond_signal(&can_read_cond);

		//写满了则挂起等待
		pthread_cond_wait(&can_write_cond,&mutex);

	}
	//最后释放锁
	pthread_mutex_unlock(&mutex);  //但是实际上不会执行
}


int main(int argc,char *argv)
{
	pthread_t writer,reader;
	//创建两个线程 一个读 一个写
	pthread_create(&reader,NULL,read_buffer,NULL);

	pthread_create(&writer,NULL,write_buffer,NULL);

	//主线程需要挂起等待两个线程完成
	pthread_join(reader,NULL);
	pthread_join(writer,NULL);
	return 0;
}

案例中读条件和写条件配合使用

总结

本文总结了有关于线程、线程处理和线程同步的知识。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值