线程
# 线程概念
进程:独立地址空间,拥有PCB 线程:也有PCB,但没有独立的地址空间(共享) 区别:在于是否共享地址空间。 独居(进程);合租(线程)。 Linux下: 线程:最小的执行单位 进程:最小分配资源单位,可看成是只有一个线程的进程。
- 线程共享资源:文件描述符表、内存地址空间(.text/.data/.bss/堆/环境变量/动态库/命令行参数)
- 线程非共享资源:线程id、栈(如果有5个线程,栈区将被平均分为5份)
察看LWP号:ps –Lf pid 查看指定线程的lwp号(线程号-给内核看的)。
# 函数
# 线程创建函数 pthread_create
//第一个参数为指向线程标识符的指针,传递一个pthread_t变量地址进来,用于保存新线程的tid(线程ID),可这样定义pthread_t thread1;
//第二个参数用来设置线程属性,如使用默认属性,则传NULL;
//第三个参数是线程运行函数的地址,即函数指针,指向新线程应该加载执行的函数模块;
//最后一个参数是运行函数的参数。
//若成功则返回0,否则返回出错编号。
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
# 获取线程ID函数 pthread_self
//获取线程ID。其作用对应进程中 getpid() 函数。
pthread_t pthread_self(void); 返回值:成功:0; 失败:无!
// 线程ID:pthread_t类型,本质:在Linux下为无符号整数(%lu),其他系统中可能是结构体实现
// 线程ID是进程内部,识别标志。(两个进程间,线程ID允许相同)
// 注意:不应使用全局变量 pthread_t tid,在子线程中通过pthread_create传出参数来获取线程ID,而应使用pthread_self。
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h> //strerror
void *tfn(void *arg)
{
printf("I'm thread, Thread_ID = %lu\n", pthread_self());
return NULL;
}
int main(void)
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, tfn, NULL);
if(ret != 0)
{
printf("thread error : %s\n",strerror(ret)); //strerror打印错误信息
}
sleep(1);
printf("I am main, my pid = %d\n", getpid());
return 0;
}
# 线程退出函数 pthread_exit
//参数: 线程退出时传递出的参数,可以是退出值或地址。如果线程不需要返回任何数据,将 retval 参数置为 NULL 即可。
//pthread_exit() 函数只会终止当前线程,不会影响进程中其它线程的执行。
void pthread_exit(void *retval);
# 阻塞等待线程退出 pthread_join
//以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回,如果线程已经结束,那么该函数会立即返回,并且指定的线程必须是joinable的。
//第二个参数为用户定义的指针,用来存储被等待线程的返回值(接收退出线程传递出的返回值)
//如果执行成功将返回0,失败则返回一个错误号。
int pthread_join(pthread_t thread, void **retval);//本质是回收子线程的PCB
//如果thread线程通过return返回,retval所指向的单元里存放的是thread线程函数的返回值;如果thread线程是被别的线程调用pthread_cancel异常终止掉,retval所指向的单元里存放的是常数PTHREAD_CANCELED;如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数;如果对thread线程的终止状态不感兴趣,可以传NULL给retval参数。
#include <stdio.h>
#include <pthread.h>
//线程要执行的函数,arg 用来接收线程传递过来的数据
void *ThreadFun(void *arg)
{
//终止线程的执行,将“http://c.biancheng.net”返回
pthread_exit("http://c.biancheng.net"); //返回的字符串存储在常量区,并非当前线程的私有资源
printf("*****************");//此语句不会被线程执行
}
int main()
{
int res;
//创建一个空指针
void * thread_result;
//定义一个表示线程的变量
pthread_t myThread;
res = pthread_create(&myThread, NULL, ThreadFun, NULL);
if (res != 0) {
printf("线程创建失败");
return 0;
}
//等待 myThread 线程执行完成,并用 thread_result 指针接收该线程的返回值
res = pthread_join(myThread, &thread_result);
if (res != 0) {
printf("等待线程失败");
}
printf("%s", (char*)thread_result);
return 0;
}
# 线程分离函数 pthread_detach
//成功:0;失败:错误号
int pthread_detach(pthread_t thread);
// 线程分离状态:指定该状态,线程主动与主控线程断开关系。线程结束后,其退出状态不由其他线程获取,而直接自己自动释放。网络、多线程服务器常用。
// 进程若有该机制,将不会产生僵尸进程。僵尸进程的产生主要由于进程死后,大部分资源被释放,一点残留资源仍存于系统中,导致内核认为该进程仍存在。
也可使用 pthread_create函数参2(线程属性)来设置线程分离。
【练习】:使用pthread_detach函数实现线程分离 【pthrd_detach.c】 一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL。如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
# 线程取消/杀死函数
//如果 pthread_cancel() 函数成功地发送了 Cancel 信号,返回数字 0;反之如果发送失败,函数返回值为非零数。
//对于接收 Cancel 信号后结束执行的目标线程,等同于该线程自己执行如下语句:pthread_exit(PTHREAD_CANCELED);
int pthread_cancel(pthread_t thread);
// 取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,pause,close,read,write.....
// 可粗略认为一个系统调用(进入内核)即为一个取消点。
// 被取消的线程,退出值,定义在Linux的pthread库中常数PTHREAD_CANCELED的值是-1。可以在头文件pthread.h中找到它的定义:#define PTHREAD_CANCELED ((void *) -1)
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h> // sleep() 函数
//线程执行的函数
void * thread_Fun(void * arg) {
printf("新建线程开始执行\n");
sleep(10);
}
int main()
{
pthread_t myThread;
void * mess;
int value;
int res;
//创建 myThread 线程
res = pthread_create(&myThread, NULL, thread_Fun, NULL);
if (res != 0) {
printf("线程创建失败\n");
return 0;
}
sleep(1);
//向 myThread 线程发送 Cancel 信号
res = pthread_cancel(myThread);
if (res != 0) {
printf("终止 myThread 线程失败\n");
return 0;
}
//获取已终止线程的返回值
res = pthread_join(myThread, &mess);
if (res != 0) {
printf("等待线程失败\n");
return 0;
}
//如果线程被强制终止,其返回值为 PTHREAD_CANCELED
if (mess == PTHREAD_CANCELED) {
printf("myThread 线程被强制终止\n");
}
else {
printf("error\n");
}
return 0;
}
//新建线程开始执行
//myThread 线程被强制终止
# 线程属性
typedef struct
{
int etachstate; //线程的分离状态
int schedpolicy; //线程调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
} pthread_attr_t;
主要结构体成员: 1. 线程分离状态 2. 线程栈大小(默认平均分配) 3. 线程栈警戒缓冲区大小(位于栈末尾) 4. 线程栈最低地址 属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。之后须用pthread_attr_destroy函数来释放资源。 线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。
# 线程属性初始化
// 注意:应先初始化线程属性,再pthread_create创建线程
// 初始化线程属性
//成功:0;失败:错误号
int pthread_attr_init(pthread_attr_t *attr);
# 线程分离状态
// 设置线程属性,分离or非分离
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
// 获取程属性,分离or非分离
pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
// 参数: attr:已初始化的线程属性
// detachstate: PTHREAD_CREATE_DETACHED(分离线程)
// PTHREAD _CREATE_JOINABLE(非分离线程)
# 释放线程资源
// 销毁线程属性所占用的资源
//成功:0;失败:错误号
int pthread_attr_destroy(pthread_attr_t *attr);
# 线程同步
# 互斥量(互斥锁)
- 创建互斥锁
- 初始化互斥锁
- 加锁
- 尝试加锁
- 解锁
- 销毁互斥锁
pthread_mutex_t mtx; //这就定义了一个互斥锁,但如果想使用跟这个互斥锁还是不行,需要对它进行初始化操作
//第二个参数是NULL的话,互斥锁的属性会设置为默认属性
pthread_mutex_init(&mtx, NULL);
//还可以用下面的方式定义一个互斥锁
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
//如果这个锁此时正在被其他线程调用,函数调用会进入阻塞状态,直道拿到锁才会返回。
pthread_mutex_lock(&mtx);
//当请求的锁正在被占用的时候,不会进入阻塞状态,而是立刻返回,并返回一个错误代码EBUSY,意思是有其他线程正在使用这个锁。
int err = pthread_mutex_trylock(&mtx);
if(0 != err){
if(EBUSY == err){
//the mutex could not be acquired because it was already locked.
}
}
//阻塞调用,增加一个超时时间
//时间相关函数(需笔记) https://blog.csdn.net/weixin_44880138/article/details/102605681
//clock()是以毫秒为单位,要正确输出时间差需要把它换成秒,因此需要除以CLOCKS_PER_SEC。
//阻塞等待线程锁,等待1s,1s后如果还没拿到锁的话,返回一个错误代码ETIMEDOUT,意思是超时了
struct timespec abs_timeout;
abs_timeout.tv_sec = time(NULL)+1;
abs_timeout.tv_nsec = 0;
int err = pthread_mutex_timedlock(&mtx, &abs_timeout);
if(0 != err){
if(EBUSY == err){
//the mutex could not be acquired because it was alreadu locked.
}
}
//用完互斥锁,一定要释放,不然下一个想要获得这个锁的线程,就只能阻塞
pthread_mutex_unlock(&mtx);
//使用此函数销毁一个线程锁,线程锁的状态定义为“未定义”,对一个处于已初始化但未锁定状态的线程锁进行销毁是安全的。
pthread_mutex_destroy(&mtx);
# 读写锁
与互斥量类似,但读写锁允许更高的并行性。其特性为:写独占,读共享。
# 读写锁状态:
一把读写锁具备三种状态: 1. 读模式下加锁状态 (读锁) 2. 写模式下加锁状态 (写锁) 3. 不加锁状态
# 读写锁特性:
- 读写锁是“写模式加锁”时, 解锁前,所有对该锁加锁的线程都会被阻塞。
- 读写锁是“读模式加锁”时, 如果线程以读模式对其加锁会成功;如果线程以写模式加锁会阻塞。
- 读写锁是“读模式加锁”时, 既有试图以写模式加锁的线程,也有试图以读模式加锁的线程。那么读写锁会阻塞随后的读模式锁请求。优先满足写模式锁。读锁、写锁并行阻塞,写锁优先级高 读写锁也叫共享-独占锁。当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。写独占、读共享。 读写锁非常适合于对数据结构读的次数远大于写的情况。
# 主要应用函数:
pthread_rwlock_init函数
pthread_rwlock_destroy函数
pthread_rwlock_rdlock函数
pthread_rwlock_wrlock函数
pthread_rwlock_tryrdlock函数
pthread_rwlock_trywrlock函数
pthread_rwlock_unlock函数
以上7 个函数的返回值都是:成功返回0, 失败直接返回错误号。
//pthread_rwlock_t类型 用于定义一个读写锁变量。
pthread_rwlock_t rwlock;
//pthread_rwlock_init函数,初始化一把读写锁
//参2:attr表读写锁属性,通常使用默认属性,传NULL即可。
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
//pthread_rwlock_destroy函数,销毁一把读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//pthread_rwlock_rdlock函数,以读方式请求读写锁。(常简称为:请求读锁)
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//pthread_rwlock_wrlock函数,以写方式请求读写锁。(常简称为:请求写锁)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//pthread_rwlock_unlock函数,解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//pthread_rwlock_tryrdlock函数,非阻塞以读方式请求读写锁(非阻塞请求读锁)
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//pthread_rwlock_trywrlock函数,非阻塞以写方式请求读写锁(非阻塞请求写锁)
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
读写锁示例
看如下示例,同时有多个线程对同一全局数据读、写操作。
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
int counter;
pthread_rwlock_t rwlock;
/* 3个线程不定时写同一全局资源,5个线程不定时读同一全局资源 */
void *th_write(void *arg)
{
int t, i = (int)arg;
while (1) {
pthread_rwlock_wrlock(&rwlock);
t = counter;
usleep(1000);
printf("=======write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);
pthread_rwlock_unlock(&rwlock);
usleep(10000);
}
return NULL;
}
void *th_read(void *arg)
{
int i = (int)arg;
while (1) {
pthread_rwlock_rdlock(&rwlock);
printf("----------------------------read %d: %lu: %d\n", i, pthread_self(), counter);
pthread_rwlock_unlock(&rwlock);
usleep(2000);
}
return NULL;
}
int main(void)
{
int i;
pthread_t tid[8];
pthread_rwlock_init(&rwlock, NULL);
for (i = 0; i < 3; i++)
pthread_create(&tid[i], NULL, th_write, (void *)i);
for (i = 0; i < 5; i++)
pthread_create(&tid[i+3], NULL, th_read, (void *)i);
for (i = 0; i < 8; i++)
pthread_join(tid[i], NULL);
pthread_rwlock_destroy(&rwlock);
return 0;
}
# 条件变量
条件本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用。给多线程提供一个会合的场所。 互斥量:保护共享数据 条件变量:引起阻塞
# 主要应用函数:
//pthread_cond_t类型 用于定义条件变量
pthread_cond_t cond;
//pthread_cond_init函数,初始化一个条件变量
//参2:attr表条件变量属性,通常为默认值,传NULL即可
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
//也可以使用静态初始化的方法,初始化条件变量:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//pthread_cond_destroy函数,销毁一个条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
//pthread_cond_wait函数,阻塞等待一个条件变量,将已经上锁的mutex解锁,解除阻塞会对互斥锁加锁
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
// 函数作用:
// 1. 阻塞等待条件变量cond(参1)满足
// 2. 释放已掌握的互斥锁(解锁互斥量)相当于pthread_mutex_unlock(&mutex);
// 1.2.两步为一个原子操作。
// 3. 当被唤醒,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex);
//pthread_cond_timedwait函数,限时等待一个条件变量
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
// 参3:
// struct timespec {
// time_t tv_sec; /* seconds */ 秒
// long tv_nsec; /* nanosecondes*/ 纳秒
// }
// 形参abstime:绝对时间。
// 如:time(NULL)返回的就是绝对时间。而alarm(1)是相对时间,相对当前时间定时1秒钟。
// struct timespec t = {1, 0};
// sem_timedwait(&sem, &t); 这样只能定时到 1970年1月1日 00:00:01秒(早已经过去)
// 正确用法:
// time_t cur = time(NULL); 获取当前时间。
// struct timespec t; 定义timespec 结构体变量t
// t.tv_sec = cur+1; 定时1秒
// pthread_cond_timedwait (&cond, &t); 传参 参APUE.11.6线程同步条件变量小节
// 在讲解setitimer函数时我们还提到另外一种时间类型:
// struct timeval {
// time_t tv_sec; /* seconds */ 秒
// suseconds_t tv_usec; /* microseconds */ 微秒
// };
//pthread_cond_signal函数,唤醒至少一个阻塞在条件变量上的线程
int pthread_cond_signal(pthread_cond_t *cond);
//pthread_cond_broadcast函数,唤醒全部阻塞在条件变量上的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
生产者消费者条件变量模型 线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。两个线程同时操作一个共享资源(一般称之为汇聚),生产向其中添加产品,消费者从中消费掉产品。 看如下示例,使用条件变量模拟生产者、消费者问题:
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
struct msg {
struct msg *next;
int num;
};
struct msg *head;
pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *consumer(void *p)
{
struct msg *mp;
for (;;) {
pthread_mutex_lock(&lock);
while (head == NULL) { //头指针为空,说明没有节点 可以为if吗
pthread_cond_wait(&has_product, &lock);
}
mp = head;
head = mp->next; //模拟消费掉一个产品
pthread_mutex_unlock(&lock);
printf("-Consume ---%d\n", mp->num);
free(mp);
sleep(rand() % 5);
}
}
void *producer(void *p)
{
struct msg *mp;
while (1) {
mp = malloc(sizeof(struct msg));
mp->num = rand() % 1000 + 1; //模拟生产一个产品
printf("-Produce ---%d\n", mp->num);
pthread_mutex_lock(&lock);
mp->next = head;
head = mp;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&has_product); //将等待在该条件变量上的一个线程唤醒
sleep(rand() % 5);
}
}
int main(int argc, char *argv[])
{
pthread_t pid, cid;
srand(time(NULL));
pthread_create(&pid, NULL, producer, NULL);
pthread_create(&cid, NULL, consumer, NULL);
pthread_join(pid, NULL);
pthread_join(cid, NULL);
return 0;
}
条件变量的优点: 相较于mutex而言,条件变量可以减少竞争。 如直接使用mutex,除了生产者、消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果汇聚(链表)中没有数据,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争。提高了程序效率。
# 信号量
进化版的互斥锁(1 --> N) 由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。 信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。
# 主要应用函数:
由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住。这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与直接使用单进程无异。 信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。
//sem_t类型,本质仍是结构体。但应用期间可简单看作为整数,忽略实现细节(类似于使用文件描述符)。
sem_t sem; //规定信号量sem不能 < 0。头文件 <semaphore.h>
// 信号量基本操作:
// sem_wait: 1. 信号量大于0,则信号量-- (类比pthread_mutex_lock)
// | 2. 信号量等于0,造成线程阻塞
// 对应
// |
// sem_post: 将信号量++,同时唤醒阻塞在信号量上的线程 (类比pthread_mutex_unlock)
// 但,由于sem_t的实现对用户隐藏,所以所谓的++、--操作只能通过函数来实现,而不能直接++、--符号,信号量的初值,决定了占用信号量的线程的个数。
//sem_init函数,初始化一个信号量
// 参1:sem信号量
// 参2:pshared取0用于线程间;取非0用于进程间
// 参3:value指定信号量初值
int sem_init(sem_t *sem, int pshared, unsigned int value);
//sem_destroy函数,销毁一个信号量
int sem_destroy(sem_t *sem);
//sem_wait函数,给信号量加锁 -- ,如果sem为0,线程阻塞
int sem_wait(sem_t *sem);
//sem_post函数,给信号量解锁 ++
int sem_post(sem_t *sem);
//sem_trywait函数,尝试对信号量加锁 -- (与sem_wait的区别类比lock和trylock)
int sem_trywait(sem_t *sem);
//sem_timedwait函数,限时尝试对信号量加锁 --
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
// 参2:abs_timeout采用的是绝对时间。
// 定时1秒:
// time_t cur = time(NULL); 获取当前时间。
// struct timespec t; 定义timespec 结构体变量t
// t.tv_sec = cur+1; 定时1秒
// sem_timedwait(&sem, &t); 传参