线程知识记录

1、线程概述

进程是资源管理的最小单位,线程是程序执行的最小单位,也被称为轻量级进程。

1.1、线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:
	1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。//线程return
	2. 一个线程可以调用pthread_cancel终止同一进程中的另一个线程。//被动终止
	3. 线程可以调用pthread_exit终止自己。//自己终止
	方式1和方式4的区别是,方式4可以返回一个结果指针,通过pthread_join可以获取,并且会启动资源释放。

	在任意一个时间点上,线程是可结合(joinable)或者是可分离的(detached)。一个可结合线程是可以被其他线程收回资源和杀死的。
在被回收之前,他的存储器资源(栈等)是不释放的。而对于detached状态的线程,其资源不能被别的线程收回和杀死,只有等到线程结束才能由系统自动释放。
	默认情况,线程状态被设置为结合的。所以为了避免资源泄漏等问题,一个线程应当是被显示的join或者detach的,否则线程的状态类似于进程中的Zombie Process。会有部分资源没有被回收的。
调用函数pthread_join,当等待线程没有终止时,主线程将处于阻塞状态。如果要避免阻塞,那么
在主线程中加入代码pthread_detach(thread_id)或者在被等待线程中加入pthread_detach(thread_self())。

1.2、线程属性

typedef struct
{
       int                               detachstate;   线程的分离状态
       int                               schedpolicy;  线程调度策略
       structsched_param              schedparam;  线程的调度参数
       int                               inheritsched;  线程的继承性
       int                                scope;       线程的作用域
       size_t                           guardsize;   线程栈末尾的警戒缓冲区大小
       int                                stackaddr_set;
       void*                          stackaddr;   线程栈的位置
       size_t                           stacksize;    线程栈的大小
}pthread_attr_t;		

1.3、线程优缺点

优:可以并发操作(对于多核或多CPU计算机来说,多线程可以最大发挥计算机能力。单核或单CPU的计算机来说,多线程其实是假的,是不停的切换调度模拟出来的)。
	相对于多进程,多线程之间数据共享效率更高。
缺:线程之间的数据同步略难搞,搞不好的话就会出现莫名其妙的问题。

1.4、线程权限

可由下图概括:
在这里插入图片描述

线程私有 线程之间共享(进程所有)
1、局部变量
2、函数参数
3、TLS数据
1、全局变量
2、堆上的数据
3、函数里的静态变量
4、程序代码(任何线程都有权利读取并执行任何代码)
5、打开的文件(A线程打开的文件可以由B线程读写)

2、线程函数

#include <pthread.h>
!!注:pthread并非Linux系统的默认库,在编译时注意加上-lpthread参数

	restrict:是c99标准引入的,它只可以用于限定和约束指针,并表明指针是访问一个数据对象的唯一且初始的方式.即它告诉编译器,
