linux——线程及线程同步

17 篇文章 0 订阅

线程

线程基本知识

什么是线程
轻量级进程(LWP: light weight process),在linux环境下线程的本质仍然是进程.

系统分配资源的最基本单位是——进程
系统调度进程执行的最小单位是——线程

1、多个子线程和主线程共享一个地址空间,有一个PID
2、通过使用线程号来区分不同的线程;
3、除了栈空间以外,其余资源都可以共享
4、主线程和子线程是靠抢占CPU时间片来执行的,谁先抢到谁先使用;
创建线程
查看指定线程的LWP号:ps -Lf pid

实际上,无论是创建进程的fork,还是创建线程的pthread_create,底层实现都是调用同一个内核函数clone:
如果复制对方的地址空间,那么就产生一个进程
如果共享对方的地址空间,就产生一个线程
linux内核是不区分进程和线程的,只在用户层面上进程区分
线程操作是调用库函数,而不是系统调用

线程共享资源

  • 文件描述符表
  • 信号处理方式
  • 用户ID和组ID
  • 内存地址空间(.txt,.data,.bss,堆,共享库)

线程非共享资源

  • 线程ID
  • errno变量
  • 调度优先级

网上找到一篇大佬对线程资源的总结 线程共享资源 很详细,一定要阅读以下!!!

线程优缺点

优点:

提高程序并发性
开销小
共享数据方便

缺点:

库函数,不稳定
gdb调试困难
编写程序困难

线程相关函数

  • pthread_create
int pthread_create(pthread_t* thread, const pthread_attr_t* att, void* (*start_routine)(void*), void* arg);
param:
	thread: 出参,线程ID
	att: 通常传NULL,表示线程属性默认
	start_routine: 函数指针,指向线程主函数,该函数运行结束,则线程结束
	arg: 线程函数执行期间需要的参数
return:
	成功:返回0
	失败:返回错误号

强调:
1、由于pthread_create的错误码不保存在errno中,因此不能直接调用perror()打印错误信息,可以先用strerror()把错误码转换成错误信息再打印。
2、如果任意一个线程调用了exit或者_exit,则整个进程的所有线程都终止,因此线程中禁止调用exit函数(除非是类似malloc调用失败)

  • pthread_self
int pthread_self(void);
return:
	返回线程ID

例1: 循环创建5个子线程
注意:如果使用变量i作为线程函数的参数,那么创造出来的线程得到的i都是5。因为i的内存只有一块,这块内存是以地址形式传给子线程的,所以被5个线程共享的,主线程在一个CPU时间片完成了5个子线程的创建,此时i=5,而其它子线程在主线程让出CPU时间片后才得到执行,所以最后每个线程读到的值都是5。另外,5个子线程也是抢占CPU的,所以执行顺序每次可能都不一样。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
void* mythread(void* arg)
{
	int i = *(int*)arg;
	printf("[%d]\n", i);
}
int main()
{
	int ret;
	int i=0;
	int arr[5];
	pthread_t thread[5];
	for(i=0;i<5;i++)
	{
		arr[i]=i;
		//ret = pthread_create(&thread[i], NULL, mythread, &i);
		ret = pthread_create(&thread[i], NULL, mythread, &arr[i]);
		if(ret!=0)
		{
			printf("pthread_create error, [%s]\n", strerror(ret));
			return -1;
		}
	}
	sleep(1);//可以用线程函数pthread_join代替
	return 0;
}
  • pthread_exit

使一个线程退出,如果主线程调用pthread_exit函数也不会使整个进程退出,不影响其他线程的执行。

void pthread_exit(void* retval);
param:
	retval: 表示线程的退出状态,通常传NULL

注意:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为栈空间会被回收。

  • pthread_join

阻塞并等待线程退出,获取线程退出状态。对应进程的waitpid()函数

int pthread_join(pthread_t thread, void** retval);
param:
	thread: 线程ID
	retval: 存储线程结束状态,整个指针和pthread_exit的参数是同一块内存地址
return:
	成功:0
	失败:返回错误号

pthread_join中的指针与pthread_exit指向的是同一块地址(最好用全局变量的地址,因为栈会被释放)

例2: 子线程返回给主线程值

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>

