嵌入式Linux入门—Linux多线程编程、互斥量、信号量、条件变量

目录

1. 认识线程

1.1 线程的概念

1.2 线程号tid

1.3 创建线程pthread_create()

1.4 线程的退出与回收

1.4.1 pthread_exit 主动退出线程

1.4.2 pthread_cancel 线程被动退出

1.4.3 pthread_join 线程资源回收(阻塞方式)

1.4.4 pthread_tryjoin_np 线程资源回收(非阻塞)

2. 互斥量

2.1 初始化互斥量pthread_mutex_init

2.2 互斥量加锁/解锁

2.3 互斥量加锁(非阻塞方式)

2.4 互斥量销毁

3. 信号量

3.1 初始化信号量sem_init

3.2 信号量申请与释放sem_wait/sem_post

3.3 信号量申请(非阻塞方式)sem_trywait

3.4 信号量销毁sem_destory

4. 条件变量

4.1 创建和销毁条件变量

4.2 等待条件变量pthread_cond_wait

4.3 通知条件变量pthread_cond_signal


嵌入式Linux入门完整教程:嵌入式Linux教程—裸机、应用、驱动完整教程目录

操作系统以进程为单位分配资源,各个进程相互独立,执行过程互不干扰。同一时间,操作系统可以运行多个进程,每个进程还可以同时执行多个任务,同一进程中,执行的每个任务都被视为一个线程。

1. 认识线程

一个进程中可以包含多个线程,所有线程共享进程拥有的资源。当然,每个线程也可以拥有自己的私有资源。

1.1 线程的概念

线程,是操作系统所能调度的最小单位。普通的进程,只有一个线程在执行对应的逻辑。我们可以通过多线程编程,使一个进程可以去执行多个不同的任务。相比多进程编程而言,线程享有共享资源,即在进程中出现的全局变量,每个线程都可以去访问它,与进程共享“ 4G”内存空间,使得系统资源消耗减少。

1.2 线程号tid

tid,其本质是一个pthread_t 类型的变量。线程号与进程号是表示线程和进程的唯一标识,但是对
于线程号而言,其仅仅在其所属的进程上下文中才有意义。

也就是说在不同进程里的两个线程tid是可能相等的。

可以用函数pthread_self(),获取当前线程的线程号:

#include <pthread.h>
#include <stdio.h>
int main()
{
    pthread_t tid = pthread_self();//获取主线程的 tid 号
    printf("tid = %lu\n",(unsigned long)tid);
    return 0;
}

注意: 因采用 POSIX 线程接口,故在要编译的时候包含 pthread 库,使用 gcc编译应 gcc xxx.c -lpthread 方可编译多线程程序

1.3 创建线程pthread_create()

函数原型:

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routi
ne) (void *), void *arg);

该函数一共四个参数:

第一个参数为 pthread_t 指针,用来保存新建线程的线程号

第二个参数表示了线程的属性,一般传入 NULL 表示默认属性;

第三个参数是一个函数指针,就是线程执行的函数。这个函数返回值为 void*,形参为 void*。

第四个参数则表示为向线程处理函数传入的参数,若不传入,可用 NULL 填充。void*型万能指针代表什么都能传。

#include <pthread.h>
#include <stdio.h>

void *thread_printf_tid(void *arg)
{
	pthread_t tid;
	tid=pthread_self();
	printf("sub_thread_tid=id:%lu\n",tid);
}


int main(int argc,char **argv)
{
	pthread_t tid;
	tid=pthread_self();
	printf("tid_main:%lu\n",tid);
	pthread_create(&tid,NULL,thread_printf_tid,NULL);
	sleep(1);
	return 0;
}

结果:

当主线程伴随进程结束时,所创建出来的线程也会立即结束,不会继续执行。并且创建出来的线程的执行顺序是随机竞争的,并不能保证哪一个线程会先运行。

因线程执行顺序随机,上面的程序不加 sleep 可能导致主线程先执行,导致进程结束,无法执行到子线程。

1.4 线程的退出与回收

线程的退出情况有三种

1.进程结束,进程中所有的线程也会随之结束。

2.通过函数 pthread_exit 来主动的退出线程。