所有修改该指针所指向内存中内容的操作都必须通过该指针来修改,而不能通过其它途径(其它变量或指针)来修改;这样做的好处是,
能帮助编译器进行更好的优化代码,生成更有效率的汇编代码.如 int *restrict ptr, ptr 指向的内存单元只能被 ptr 访问到,
任何同样指向这个内存单元的其他指针都是无效指针。
函数名 参数说明 返回值 说明
int pthread_create(pthread_t *restrict tidp,
const pthread_attr_t *restrict attr,
void *(*start_rtn)(void),
void *restrict arg);
//返回线程的ID
//线程属性,默认为NULL使用默认属性
//线程函数入口地址
//线程函数参数,传入的是参数的地址,如果有多个参数就传结构体的地址。!!注:传栈地址的时候要小心
成功返回0 失败返回错误编号
pthread_t pthread_self(void); 返回调用该函数的线程的线程ID
void pthread_exit(void *rval_ptr); 要返回的值 终止线程。!!注:如果在主线程中调用,就会终止主线程,而主线程创建的分离子线程还会跑,但是如果在主线程中return,整个进程都会结束包括所有线程
int pthread_join(pthread_t thread, void **rval_ptr); 1、线程id
2、存储被等待线程的返回值
成功返回0 否则返回错误编号 该函数阻塞等待参数thread指定的线程,被等待的线程必须是jsonable,使用此函数来释放线程资源
int pthread_detach(pthread_t tid); 1、线程id 成功返回0 否则返回错误编号 使线程处于分离状态,线程终止后,系统会自动释放其资源
int pthread_cancel(pthread_t tid); 成功返回0 否则返回错误编号 请求同一进程中的其它线程退出。要注意的是,该函数并不等待线程终止,他仅仅提出请求。调用了该函数也不等于目标线程马上就会退出,目标线程有可能再运行一段时间后到达取消点才退出;甚至有可能不响应退出。!!注:具体响应操作可以通过pthread_setcancelstate、pthread_setcanceltype进行设置
int pthread_equal(pthread_t tid1, pthread_t tid2); 若相等则返回非0值,否则返回0值
int pthread_attr_init(pthread_attr_t*attr); 0 - 成功,非0 - 失败 线程属性使用前必须使用该函数进行初始化
int pthread_attr_destroy(pthread_attr_t*attr); 0 - 成功,非0 - 失败 销毁一个目标结构,并且使它在重新初始化之前不能重新使用
int pthread_attr_getdetachstate(pthread_attr_t *attr,int detachstate); 1、线程属性变量
2、线程的分离状态属性
获取线程的分离属性
int pthread_attr_setdetachstate(pthread_attr_t *attr,int *detachstate); 0 - 成功,非0 - 失败 设置线程分离属性
其他属性的获取和设置都类似

3、线程安全

3.1、同步的方法

3.1.1、信号量(Semaphore)

全称是多元信号量,对于允许多线程并发访问的资源,信号量是一个很好的选择。一个初始值为N的信号量允许N个线程并发访问。
线程访问资源的时候,首先获取信号量,操作如下:、
1、将信号量的值减1;
2、如果信号量的值小于0,则进入等待状态,否则继续执行。
访问完资源后,释放信号量,操作如下:
1、将信号箱的值加1;
2、如果信号量的值小于1,唤醒一个等待中的线程。

使用到的函数有:
#include<sys/sem.h>
(1)int semget(key_t key,int num_sems,int sem_flags);
	key:信号量键值,可以理解为信号量的唯一性标记。
	num_sems:信号量的数目,一般为1
	sem_flags:有两个值,IPC_CREATE和IPC_EXCL,
	IPC_CREATE表示若信号量已存在,返回该信号量标识符。
	IPC_EXCL表示若信号量已存在,返回错误。
	返回值:相应的信号量标识符,失败返回-1
(2)int semop(int sem_id,struct sembuf *sem_opa,size_t num_sem_ops);
	sem_id:信号量标识符
	sem_opa:结构如下
	struct sembuf{  
	    short sem_num;//除非使用一组信号量,否则它为0  
	    short sem_op;//信号量在一次操作中需要改变的数据,通常是两个数,一个是-1,即P(等待)操作,  
	                    //一个是+1,即V(发送信号)操作。  
	    short sem_flg;//通常为SEM_UNDO,使操作系统跟踪信号,  
	                    //并在进程没有释放该信号量而终止时,操作系统释放信号量  
	};
(3)int semctl(int sem_id,int sem_num,int command,[union semun sem_union]);
	command:有两个值SETVAL,IPC_RMID,分别表示初始化和删除信号量。
	sem_union:可选参数,结构如下:
	union semun{  
	    int val; 
	    struct semid_ds *buf;  
	    unsigned short *arry;  
	}; 
	一般用到的是val,表示要传给信号量的初始值。
	
