线程总结
- 1 线程的实现
- 线程创建
- 线程退出
- 线程等待
- 线程清理
- 2 线程的属性
- 线程的分离
- 线程的栈地址
- 线程栈大小
- 线程的调度策略
- 线程优先级
- 3 线程的同步
- 互斥锁
- 读写锁
- 条件变量
- 信号量
线程是系统独立调度和分配的基本单位。同一进程中的多个线程将共享该进程中的全部系统资源,例如文件描述符和信号处理等。一个进程可以有很多线程,每个线程并发执行不同的任务。
1 线程的实现
线程创建
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
- thread:指向线程标识符的指针,当线程创建成功后,用来返回创建的线程ID
- att:指定线程的属性,NULL表示默认
- start_routine:函数指针,指向线程创建后要调用的函数,直接赋值函数名即可
- arg是传给函数的参数
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>void * my_pthread(void *arg)
{printf("pthread id =%ld\n",pthread_self());return NULL;
}int main(void)
{pthread_t pid;if(pthread_create(&pid,NULL,my_pthread,NULL)){printf("pthread_create error\n");exit(1);}sleep(2);return 0;
}
线程退出
void pthread_exit(void *retval);
- retval是线程结束时的返回值,可由其他函数如pthread_join来获取
注意:如果进程中任何一个线程调用exit()或_exit()函数,那么整个进程就会终止。线程的正常退出方法有线程从线程函数中返回(return)、线程可以被另一个线程终止以及线程自己调用pthread_exit()
线程等待
在调用pthread_create函数后,就会运行相关的线程函数。pthread_join()是一个线程阻塞函数,调用后,调用者一直等待指定的线程结束才返回,被等待线程的资源就会被回收。
int pthread_join(pthread_t thread, void **retval);
- pthread是等待结束的线程id
- retval是用户定义的指针,用来存储被等待线程结束时的返回值。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>void * my_pthread(void *arg)
{int ret =5;printf("pthread id =%ld\n",pthread_self());pthread_exit((void *)&ret);
}int main(void)
{pthread_t pid;void *ret ;if(pthread_create(&pid,NULL,my_pthread,NULL)){printf("pthread_create error\n");exit(1);}pthread_join(pid,&ret);printf("ret = %d\n",*(int * )ret);return 0;
}
线程函数运行结束是可以有返回值的,这个函数的返回值怎么返回呢?可以通过return语句进行返回,也可以通过pthread_exit()函数进行返回。函数的这个返回值怎么来接收呢?就通过pthread_join()函数来接受。
当然也可以选择不接受该线程的返回值,只阻塞该线程:
pthread_join(tid, NULL);
线程清理
线程终止有两种情况:正常终止和非正常终止。线程主动调用pthread_exit()或者从线程中return都使进程正常退出。非正常终止时线程在其他线程的干预下停止运行或者由于自身运行错误而退出。
线程可以安排它退出时需要调用的函数,这与进程在退出时可以用atexit函数安排退出函数是类似的。这样的函数称为线程清理处理程序。一个线程可以建立多个清理处理程序。
void pthread_cleanup_push(void (* rtn)(void *), void * arg);
函数说明:将清除函数压入清除栈。rtn是清除函数,arg是清除函数的参数。
void pthread_cleanup_pop(int execute);
函数说明:将清除函数弹出清除栈。执行到pthread_cleanup_pop()时,参数execute决定是否在弹出清除函数的同时执行该函数,execute非0时,执行;execute为0时,不执行。
从pthread_cleanup_push的调用点到pthread_cleanip_pop之间的程序段中的终止动作(包括调用pthread_exit()和异常终止,不包括return)都将执行pthread_cleanup_push()所指定的清理函数。
int pthread_cancel(pthread_t thread);
函数说明:取消线程,该函数在其他线程中调用,用来强行杀死指定的线程。
2 线程的属性
参考文章:
- https://blog.csdn.net/zsf8701/article/details/7842392
- https://blog.csdn.net/qq_22847457/article/details/89461222
- https://blog.csdn.net/yychuyu/article/details/84503261?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_antiscanv2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_antiscanv2&utm_relevant_index=2
线程属性标识符:pthread_attr_t 包含在 pthread.h 头文件中。
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;
属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。
线程的分离
分离状态属性确定使用线程属性对象 attr 创建的线程是在可连接状态还是可分离状态下创建。
如果在创建线程时就知道不需要了解线程的终止状态,就可以修改pthread_attr_t结构中的detachstate线程属性,让线程一开始就处于分离状态。可以使用pthread_attr_setdetachstate函数把线程属性detachstate设置成以下两个合法值之一:
PTHREAD_CREATE_DETACHED
PTHREAD_CREATE_JOINABLE
/* 设置线程的分离状态 */int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);/* 获取线程的分离状态 */ int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
注意事项:如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,
它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。
解决方法:要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timedwait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。
线程的栈地址
POSIX.1定义了两个常量_POSIX_THREAD_ATTR_STACKADDR 和_POSIX_THREAD_ATTR_STACKSIZE检测系统是否支持栈属性。也可以给sysconf函数传递_SC_THREAD_ATTR_STACKADDR或 _SC_THREAD_ATTR_STACKSIZE来进行检测。
当进程栈地址空间不够用时,指定新建线程使用由malloc分配的空间作为自己的栈空间。通过pthread_attr_setstack和pthread_attr_getstack两个函数分别设置和获取线程的栈地址。
int pthread_attr_setstack(pthread_attr_t *attr,void *stackaddr, size_t stacksize);int pthread_attr_getstack(const pthread_attr_t *attr,void **stackaddr, size_t *stacksize);
线程栈大小
当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用。当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。
函数pthread_attr_getstacksize和 pthread_attr_setstacksize可以设置或者获取线程的栈大小。
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
线程的调度策略
POSIX标准指定了三种调度策略:先入先出策略 (SCHED_FIFO)、循环策略 (SCHED_RR) 和自定义策略 (SCHED_OTHER)。SCHED_FIFO 是基于队列的调度程序,对于每个优先级都会使用不同的队列。SCHED_RR 与 FIFO 相似,不同的是前者的每个线程都有一个执行时间配额。SCHED_FIFO 和 SCHED_RR 是对 POSIX Realtime 的扩展。SCHED_OTHER 是缺省的调度策略。
- 新线程默认使用 SCHED_OTHER 调度策略。线程一旦开始运行,直到被抢占或者直到线程阻塞或停止为止。
- SCHED_FIFO
如果调用进程具有有效的用户 ID ,则争用范围为系统 (PTHREAD_SCOPE_SYSTEM) 的先入先出线程属于实时 (RT) 调度类。如果这些线程未被优先级更高的线程抢占,则会继续处理该线程,直到该线程放弃或阻塞为止。对于具有进程争用范围 (PTHREAD_SCOPE_PROCESS)) 的线程或其调用进程没有有效用户 ID 的线程,请使用 SCHED_FIFO,SCHED_FIFO 基于 TS 调度类。 - SCHED_RR
如果调用进程具有有效的用户 ID ,则争用范围为系统 (PTHREAD_SCOPE_SYSTEM)) 的循环线程属于实时 (RT) 调度类。如果这些线程未被优先级更高的线程抢占,并且这些线程没有放弃或阻塞,则在系统确定的时间段内将一直执行这些线程。对于具有进程争用范围 (PTHREAD_SCOPE_PROCESS) 的线程,请使用 SCHED_RR(基于 TS 调度类)。此外,这些线程的调用进程没有有效的用户 ID 。
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);
参数policy可以为SCHED_FIFO, SCHED_RR, and SCHED_OTHER
线程优先级
线程优先级存放在结构sched_param中,
struct sched_param {int sched_priority; /* Scheduling priority */};
可以通过pthread_attr_setschedparam()和pthread_attr_getschedparam设置和获取线程的调度优先级 ,它的完整定义是:
int pthread_attr_setschedparam(pthread_attr_t *attr,const struct sched_param *param);int pthread_attr_getschedparam(const pthread_attr_t *attr,struct sched_param *param);
3 线程的同步
参考文章
https://blog.csdn.net/qq_43412060/article/details/106989170
https://www.cnblogs.com/wsw-seu/p/8036218.html
互斥锁
互斥锁用pthread_mutex_t数据类型表示,互斥锁可以用来控制线程对共享资源的互斥访问,确保同一时间只有一个线程访问数据 。
/* 初始化互斥锁 */
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
/* 销毁互斥锁 */
int pthread_mutex_destroy(pthread_mutex_t *mutex);
/* 对互斥锁加锁 */
int pthread_mutex_lock(pthread_mutex_t *mutex);
/* 对互斥锁尝试加锁 */
int pthread_mutex_trylock(pthread_mutex_t *mutex);
/* 对互斥锁解锁 */
int pthread_mutex_unlock(pthread_mutex_t *mutex);
接下来我们来实现主线程负责接收用户输入,函数线程负责将用户输入打印到终端界面
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>
#include<unistd.h>#include<pthread.h>
#include<time.h>
#include<fcntl.h>pthread_mutex_t mutex;char buff[128] = {0};void *fun(void *arg)
{while(1){pthread_mutex_lock(&mutex);if(strncmp(buff,"end",3) == 0){break;}printf("fun :%s\n",buff);memset(buff,0,128);int n = rand() % 3 +1;sleep(n);pthread_mutex_unlock(&mutex);n = rand() % 3 + 1;sleep(n);}
}int main()
{srand((unsigned int)(time(NULL) * time(NULL)));pthread_mutex_init(&mutex,NULL);//初始化的锁是解锁状态的//创建一个线程pthread_t id;int res = pthread_create(&id,NULL,fun,NULL);assert(res == 0);while(1){pthread_mutex_lock(&mutex);printf("input:");fgets(buff,127,stdin);pthread_mutex_unlock(&mutex);if(strncmp(buff,"end",3) == 0){break;}int n = rand()%3 + 1;sleep(n);}//等待函数线程的结束pthread_join(id,NULL);pthread_mutex_destroy(&mutex);exit(0);
}
读写锁
读写锁是更高级的互斥锁,有更高的并行性。互斥锁只允许一个线程对临界区访问,而读写锁可以让多个读者并发访问。
读写锁可以多个读者读,但只允许一个写者写。
- 如果一个线程用读锁锁定了临界区,那么其他线程也可以用读锁来进入临界区,这样就可以多个线程并行操作。但这个时候,如果再进行写锁加锁就会发生阻塞,写锁请求阻塞后,后面如果继续有读锁来请求,这些后来的读锁都会被阻塞!这样避免了读锁长期占用资源,防止写锁饥饿!
- 如果一个线程用写锁锁住了临界区,那么其他线程不管是读锁还是写锁都会发生阻塞!
/* 读写锁的销毁 */
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
/* 读写锁的初始化 */
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;/* 读写锁加锁解锁 */
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
我们使用读写锁来实现两个线程读写一段数据
主线程写入时加写锁,两个函数线程读取时加读锁,这样两个函数线程可以同时读取,
# include<unistd.h>
#include <stdlib.h>
# include<stdio.h>
# include<string.h>
# include<time.h>
# include<assert.h>
# include<pthread.h>pthread_rwlock_t rwlock;//创建读写锁
char buff[128]={0};void* fun(void* arg)//读数据
{while(1){pthread_rwlock_rdlock(&rwlock);if(strncmp(buff,"end",3)==0){break;}printf("fun:%s\n",buff);//memset(buff,0,128);int n=rand()%3+1;sleep(n);pthread_rwlock_unlock(&rwlock);n=rand()%3+1;sleep(1);}
}
void* fun1(void* arg)//读数据
{while(1){pthread_rwlock_rdlock(&rwlock);if(strncmp(buff,"end",3)==0){break;}printf("fun1:%s\n",buff);//memset(buff,0,128);int n=rand()%3+1;sleep(n);pthread_rwlock_unlock(&rwlock);n=rand()%3+1;sleep(1);}
}
int main()
{srand((unsigned int)(time(NULL)*time(NULL)));pthread_rwlock_init(&rwlock,NULL);//初始化pthread_t id[2];int res=pthread_create(&id[0],NULL,fun,NULL);//创建线程int r=pthread_create(&id[1],NULL,fun1,NULL);assert(res==0);while(1)//写数据{pthread_rwlock_wrlock(&rwlock);printf("input:");fgets(buff,127,stdin);pthread_rwlock_unlock(&rwlock);if(strncmp(buff,"end",3)==0){break;}int n=rand()%3+1;sleep(1);}pthread_join(id[0],NULL);pthread_join(id[1],NULL);pthread_rwlock_destroy(&rwlock);
}
条件变量
条件变量是利用线程间共享全局变量进行同步的一种机制。一个线程修改条件,另一个线程等待条件,一旦等到自己需要的条件,就去运行。条件变量用pthread_cond_t类型的实例表示。
信号量
这个信号量和进程间用的信号量作用类似,当线程访问一些有限的公共资源时,就必须做到线程间同步访问。其实就类似于一个计数器,有一个初始值用于记录临界资源的个数。信号量由sem_t的实例表示。