C语言-多线程

C语言-多线程

为什么用多线程

在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程。现在,多为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。

启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。

通信机制对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。

线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

  1. 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
  2. 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
  3. 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

需要的头文件

#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);

锁操作:

  1. 加锁 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)
  2. 解锁 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提供纳秒级的精度

在这里插入图片描述

点赞 -收藏-关注-便于以后复习和收到最新内容
有其他问题在评论区讨论-或者私信我-收到会在第一时间回复
在本博客学习的技术不得以任何方式直接或者间接的从事违反中华人民共和国法律,内容仅供学习、交流与参考
免责声明:本文部分素材来源于网络,版权归原创者所有,如存在文章/图片/音视频等使用不当的情况,请随时私信联系我、以迅速采取适当措施,避免给双方造成不必要的经济损失。
感谢,配合,希望我的努力对你有帮助^_^

猜你喜欢

转载自blog.csdn.net/weixin_45203607/article/details/126805165