例子: 起两个进程进行操作
	#include<stdio.h>
	#include<stdlib.h>
	#include<sys/sem.h>
	union semun
	{
	    int val;
	    struct semid_ds *buf;
	    unsigned short *array;
	};
	int sem_id;
	int set_semvalue()
	{
	    union semun sem_union;    
	    sem_union.val = 1;
	    if(semctl(sem_id,0,SETVAL,sem_union)==-1)
	        return 0;
	    return 1;
	}
	int semaphore_p()
	{
	    struct sembuf sem_b;
	    sem_b.sem_num = 0;
	    sem_b.sem_op = -1;
	    sem_b.sem_flg = SEM_UNDO;
	    if(semop(sem_id,&sem_b,1)==-1)
	    {
	        fprintf(stderr,"semaphore_p failed\n");
	        return 0;
	    }
	    return 1;
	}
	int semaphore_v()
	{
	    struct sembuf sem_b;
	    sem_b.sem_num = 0;
	    sem_b.sem_op = 1;
	    sem_b.sem_flg = SEM_UNDO;
	    if(semop(sem_id,&sem_b,1)==-1)
	    {
	        fprintf(stderr,"semaphore_v failed\n");
	        return 0;
	    }
	    return 1;
	}
	void del_semvalue()
	{
	    //删除信号量
	    union semun sem_union;
	    if(semctl(sem_id,0,IPC_RMID,sem_union)==-1)
	        fprintf(stderr,"Failed to delete semaphore\n");
	}
	int main(int argc,char *argv[])
	{
	    char message = 'x';
	    //创建信号量
	     sem_id = semget((key_t)1234,1,0666|IPC_CREAT);
	    if(argc>1)
	    {
	        //初始化信号量
	        if(!set_semvalue())
	        {
	            fprintf(stderr,"init failed\n");
	            exit(EXIT_FAILURE);
	        }
	        //参数的第一个字符赋给message
	        message = argv[1][0];
	    }
	    int i=0;
	    for(i=0;i<5;i++)
	    {
	        //等待信号量
	        if(!semaphore_p())
	            exit(EXIT_FAILURE);
	        printf("%c",message);
	        fflush(stdout);
	        sleep(1);
	        //发送信号量
	        if(!semaphore_v())
	            exit(EXIT_FAILURE);
	        sleep(1);
	    }
	    printf("\n%d-finished\n",getpid());
	    if(argc>1)
	    {
	        //退出前删除信号量
	        del_semvalue();
	    }
	    exit(EXIT_SUCCESS);
	}

3.1.2、互斥量(Mutex)

	也叫互斥锁。和二元信号量类似。不同的是信号量在整个系统可以被任意线程获取并释放,也就是说,同一个信号量可以被系统中的一个线程获取之后由另一个线程释放。
而互斥量则要求谁获取谁释放,其他线程越俎代庖去释放是无效的。

使用到的函数有:
(1)int pthread_mutex_init (pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
	初始化一个mutex,如果attr为NULL测按默认值初始化,另外还可以在定义互斥量的时候按照下面的方式初始化一个互斥量:
	返回0表示成功
	
(2)int pthread_mutex_destroy (pthread_mutex_t *mutex);
	销毁一个 mutex ,只有空闲状态(没有被lock或者lock之后被unlock了)的mutex才可以被销毁,销毁一个被lock的信号量,将产生(16,Device or resource busy)错误;
	返回0表示成功

(3)int pthread_mutex_lock (pthread_mutex_t *mutex) 
	如果mutex指向的锁已经被锁定,那么当前调用锁定函数的线程将阻塞直到互斥锁被其他线程释放(阻塞线程按照优先级等待)。当pthread_mutex_lock返回时,说明互斥锁已经被当前线程成功锁定
	返回0表示加锁成功

(4)int pthread_mutex_trylock (pthread_mutex_t *mutex)
	该函数是pthread_mutex_lock的非阻塞版本。trylock在给一个互斥锁加锁时,如果互斥锁已经被锁定,那么函数将返回错误而不会阻塞线程
	返回0表示加锁成功
	
(5)int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime); 
	函数用于将mutex表示的互斥量锁住,如果该互斥量已经上锁,那么该函数会一直等到该互斥量解锁,等待时长为abstime指定的时间。 
	返回0表示加锁成功,其它表示加锁失败。
	struct timespec
	{
	    time_t  tv_sec;    /*second 秒*/
	    long    tv_nsec;   /*nanosecond 纳秒*/
	}
	struct timeval
	{
	    time_t      tv_sec;     /*seconds 秒*/
	    suseconds   tv_usec;    /*microseconds 微妙*/
	}
	