typedef struct
{
	int a;
	char str[10];
}Arg;
void* mythread(void* arg)
{
	Arg *r = (Arg*)arg;
	r->a = r->a * r->a;
	printf("--- [%p] --- \n", r);
	//return (void*)r;
	pthread_exit((void*)r);
}
int main()
{
	pthread_t tid;
	Arg r = {10, "abcdef"};
	pthread_create(&tid, NULL, mythread, (void*)&r);
	int *result;
	pthread_join(tid, (void**)&result);
	printf("===[%p]===%d  %s\n", result, ((Arg*)result)->a, ((Arg*)result)->str);
	return 0;
}

在这里插入图片描述

例3: 子线程将返回值以地址的形式返回给主线程

typedef struct
{
	int a;
	char str[10];
}Arg;

void* mythread(void* arg)
{
	Arg *r = (Arg*)arg;
	r->a = r->a * r->a;
	pthread_exit((void*)r->a);
}

int main()
{
	pthread_t tid;
	Arg r = {10, "abcdef"};
	pthread_create(&tid, NULL, mythread, (void*)&r);
	int *result;
	pthread_join(tid, (void**)&result);
	printf("线程返回值:%d\n", (int)result);
	return 0;
}

在这里插入图片描述

  • pthread_detach

设置线程属性为分离线程,一般情况下,线程终止后,其终止状态一直保留到其他线程调用pthread_join,但是线程也可以被设置成detach状态,这样的线程一旦终止就立刻回收它所占用的资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用
phread_join,这样调用将返回错误。分离属性在网络通讯中使用较多。

int pthread_detach(pthread_t thread);
param:
	thread: 线程ID
return:
	成功:0
	失败:错误号
  • pthread_cancel

杀死线程,相当于进程中的kill()
注意:线程取消不是实时的,而是有一定的延时,需要达到某个取消点man 7 pthreads可以粗略的认为一个系统调用(进入内核)就是一个取消点,比如printf就会调用write进入内核,另外,也可以用pthread_testcancel(void)设置取消点)

int pthread_cancel(pthread_t thread);
param:
	thread: 线程ID
return:
	成功:0
	失败:错误号

线程属性

默认线程属性都是非分离状态

//1、定义线程属性类型的变量
pthread_attr_t attr;
//2、兑线程属性变量进行初始化
int pthread_attr_init(pthread_attr_t* attr);
//3、设置线程分离属性
int pthread_attr_setdetachstate(pthread_attr_t* attr, int detach);
param:
	detach:PTHREAD_CREATE_DETACHED(分离)
		   PTHREAD_CREATE_JOINABLE(非分离)
//前三步都是为了pthread_creat的第二个参数准备
//4、释放线程属性资源(主线程中调用)
int pthread_attr_destroy(pthread_attr_t* attr);

线程同步

线程同步指一个线程发出某一功能调用时,在没有得到结果之前,该调用不返回。同时其他线程为保证数据一致性,不能调用该功能。

互斥锁

线程A和线程B共同访问共享资源,当线程A想访问共享资源的时候,要先获得锁,如果锁被占用,则加锁不成功,需要阻塞并等待对方释放锁;若锁没有被占用,则获得锁成功—加锁,然后操作共享资源,操作完之后,必须解锁,同理线程B也一样。也就是同时不能有两个线程操作共享资源,属于互斥操作。

原子操作:该操作要么不执行,要么就完成,不会出现做一半另一半不做的情况。使用互斥锁其实就是模拟原子操作。

linux提供一把互斥锁mutex(也称为互斥量)。每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。

互斥锁函数

1、mutex类型的变量
pthread_mutex_t mutex;

2、初始化一个互斥锁,可以理解成mutex初始值是1
int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr_t* restrict attr);
param:
	attr: 互斥锁属性,默认为NULL

3、销毁一个互斥锁
int pthread_mutex_destroy(pthread_mutex_t* mutex);

4、加锁,可以理解为mutex--,由1变成0,阻塞函数
int pthread_mutex_lock(pthread_mutex_t* mutex);

5、解锁,可以理解为mutex++,由0变成1
int pthread_mutex_unlock(pthread_mutex_t* mutex);

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

使用互斥锁后,两个线程并行操作变成了串行操作,效率降低了,但是数据不一致的问题得到了解决。

死锁