3.被其他线程调用 pthread_cancel 来被动退出。

当线程结束后,主线程可以通过函数 pthread_join/pthread_tryjoin_np来回收线程的资源,并且获得线程结束后需要返回的数据

1.4.1 pthread_exit 主动退出线程

pthread_exit 函数原型如下:

#include <pthread.h>
void pthread_exit(void *retval);

pthread_exit 函数为线程退出函数,在退出时候可以传递一个 void*类型的数据带给主线程,若选择不传出数据,可将参数填充为 NULL。

1.4.2 pthread_cancel 线程被动退出

其他线程使用该函数让另一个线程退出,pthread_cancel 函数原型如下:

#include <pthread.h>
int pthread_cancel(pthread_t thread);

该函数传入一个 tid 号,会强制退出该 tid 所指向的线程,若成功执行会返回 0。

1.4.3 pthread_join 线程资源回收(阻塞方式)

pthread_join 函数原型如下:

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

该函数为线程回收函数,默认状态为阻塞状态,直到成功回收线程后才返回。第一个参数为要回收线程的 tid 号,第二个参数为线程回收后接受线程传出的数据.线程资源回收(非阻塞方式)。

1.4.4 pthread_tryjoin_np 线程资源回收(非阻塞)

函数原型如下:

#define _GNU_SOURCE
#include <pthread.h>
int pthread_tryjoin_np(pthread_t thread, void **retval);

该函数为非阻塞模式回收函数,通过返回值判断是否回收掉线程,成功回收则返回 0,其余参数与 pthread_join 一致。

注意:通过阻塞方式回收线程几乎规定了线程回收的顺序,若最先回收的线程未退出,则一直会被阻塞,导致后续先退出的线程无法及时的回收。通过函数 pthread_tryjoin_np,使用非阻塞回收,线程可以根据退出先后顺序自由的进行资源的回收。

2. 互斥量

当线程在运行过程中,去操作公共资源,如全局变量的时候,可能会发生彼此“矛盾”现象。

例如线程 1 企图想让变量自增,而线程 2 企图想要变量自减,两个线程存在互相竞争的关系导致变量永远处于一个“平衡状态”,两个线程互相竞争,线程 1 得到执行权后将变量自加,当线程 2 得到执行权后将变量自减,变量似乎永远在某个范围内浮动,无法到达期望数值。

为了解决上述对临界资源的竞争问题, pthread 线程引出了互斥锁来解决临界资源访问。通过对临界资源加锁来保护资源只被单个线程操作,待操作结束后解锁,其余线程才可获得操作权。

2.1 初始化互斥量pthread_mutex_init

函数原型如下:

int pthread_mutex_init(phtread_mutex_t *mutex,
const pthread_mutexattr_t *restrict attr);

该函数初始化一个互斥量,第一个参数是互斥量指针,第二个参数为控制互斥量的属性,一般为 NULL。当函数成功后会返回 0,代表初始化互斥量成功。

还可以可以调用宏来快速初始化,代码如下:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;

2.2 互斥量加锁/解锁

函数原型如下:

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

成功:返回 0

lock 函数与 unlock 函数分别为加锁解锁函数,只需要传入已经初始化好的pthread_mutex_t 互斥量指针。 成功后会返回 0。

当某一个线程获得了执行权后,执行 lock 函数,一旦加锁成功后,其余线程遇到 lock 函数时候会发生阻塞,直至获取资源的线程执行 unlock 函数后。unlock 函数会唤醒其他正在等待互斥量的线程。

特别注意的是,当获取 lock 之后,必须在逻辑处理结束后执行 unlock,否则会发生死锁现象!导致其余线程一直处于阻塞状态,无法执行下去。在使用互斥量的时候,尤其要注意使用 pthread_cancel 函数,防止发生死锁现象!

2.3 互斥量加锁(非阻塞方式)

函数原型如下:

#include <pthread.h>
int pthread_mutex_trylock(pthread_mutex_t *mutex);

该函数同样也是一个线程加锁函数,但该函数是非阻塞模式通过返回值来判断是否加锁成功,用法与上述阻塞加锁函数一致。

2.4 互斥量销毁

函数原型如下:

#include <pthread.h>
int pthread_mutex_destory(pthread_mutex_t *mutex);

成功:返回 0

该函数是用于销毁互斥量的,传入互斥量的指针,就可以完成互斥量的销毁,成功返回 0。

3. 信号量

我们在1.3中提到:线程执行顺序随机,信号量就是用来使得线程执行具有一定顺序。

互斥量用来防止多个线程同时访问某个临界资源。信号量起通知作用,线程 A 在等待某件事,线程 B 完成了这件事后就可以给线程 A 发信号。

3.1 初始化信号量sem_init

函数原型如下:

int sem_init(sem_t *sem,int pshared,unsigned int value);

第一个参数传入 sem_t 类型指针;
 第二个参数传入 0 代表线程控制,否则为进程控制;
 第三个参数表示信号量的初始值, 0 代表阻塞, 1 代表运行。
待初始化结束信号量后,若执行成功会返回 0。

3.2 信号量申请与释放sem_wait/sem_post

函数原型如下:

#include <pthread.h>
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);

成功:返回 0
sem_wait 函数作用为检测指定信号量是否有资源可用,若无资源可用会阻塞等待,若有资源可用会自动的执行“ sem-1”的操作。所谓的“ sem-1”是与上述初始化函数中第三个参数值一致,成功执行会返回 0。

sem_post 函数会释放指定信号量的资源,执行“ sem+1”操作。

通过以上 2 个函数可以完成所谓的 PV 操作,即信号量的申请与释放,完成对线程执行顺序的控制。

3.3 信号量申请(非阻塞方式)sem_trywait

函数原型如下:

#include <pthread.h>
int sem_trywait(sem_t *sem);

成功:返回 0

此函数是信号量申请资源的非阻塞函数,功能与 sem_wait 一致,唯一区别在于此函数为非阻塞。

3.4 信号量销毁sem_destory

函数原型如下:

#include <pthread.h>
int sem_destory(sem_t *sem);

成功:返回 0

该函数为信号量销毁函数,执行过后可将信号量进行销毁。
 

4. 条件变量

条件变量是线程的一种同步机制,条件变量与互斥量一起使用,适用于多个线程之间存在某种依赖关系,只有满足某种条件时,某个线程才可以使用的情景。

与互斥锁不同,条件变量是用来等待而不是用来上锁的。条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。

条件变量使我们可以睡眠等待某种条件出现,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。

条件的检测是在互斥锁的保护下进行的。如果一个条件为假,一个线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程,重新获得互斥锁,重新评价条件。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步。

有一个IO请求队列,入队线程不断的往队列里面push_back请求,出队线程不断的从队列里面pop_front请求。入队线程在push_back的时候要独占队列,出队线程在pop_front的时候也要独占队列。如果在某一时刻,入队线程抢到了互斥量,但是发现队列是满的,自己不断的轮询查询队列是否非满状态这样很消耗CPU资源,也导致无意义的占用锁资源。出队线程由于拿不到互斥量,一直阻塞。这样会导致程序僵死或者时间消耗变大。换句话说就是当线程拿到锁之后,如果发现不满足自己的执行条件就应该立即释放锁,并阻塞在当前位置,等待满足自己的执行条件时,通过某种途径来唤醒它继续运行。这时我们引入条件变量来解决这种场景。

4.1 创建和销毁条件变量

函数原型如下:

#include <pthread.h>
// 初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);//cond_at
tr 通常为 NULL
// 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);

这些函数成功时都返回0。

4.2 等待条件变量pthread_cond_wait

函数原型如下:

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

这需要结合互斥量一起使用,示例代码如下:

pthread_mutex_lock(&g_tMutex);
// 如果条件不满足则,会 unlock g_tMutex
// 条件满足后被唤醒,会 lock g_tMutex
pthread_cond_wait(&g_tConVar, &g_tMutex);
/* 操作临界资源 */
pthread_mutex_unlock(&g_tMutex);

4.3 通知条件变量pthread_cond_signal

函数原型如下:

int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_signal 函数只会唤醒一个等待 cond 条件变量的线程,示例代码如下:

pthread_cond_signal(&g_tConVar);


 

猜你喜欢

转载自blog.csdn.net/freestep96/article/details/126821594