C语言-多线程
文章目录
为什么用多线程
在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程。现在,多为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。
启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
通信机制对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。
线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
- 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
- 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
- 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
需要的头文件
#include <pthread.h> //线程库
创建线程变量
pthread_t thread;
`用来定义一个线程类型的变量,创建线程和其他线程相关操作的时候需要使用
创建线程
pthread_create(&thread, NULL, print_b, NULL);
` 建立线程,它有4个参数
- 第一个参数为指向线程标识符的指针
- 第二个参数用来设置线程属性,
- 第三个参数是线程运行函数的起始地址,
- 最后一个参数是运行函数的参数。
如果我们的函数thread不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。
等待线程结束join
pthread_join(thread, &result);
用来等待一个线程的结束
- 第一个参数为被等待的线程标识符(线程变量)
- 第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。
结束线程
pthread_exit(void *res);
在线程内部使用
参数1线程退出的时候携带的数据,当前子线程的主线程会得到该数据,如果不需要那么指定为NULL
注意,res 指针不能指向函数内部的局部数据(比如局部变量)。换句话说,pthread_exit()函数不能返回一个指向局部数据的指针,否则很可能使程序运行结果出错甚至崩溃。
如果实际场景中想终止某个子线程,强烈建议大家使用 pthread_exit() 函数。终止线程,而不是return
获取当前线程引用
pthread_self()
在线程内拿到当前线程的pthread_t变量
线程属性结构体的创建与销毁
int pthread_attr_init(pthread_attr_t *attr);
初始化线程属性
int pthread_attr_destroy(pthread_attr_t *attr);
销毁线程属性
pthread_attr_init函数:
- 功能:用来对线程属性值进行初始化
- 注意:调用此函数之后,属性结构体的属性都是系统默认值,如果想要设置其他属性,还需要调用不同的函数进行设置
pthread_attr_destory函数:
- 功能:用来反初始化(销毁)属性。如果pthread_attr_init函数初始化的线程属性是动态分配的,那么此函数就会释放线程属性的内存空间
- pthread_attr_destory函数会用无效的值初始化属性对象,因此用此函数反初始化属性之后,属性就不能供pthread_create函数使用了
线程的分离
- 如果线程未分离:线程的终止状态会一直保存,直到另外一个线程对该线程调用pthread_join获取其终止状态,终止状态才会被释放
- 如果线程已经被分离:线程的底层存储资源可以在线程终止时立即被回收
在线程被分离后,我们不能用pthread_join函数等待它的终止状态,因为对分离状态的线程调用pthread_join会产生未定义行为
int pthread_detach(pthread_t tid);
线程分离函数
我们也可以设置在创建线程时就将线程设置为分离状态
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
用来设置pthread_attr_t结构的detachstate属性的值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N9pBR4Il-1662878377850)(https://note.youdao.com/yws/res/2/WEBRESOURCE4e359272465032852e2f56e102e41922)]
int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr, int *detachstate);
获得参数1pthread_attr_t结构中的detachstate值,并将值存放在参数2返回给调用者
线程私有变量
一个键可以被不同的线程所使用,但各个线程使用这个key时互不干扰,相当于每个线程都有自己的副本存储自己线程内的值
定义变量 pthread_key_t key;
初始化变量: pthread_key_create(&key, NULL);
设置变量: pthread_setspecific(key, 1);
变量值是void *value
也就是可以任意类型
使用变量: int len1 = pthread_getspecific(key);
因为设置的时候值类型是void *value
那么获取的时候需要转换对应的类型
删除变量: pthread_key_delete(key);
键防重复创建机制
pthread_key_t key;
int init_done=0;
int threadfunc(void* arg)
{
if(!init_done){
//防止多个线程重复创建
init_done=1;
err=pthread_key_create(&key,NULL);
}
......
}
创建键时避免出现冲突的一个正确方法如下:
pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;
void thread_init(void)
{
err=pthread_key_create(&key,NULL);
}
int threadfunc(void* arg)
{
// 如果每个线程都调用pthread_once,系统就能保证初始化key只被调用一次
pthread_once(&init_done,thread_init);
......
}
锁(互斥)
互斥从本质上说是一把锁,在访问共享资源前进行(加锁),在访问完成后释放(解锁),进行加锁以后,任何其他试图再次对加锁的线程都会被阻塞,直到当前线程释放该互斥锁
如果释放的任务有一个以上的线程阻塞,那么所有该锁上的阻塞线程都会变成可运行状态,第一个变为运行的线程就可以对任务加锁,其他线程就会看到任务还是锁着的,只能再次等待它重新变为可用(在这种方式下,每次只有一个线程可以向前执行)
定义锁变量: pthread_mutex_t lock;
定义锁的属性: pthread_mutexattr_t attr;
初始化锁的属性: pthread_mutexattr_init(&attr);
设置锁的作用范围: pthread_mutexattr_setpshared(&attr,PTHREAD_PROCESS_PRIVATE);
可以设置为PTHREAD_PROCESS_SHARE和PTHREAD_PROCESS_PRIVATE。默认是后者 ,前者表示进程和进程进行同步,后者表示进程内线程和线程进行同步
设置锁的互斥锁的类型: pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_RECURSIVE_NP);
有以下几个取值空间:
PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
初始化锁变量并且绑定锁的属性: pthread_mutex_init(&lock, &attr);
锁销毁: pthread_mutex_destroy(&lock);
锁属性销毁: pthread_mutexattr_destroy(&attr);
锁操作:
- 加锁
pthread_mutex_lock(pthread_mutex_t *m)
、pthread_mutex_trylock(pthread_mutex_t *m)
,pthread_mutex_timedlock(pthread_mutex_t *m, const struct timespec *ts)
- 解锁
pthread_mutex_unlock(pthread_mutex_t *m)
pthread_mutex_lock对一个 mutex 加锁。如果一个线程试图锁定一个已经被另一个线程锁定的互斥锁,那么该线程将被挂起,直到拥有该互斥锁的线程解锁。当然这是默认动作,我们可以通过定义锁的属性来改变锁的行为
pthread_mutex_trylock 只是当mutex已经是锁定的时候,其他没有拿到锁的线程直接返回错误码EBUSY,而不是阻塞进程。
pthread_mutex_timedlock也是加锁,但是只阻塞指定的时间,时间一到还没能获取锁的线程则返回错误码ETIMEDOUT。
死锁
什么情况下回产生死锁呢? 假设现在有2个互斥变量A和b ,那么线程1加锁A,线程2加锁B ,线程1在内部又对B进行加锁,而线程2内部又对A进行加锁,这时候就会相互吧对方锁死,线程1一直等待线程2释放,而线程2一直等待线程1进行释放
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QyCKLxY1-1662878377851)(https://note.youdao.com/yws/res/5/WEBRESOURCEf419355caa8548c898ed28b38ac3f355)]
简单来说: 比如一扇门,你要出我要进,你在等我让,我在等你让,这时就陷入了死循环,就形成了死锁。
避免产生死锁的方法: 注意加锁和解锁的顺序,或者采用pthread_mutex_timedlock或者pthread_mutex_trylock (最重要的还是锁的顺序一定要把控好)
读写锁
读写锁也称为共享互斥锁:
- 当读写锁是读模式锁住时,就可以说成是以共享模式锁住的
- 当它是写模式锁住时,就可以说成是以互斥模式锁住的
读写锁可以有3种状态:
- ①读模式下加锁
- ②写模式下加锁
- ③不加锁状态
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁
当读写锁是写加锁状态时:在这个锁被解锁之前,所有试图对这个锁加锁的线程都会阻塞(不论是读还是写)
当读写锁是读加锁状态时:所有试图以读模式对它进行加锁的线程都可以得到访问权。但是任何希望以写模式对此锁进行加锁的线程都会阻塞,直到所有的线程释放它们的读锁为止
当读写锁处于读模式锁住的状态,而这时有一个线程试图以写模式获取锁时,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足
读写锁的应用:读写锁非常适合于对数据读的次数大于写的情况
- 当读写锁在写模式下时:它所保护的数据结构就可以被安全的修改,因为一次只有一个线程可以在写模式下拥有这个锁
- 当读写锁在读模式下时:只要线程先获取了读模式下的读写锁,该锁所保护的数据结构就可以被多个获得读模式锁的线程读取
读写锁变量: pthread_rwlock_t rwlock
读写锁的初始化与释放:
// 静态初始化 ,静态初始化读写锁变量只能拥有默认的读写锁属性,不能设置其他读写锁属性
pthread_rwlock_t rwlock;
rwlock=PTHREAD_RWLOCK_INITIALIZER;
//或者
pthread_rwlock_t *rwlock=(pthread_rwlock_t *)malloc(sizeof(pthread_rwlock_t));
*rwlock=PTHREAD_MUTEX_INITIALIZER;
//因为静态初始化读写锁变量只能拥有默认读写锁属性,我们可以通过pthread_rwlock_init函数来动态初始化读写锁,并且可以在初始化时选择设置读写锁的属性
/*
对读写锁变量进行初始化
参数1:初始化的读写锁
参数2:读写锁初始化时的属性。如果用默认属性,此处填NULL
*/
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
/*
读写锁变量的反初始化,释放销毁
参数:读写锁变量
备注(重点):此函数并没有释放内存空间,如果读写锁变量是通过malloc等函数申请的,那么需要在free掉读写锁变量之前调用pthread_rwlock_destory函数
*/
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
演示:
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock,NULL);
/*do something*/
pthread_rwlock_destory(&rwlock);
pthread_rwlock_t* rwlock=(pthread_mutex_t*)malloc(sizeof(pthread_mutex_t));
pthread_rwlock_init(rwlock,NULL);
/*do something*/
pthread_rwlock_destory(rwlock);
free(rwlock);
读写锁-加锁与解锁函数
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 在读模式下锁定读写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); //在写模式下锁定读写锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); //不管以何种方式锁住读写锁,都可以用这个函数解锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); //尝试获得读模式的读写锁,如果可以获取返回0,不可以获取出错返回EBUSY
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); //尝试获得写模式的读写锁,如果可以获取返回0,不可以获取出错返回EBUSY
//如果时间到期超时时,我们不能获取锁,两个函数都会返回ETIMEOUT错误
int pthread_rwlock_timedrdlock(pthread_rwlock_t *rwlock, const struct timespec *ts)
int pthread_rwlock_timedwrlock(pthread_rwlock_t *rwlock, const struct timespec *ts)
读写锁属性 pthread_rwlockattr_t rwlockattr
读写锁支持的唯一属性就是进程共享属性,它与互斥量的进程共享属性是相同的,可以参考上面
读写锁属性结构体的初始化
// 对读写锁属性初始化,调用此函数之后,读写锁属性都是系统默认值,如果想要设置其他属性,还需要调用不同的函数进行设置
int pthread_rwlockattr_init(pthread_rwlockattr_t* attr);
//对读写锁属性销毁
int pthread_rwlockattr_destroy(pthread_rwlockattr_t* attr);
读写锁进程共享属性的设置与获取
//设置读写锁的进程共享属性 ,可以参考互斥锁属性设置,通用的
int pthread_rwlockattr_setshared(pthread_rwlockattr_t* attr,int pshared);
//获取读写锁的进程共享属性
int pthread_rwlockattr_getshared(const pthread_rwlockattr_t* restrict attr,int* restrict pshared);
条件变量
条件变量使我们可以睡眠等待某种条件出现。条件变量是利用线程间共享的全局变量,进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
静态初始化
pthread_cond_t cond;
cond=PTHREAD_COND_INITIALIZER;
//或者
pthread_cond_t *cond=(pthread_cond_t *)malloc(sizeof(pthread_cond_t));
*cond=PTHREAD_COND_INITIALIZER;
动态初始化
//因为静态初始化条件变量只能拥有默认条件变量属性,我们可以通过pthread_mutex_init函数来动态初始化条件变量,并且可以在初始化时选择设置条件变量的属性
/*
对条件变量初始化
参数1: 需要初始化的条件变量
参数2:初始化时条件变量的属性。如果使用默认属性,此处填NULL
*/
int pthread_cond_init(pthread_cond_t* restrict cond,const pthread_condattr_t* restrict attr);
/*
对条件变量反初始化(在条件变量释放内存之前)
参数:条件变量
此函数并没有释放内存空间,如果互斥量是通过malloc等函数申请的,那么需要在free掉互斥量之前调用pthread_mutex_destroy函数
*/
int pthread_cond_destroy(pthread_cond_t* cond);
演示:
pthread_cond_t cond;
pthread_cond_init(&cond,NULL);
/*do something*/
pthread_cond_destroy(&cond);
pthread_cond_t * cond=(pthread_cond_t *)malloc(sizeof(pthread_cond_t));
pthread_cond_init(cond,NULL);
/*do something*/
pthread_cond_destroy(cond);
free(cond);
等待条件变量函数
/*
等待条件变量变为真
参数mutex互斥量提前锁定,然后该互斥量对条件进行保护,等待参数1cond条件变量变为真。在等待条件变量变为真的过程中,此函数一直处于阻塞状态。但是处于阻塞状态的时候,mutex互斥量被解锁(因为其他线程需要使用到这个锁来使条件变量变为真)
当pthread_cond_wait函数返回时,互斥量再次被锁住
*/
int pthread_cond_wait (pthread_cond_t *cv, pthread_mutex_t *external_mutex)
/*
pthread_cond_timedwait函数与pthread_cond_wait函数功能相同。不过多了一个超时参数。超时值指定了我们愿意等待多长时间,它是通过timespec结构体表示的
如果超时到期之后,条件还是没有出现,此函数将重新获取互斥量,然后返回错误ETIMEOUT
*/
int pthread_cond_timedwait(pthread_cond_t *cv, pthread_mutex_t *external_mutex, const struct timespec *t);
这两个函数调用成功返回时,线程需要重新计算条件,因为另一个线程可能已经在运行并改变了条件
必须注意:pthread_cond_wait一定要在锁的环境下进行
pthread_mutex_lock(&qlock); //加锁
while (workq == NULL)
pthread_cond_wait(&qready, &qlock); //等待条件
mp = workq;
workq = mp->m_next;
pthread_mutex_unlock(&qlock); //解锁
条件变量信号发送函数
int pthread_cond_signal(pthread_cond_t* cond); //至少能唤醒一个等待该条件的线程
int pthread_cond_broadcast(pthread_cond_t* cond); //则唤醒等待该条件的所有线程
这两个函数用于通知线程条件变量已经满足条件(变为真)。在调用这两个函数时,是在给线程或者条件发信号
void enqueue_msg(struct msg *mp)
{
pthread_mutex_lock(&qlock);
mp->m_next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready); //发送状态,唤醒一个线程
}
条件变量属性
属性定义: pthread_condattr_t pthread_condattr;
//与互斥量的进程共享属性是相同的
//对条件变量属性结构体初始化
//调用此函数之后,条件变量属性结构体的属性都是系统默认值,如果想要设置其他属性,还需要调用不同的函数进行设置
int pthread_condattr_init(pthread_condattr_t* attr);
//对条件变量属性销毁
int pthread_condattr_destroy(pthread_condattr_t* attr);
进程共享属性的设置与获取
//设置条件变量的进程共享属性
int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);
//获取条件变量的进程共享属性
intpthread_condattr_getpshared(const pthread_condattr_t *attr, int *pshared)
超时值的绝对时间获取函数
#include <sys/time.h>
#include <stdlib.h>
void maketimeout(struct timespec *tsp, long minutes)
{
struct timeval now;
/* get the current time */
gettimeofday(&now, NULL);
tsp->tv_sec = now.tv_sec;
tsp->tv_nsec = now.tv_usec * 1000; /* usec to nsec */
/* add the offset to get timeout value */
tsp->tv_sec += minutes * 60;
}
屏障
屏障是用户协调多个线程并行工作的同步机制
工作原理:屏障允许每个线程等待,直到所有的合作线程都到达某一点(屏障),然后从该点继续执行工作
屏障变量: pthread_barrier_t pthread_barrier
屏障的初始化与释放
/*
对屏障变量进行初始化
参数1:初始化的屏障变量
参数2:屏障初始化时的属性。如果用默认属性,此处填NULL
参数3:用此参数指定,在允许所有线程继续执行之前,必须到达屏障的线程数目。当到达了这个数目之后就可以继续执行
*/
int pthread_barrier_init(pthread_barrier_t *restrict barrier,const pthread_barrierattr_t *restrict attr,unsigned int count);
/*
屏障变量的反初始化,释放销毁
参数:屏障变量
此函数并没有释放内存空间,如果屏障变量是通过malloc等函数申请的,那么需要在free掉读屏障变量之前调用pthread_barrier_destroy函数
*/
int pthread_barrier_destroy(pthread_barrier_t *barrier);
屏障下的等待
/*
线程调用该函数用来表示自己已经到达了屏障,如果线程调用这个函数发现屏障的线程计数还未满足要求,那么线程就会进入休眠状态。如果线程调用此函数之后,发现刚好满足屏障计数,那么所有的线程都被唤醒
*/
int pthread_barrier_wait(pthread_barrier_t *barrier);
屏障属性: pthread_barrierattr_t pthread_barrierattr
屏障属性结构体的初始化
/*
功能:对屏障属性结构体初始化
调用此函数之后,屏障属性结构体的属性都是系统默认值,如果想要设置其他属性,还需要调用不同的函数进行设置
*/
int pthread_barrierattr_init(pthread_barrierattr_t* attr);
/*
功能:对屏障属性反初始化(销毁)
只反初始化,不释放内存
*/
int pthread_barrierattr_destroy(pthread_barrierattr_t* attr);
进程共享属性的设置与获取
/*
功能:设置屏障的进程共享属性
进程共享属性的值可以是PTHREAD_PROCESS_SHARED(多进程中的多个线程可用),也可以是PTHREAD_PROCESS_PRIVATE(只有初始化屏障的那个进程内的多个线程可用)
*/
int pthread_barrierattr_setshared(pthread_barrierattr_t* attr,int pshared);
/*
功能:获取屏障的进程共享属性
*/
int pthread_barrierattr_getshared(const pthread_barrierattr_t* attr,int* pshared);
休眠函数
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
sleep函数使进程被挂起,休眠参数所指定的秒数 ,直到满足下面两个条件之一才返回:
①已经过了参数seconds所指定的时间。此种情况函数返回0
②调用进程捕捉到一个信号并从信号处理程序返回。此种情况函数返回sleep未休眠完的秒数
#include <unistd.h>
int usleep(useconds_t usec);
和sleep函数一样只是usleep函数休眠参数所指定的微秒
#include <time.h>
int nanosleep(const struct timespec *reqtp, struct timespec *remtp);
与sleep功能相似,nanosleep提供纳秒级的精度