死锁并不是linux提供给用户的一种使用方法,而是用户使用互斥锁不当引起的一种现象。
注意:线程在异常退出的时候也需要解锁
A线程占用着A锁,又想去获得B锁;B线程占用着B锁,又想去获取A锁。两个线程都不释放自己的锁,又想去获得对方的锁,从而造成了死锁。

如何解锁死锁

  • 让线程按照一定的顺序去访问共享资源;
  • 在访问其他锁的时候,需要先将自己的锁解开;
  • 调用pthread_mutex_trylock,如果加锁不成功会立刻返回;
  • 避免使用嵌套的锁

读写锁

读写锁是一把锁

读写锁也叫共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的写独占、读共享当读和写一起等待锁的时候,写的优先级高。

适用情况:读写锁非常适合于对数据结构读的次数远大于写的情况。

读写锁特性:

  • 读写锁是“写模式加锁”时,解锁前,所有对该锁加锁的线程都会被阻塞;
  • 读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞;

练习:
1、线程A加写锁成功,线程B请求读锁——线程B阻塞,当线程A解锁后,线程B加锁成功;
2、线程A加读锁成功,线程B请求写锁——线程B阻塞,当线程A解锁后,线程B加锁成功;
3、线程A加读锁成功,线程B请求读锁——线程B请求成功;
4、线程A加读锁成功,线程B请求写锁,线程C请求读锁——线程B和C都阻塞,当线程A释放锁后,线程B先获得锁,C阻塞;当B释放锁后,C获得锁;
5、线程A加写锁成功,线程B请求读锁,线程C请求写锁——线程B和C都阻塞,当线程A释放锁后,线程C先获得锁,B阻塞;当C释放锁后,B获得锁;

读写锁函数

1、定义一把读写锁
pthread_rwlock_t rwlock;

2、初始化一个读写锁
int pthread_rwlock_init(pthread_rwlock_t* restrict rwlock, const pthread_rwlockattr_t* restrict attr);
param:
	attr: 读写锁属性,默认为NULL

3、销毁一个读写锁
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);

4、加读锁数
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);

5、加写锁
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);

6、解锁
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);

6、尝试加锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t* rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t* rwlock);

例4: 3个线程写数据,5个线程读数据

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
#include<time.h>

int number = 0;
//定义一把读写锁
pthread_rwlock_t rwlock;
//写线程回调函数
void* thread_write(void* arg)
{
	int i = *(int*)arg;
	int cur;
	while(1)
	{
		//加写锁
		pthread_rwlock_wrlock(&rwlock);

		cur=number;
		cur++;
		number=cur;
		printf("[%d]-W:[%d]\n", i, cur);
		
		//解锁
		pthread_rwlock_unlock(&rwlock);
		usleep(500);
	}
}

//读线程回调函数
void* thread_read(void* arg)
{
	int i=*(int*)arg;
	int cur;
	while(1)
	{
		//加读锁
		pthread_rwlock_rdlock(&rwlock);

		cur=number;
		printf("[%d]-R:[%d]\n", i, cur);
		
		//解锁
		pthread_rwlock_unlock(&rwlock);
		usleep(400);
	}
}

int main()
{
	//创建子线程
	int n = 8;
	int i = 0;
	int arr[8];
	pthread_t thread[8];
	//初始化读写锁
	pthread_rwlock_init(&rwlock, NULL);
	//创建3个写线程
	for(i = 0; i < 3; i++)
	{
		arr[i]=i;
		pthread_create(&thread[i], NULL, thread_write, &arr[i]);
	}
	
	//创建5个读线程
	for(i = 3; i < 8; i++)
	{
		arr[i]=i;
		pthread_create(&thread[i], NULL, thread_read, &arr[i]);
	}

	//回收子线程	
	int j=0;
	for(; j <  n; j++)
	{
		pthread_join(thread[j], NULL);
	}
	//销毁读写锁
	pthread_rwlock_destroy(&rwlock);

	return 0;
}

条件变量

条件变量本身不是锁,但它可以造成线程阻塞,通常和互斥锁配合使用。给多个线程提供了一个会合的场所。

  • 使用互斥量保护共享数据
  • 使用条件变量可以使线程阻塞,等待某个条件的发生,当条件满足的时候解除阻塞。

条件变量的两个动作:

  • 条件不满足,阻塞线程,并解锁
  • 条件满足,解除阻塞,并加锁

条件变量函数