(6)int pthread_mutex_unlock(pthread_mutex_t *mutex); 
	函数用于将mutex表示的互斥量释放掉。 
	返回0表示成功

3.1.3、临界区

比互斥量更加严格的同步手段。临界区和互斥量、信号量的区别在于,互斥量、信号量在系统的任何进程里都是可见的,也就是说,一个进程创建了一个互斥量、信号量,另一个进程试图去获取该锁也是合法的。而临界区的作用范围仅限于本进程。
其实临界区就是本进程中的某一段代码,只是这段代码被加锁了。

3.1.4、读写锁

对于一段数据,多个线程同时读取总是没问题的。但是只要有一个去修改,就必须使用同步手段来避免出错。
使用信号量、互斥量、临界区来同步都可以,但是对于读取频繁,而仅仅偶尔写入的情况,会显得非常低效(每次读都要加锁解锁)。
读写锁可以避免这个问题。
读写锁有两种获取方式:共享的(Shared)和独占的(Exclusive)。
锁状态及线程以不同方式尝试获取的结果如下:
||读写锁状态 	||以共享方式获取	||以独占方式获取	||
|| 自由			||成功			||成功			||
|| 共享			||成功			||等待			||
|| 独占			||等待			||等待			||

用到的函数有:
(1)int pthread_rwlock_init ( pthread_rwlock_t *restrict rwlock , const pthread_rwlockattr_t *restrict attr ) ;
(2)int pthread_rwlock_destory ( pthread_rwlock_t *rwlock )
(3)pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
	在进行读操作的时候加的锁:
(4)pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
	在进行写操作的时候加的锁:	
(5)pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
	对读/写统一进行解锁:

3.1.5、条件变量

类似于一个栅栏。线程可以等待和唤醒条件变量,一个条件变量可以被多个线程等待,唤醒条件变量后,所有等待的线程都会恢复执行。
用到的函数有:使用方法类似互斥锁,貌似条件变量需要和互斥锁一起用,不然会有隐患
(1)int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
(2)int pthread_cond_destroy(pthread_cond_t *cond);
(3)int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
(4)int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abtime);
(5)int pthread_cond_signal(pthread_cond_t *cond);
(6)int pthread_cond_broadcast(pthread_cond_t *cond);

3.2、使用可重入函数

意思就是可以被多个线程同时调用的函数。可重入函数必须满足以下几点,不然就会存在隐患!!!
(1)不使用任何(局部)静态或全局的非const变量。若使用局部静态变量,局部静态变量并非私有,会受到其他线程调用影响。全局变量同理。至于const变量,本身就不会改变不用担心同步问题。
(2)不调用任何不可重入的函数。
(3)仅依赖于调用方提供的参数。
(4)不依赖任何单个资源的锁(mutex等)。

3.3、编译器过度优化造成的隐患

例1:
	有如下代码:
	x=0
	Thread1		Thread2
	lock();			lock();
	x++;				x++;
	unlock();		unlock();
		由于有lock保护,那么x的值似乎必然是2。但是,如果编译器为了提高x的访问速度,把x放到了某个寄存器里不及时写回变量,
	而不同线程的寄存器各自独立,因此如果Thread1先获得了锁,然后放到了寄存器中,然后++变成了1,但是为了访问速度并没有马上写会至变量x;
	此时Thread2获得了锁,x值还是0,然后++变成了1,然后写回了变量x=1;然后一段时间后Thread1将寄存器中的值1写回变量x,导致x经过两次++还是1。