1、定义一个条件变量
pthread_cond_t cond;

2、初始化条件变量
int pthread_cond_init(pthread_cond_t* restrict cond, const pthread_condattr_t* restrict attr);

3、销毁条件变量
int pthread_cond_destroy(pthread_cond_t* cond);

4、阻塞等待
int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex);

5、唤醒阻塞的线程
int pthread_cond_signal(pthread_cond_t* cond);

例5: 生产者消费者模型:生产者生产链表,消费者读取链表数据并释放

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
#include<time.h>

typedef struct node
{
	int data;
	struct node* next;
}NODE;
NODE* head=NULL;

//定义一把锁
pthread_mutex_t mutex;

//定义条件变量
pthread_cond_t cond;

//生产者线程
void* producer(void* arg)
{
	NODE* pNode=NULL;
	while(1)
	{
		//生产一个节点
		pNode=(NODE*)malloc(sizeof(NODE));
		if(pNode==NULL)
		{
			perror("malloc error");
			exit(-1);
		}
		pNode->data=rand()%1000;
		printf("Producer:[%d]\n", pNode->data);
		
		//加锁
		pthread_mutex_lock(&mutex);
		pNode->next=head;
		head=pNode;
		//解锁
		pthread_mutex_unlock(&mutex);
		
		//唤醒条件
		pthread_cond_signal(&cond);
		
		sleep(rand()%3);//避免申请太快
	}
}

//消费者线程
void* consumer(void* arg)
{
	NODE* pNode=NULL;
	while(1)
	{
		//加锁
        pthread_mutex_lock(&mutex);
		
		if(head==NULL)
		{
			//条件不满足,需阻塞等待
			//若条件不满足,则阻塞等待并加锁;条件满足,则接触阻塞并加锁
			pthread_cond_wait(&cond, &mutex);
		}
		printf("Consumer:[%d]\n", head->data);
		pNode=head;
		head=head->next;
		
		//解锁
        pthread_mutex_unlock(&mutex);

		free(pNode);
		pNode=NULL;
		
		sleep(rand()%3);
	}
}

int main()
{
	srand(time(NULL));
	pthread_t thread1;
	pthread_t thread2;
	//初始化锁
	pthread_mutex_init(&mutex, NULL);
	//初始化条件变量
	pthread_cond_init(&cond, NULL);
	//创建生产者线程
	pthread_create(&thread1, NULL, producer, NULL);
	//创建生产者线程
	pthread_create(&thread2, NULL, consumer, NULL);

	//回收子线程	
	pthread_join(thread1, NULL);
	pthread_join(thread2, NULL);
	
	//销毁锁
	pthread_mutex_destroy(&mutex);
	//销毁条件变量
	pthread_cond_destroy(&cond);
	return 0;
}

例6: 强化:有5个生产者和5个消费者,共同来创造和消费链表
发生段错误的情况:假如只有一个生产者生产了一个节点,然后通知消费者,此时若有多个消费者被唤醒,那么条件变量满足,但是只有一个消费者获得锁,其余消费者都阻塞在了mutex而不是条件变量,因此只有一个消费者消费了节点,之后释放锁,其他消费者获得锁后,但此时head已经成了NULL,所以发生了段错误。

在使用条件变量的线程中,能够引起线程阻塞的地方有两个:
1、在条件变量处阻塞——这个会被pthread_cond_signal唤醒
2、互斥锁也会使线程阻塞——其它线程解锁会解除阻塞

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
#include<time.h>

typedef struct node
{
	int data;
	struct node* next;
}NODE;
NODE* head=NULL;

//定义一把锁
pthread_mutex_t mutex;

//定义条件变量
pthread_cond_t cond;

//生产者线程
void* producer(void* arg)
{
	NODE* pNode=NULL;
	int n=*(int*)arg;
	while(1)
	{
		//生产一个节点
		pNode=(NODE*)malloc(sizeof(NODE));
		if(pNode==NULL)
		{
			perror("malloc error");
			exit(-1);
		}
		pNode->data=rand()%1000;
		printf("Producer[%d]:[%d]\n", n, pNode->data);

		//加锁
		pthread_mutex_lock(&mutex);
		pNode->next=head;
		head=pNode;
		//解锁
		pthread_mutex_unlock(&mutex);

		//唤醒条件
		pthread_cond_signal(&cond);

		sleep(rand()%3);//避免申请太快
	}
}