例:2:
	有如下代码:
	x=y=0;
	Thread1		Thread2
	x = 1;			y = 1;
	r1 = y;			r2 = x;
		显然,r1、r2至少有一个为1,逻辑上不可能同时为0.然而事实上r1=r2=0的情况确实会发生。
	原因在于几十年前,CPU就发展出了动态调度,在执行程序的时候为了提高效率有可能交换指令的顺序。
	同样,编译器在进行优化的时候,也有可能为了效率交换毫不相干的两条执行的执行顺序。
	所以上面的代码执行的时候可能是这样的:
	x = y = 0;
	Thread1		Thread2
	r1 = y;			y = 1;
	x = 1;			r2 = x;
		我们可以使用关键字volatile阻止过度优化。volatile可以做到两件事情:
	1、阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回。
	2、阻止编译器调整volatile变量的指令顺序。但是不能阻止CPU动态调度换序。
例3:
	volatile T* p = 0;
	T* GetInstance()
	{
		if(p == NULL)
		{
			lock();
			if(p == NULL)
			{
				p = new T;
			}
			unlock();
		}
		return p;
	}
	乍一看,当函数返回时,p总是指向一个有效的对象。但是由于CPU的乱序执行,会有问题。
	p = new T;包含了三个步骤:
	(1)分配内存
	(2)在内存的位置上调用构造函数
	(3)将内存的地址赋值给p
	然而,(2)(3)的顺序可以颠倒,也就是说p的值不是NULL了,已经分配好内存了,但是还没有构造完毕,
	如果此时另一个线程对GetInstance并发调用,那么if判断p不是NULL,就返回了尚未构造完毕的对象的地址,此时使用的话会有隐患。
	要阻止CPU换序可以调用CPU提供的一条执行——barrier,它可以阻止CPU将barrier前的指令交换到barrier之后。
	许多体系结构的CPU都提供barrier指令,不过他们的名称各不相同,例如POWERPC提供的其中一条名叫lwsync。我们可以这样使用:
	#define barrier() __asm__ volatile ("lwsync")
	volatile T* p = 0;
	T* GetInstance()
	{
		if(p == NULL)
		{
			lock();
			if(p == NULL)
			{
				T* tmp = new T;
				barrier();
				p = tmp;
			}
			unlock();
		}
		return p;
	}

4、线程模型

线程实现机制:一开始的是LinuxThreads,
	后来有了IBM开发的NGPT(Next Generation POSIX Threads)
	再后来是NPTL(Native POSIX Threads Library)
LinuxThreads和NPTL都是采用一对一的线程模型,NGPT采用的是多对多的线程模型。
现在用的基本都是NPTL。

1、一对一模型:一个用户线程对应一个内核线程(内核调度实体)。
	优:此种模型可以实现真正的并发。
	缺:许多操作系统限制了内核线程的数量,导致用户线程数量受到限制;
		而且内核线程调度时,上下文切换开销较大,导致用户线程的执行效率下降。
2、多对一模型:多个用户线程对应一个内核线程(内核调度实体)。
	优:相对于一对一模型,线程切换要快速得多;
		并且用户线程数量几乎没有限制。
	缺:若一个用户线程阻塞,那么内核线程也阻塞了,导致其他用户线程无法执行;
		在多处理其系统上,处理器增多对这种模型的线程性能也不会有明显的帮助。
3、多对多模型:多个用户线程对多个内核线程(内核调度实体)。
	集上两个模型z的优点,去上两个模型的缺点。
发布了38 篇原创文章 · 获赞 17 · 访问量 4300

猜你喜欢

转载自blog.csdn.net/qq_14877637/article/details/88753491