//消费者线程
void* consumer(void* arg)
{
	NODE* pNode=NULL;
	int n=*(int*)arg;
	while(1)
	{
		//加锁
		pthread_mutex_lock(&mutex);

		if(head==NULL)
		{
			//条件不满足,需阻塞等待
			//若条件不满足,则阻塞等待并加锁;条件满足,则解除阻塞并加锁
			pthread_cond_wait(&cond, &mutex);
		}
		if(head==NULL)
		{
			pthread_mutex_unlock(&mutex);
			continue;
		}
		printf("Consumer[%d]:[%d]\n", n, head->data);
		pNode=head;
		head=head->next;

		//解锁
		pthread_mutex_unlock(&mutex);

		free(pNode);
		pNode=NULL;

		sleep(rand()%3);
	}
}

int main()
{
	srand(time(NULL));
	pthread_t thread1[5];
	pthread_t thread2[5];
	int i;
	int arr[5];
	//初始化锁
	pthread_mutex_init(&mutex, NULL);
	//初始化条件变量
	pthread_cond_init(&cond, NULL);
	for(i=0;i<5;i++)
	{
		arr[i]=i;
		//创建生产者线程
		pthread_create(&thread1[i], NULL, producer, &arr[i]);
		//创建生产者线程
		pthread_create(&thread2[i], NULL, consumer, &arr[i]);
	}

	for(i=0;i<5;i++)
	{
		//回收子线程	
		pthread_join(thread1[i], NULL);
		pthread_join(thread2[i], NULL);
	}
	//销毁锁
	pthread_mutex_destroy(&mutex);
	//销毁条件变量
	pthread_cond_destroy(&cond);
	return 0;
}

信号量

信号量相当于多把锁,可以理解为加强版的互斥锁

信号量函数

1、定义信号量
sem_t sem;

2、初始化信号量
int sem_init(sem_t* sem, int pshared, unsigned int value);
param:
	pshared: 0表示线程同步;1表示进程同步
	value: 表示最多有几个线程操作共享数据

3、调用一次相当于sem--,当sem为0时,引起阻塞
int sem_wait(sem_t* sem);

4、调用一次相当于sem++
int sem_post(sem_t* sem);

5、尝试加锁,直接返回
int sem_trywait(sem_t* sem);

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

例7: 使用信号量实现生产者消费者模型

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<pthread.h>
#include <semaphore.h>
#include<time.h>

typedef struct node
{
	int data;
	struct node* next;
}NODE;
NODE* head=NULL;

//定义信号量
sem_t sem_producer;
sem_t sem_consumer;
//生产者线程
void* producer(void* arg)
{
	NODE* pNode=NULL;
	while(1)
	{
		//生产一个节点
		pNode=(NODE*)malloc(sizeof(NODE));
		if(pNode==NULL)
		{
			perror("malloc error");
			exit(-1);
		}
		pNode->data=rand()%1000;
		printf("Producer:[%d]\n", pNode->data);
		
		//加锁,相当于--
		sem_wait(&sem_producer);
		pNode->next=head;
		head=pNode;
		//解锁,相当于++
		sem_post(&sem_consumer);
		
		sleep(rand()%3);//避免申请太快
	}
}

//消费者线程
void* consumer(void* arg)
{
	NODE* pNode=NULL;
	while(1)
	{
		//加锁,相当于--
        sem_wait(&sem_consumer);
		
		printf("Consumer:[%d]\n", head->data);
		pNode=head;
		head=head->next;
		
		//解锁,相当于++
        sem_post(&sem_producer);

		free(pNode);
		pNode=NULL;
		
		sleep(rand()%3);
	}
}

int main()
{
	srand(time(NULL));
	pthread_t thread1;
	pthread_t thread2;
	//初始化信号量
	sem_init(&sem_producer, 0, 5);
	sem_init(&sem_consumer, 0, 0);
	//创建生产者线程
	pthread_create(&thread1, NULL, producer, NULL);
	//创建生产者线程
	pthread_create(&thread2, NULL, consumer, NULL);

	//回收子线程	
	pthread_join(thread1, NULL);
	pthread_join(thread2, NULL);
	
	//销毁信号量
	sem_destroy(&sem_producer);
	sem_destroy(&sem_consumer);
	return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值