linux系统编程——线程学习

本文深入探讨线程的基本概念,包括线程ID、线程的创建与退出,以及线程间的同步机制如互斥量和条件变量的使用。通过实例代码展示了线程的创建、互斥量的保护和条件变量的唤醒过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

~线程基本概念:
线程是程序中完成一个独立任务的完整执行序列,即一个可调度的实体;进程相当于运行中程序的一种抽象。根据运行环境的调度者的身份,线程可分为内核线程和用户线程。内核线程,在有的系统上称为LWP(Light Weight Process,轻量级线程),运行在内核空间,由内核调度;用户线程运行在用户空间,由线程库来调度。当进程的一个内核线程获得CPU的使用权时,它就加载并运行一个用户线程。可见,内核线程相当于用户线程运行的‘容器’,一个进程可以拥有M个内核线程和N个用户线程,其中M<=N,并且一个系统的所有进程中,M和N的比值是固定的。典型的linux进程可以看成是只有一个控制线程,一个进程同时只能做一件事情,但是多个控制线程就可以做多件事情(争夺cpu资源)。
~线程ID:就像每个进程有自己的pid一样,线程也有相应的标识符,但是只能在某一个进程上下文起作用;

线程编程的API函数详解:
<1>线程的创建退出等函数。

#include <pthread.h>
int pthread_create(pthread_t * tidp, const pthread_attr_t *attr, void *(*start_rtn)(void *), void *arg); // 返回:成功返回0,出错返回错误编号
//参数1:当pthread_create函数返回成功时,有tidp指向的内存被设置为新创建线程的线程ID,其类型pthread_t定义为:
//参数2:attr参数用于设置各种不同的线程属性,为NULL时表示默认线程属性。新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针的参数arg,如果需要向start_rtn函数传入的参数不止一个,可以把参数放入到一个结构中,然后把这个结构的地址作为arg的参数传入。
//注: 线程创建时并不能保证哪个线程会先运行:是新创建的线程还是调用线程。新创建的线程可以访问调用进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,但是该线程的未决信号集被清除。那什么是未决信号呢,信号产生到信号被处理这段时间间隔,称信号是未决的。
void pthread_exit(void *rval_ptr); // 线程终止参数 返回一个线程函数里面的变量值pthread_exit((void*)test)
//线程在结束时最好调用该函数,以确保安全、干净的退出。pthread_exit函数通过rval_ptr参数向调用线程的回收者传递退出信息,进程中的其他线程可以调用pthread_join函数访问到这个指针。pthread_exit执行完后不会返回到调用者,而且永远不会失败。
int pthread_join(pthread_t thread, void **rval_ptr);
// 返回:成功返回0,出错返回错误代码
//注:thread是目标线程标识符,rval_ptr指向目标线程返回时的退出信息,该函数会一直阻塞,直到被回收的线程结束为止。
int pthread_cancel(pthread_t thread);
    // 返回:成功返回0,出错返回错误代码
  //默认情况下,pthread_cancel函数会使有thread标识的线程的表现为如同调用了参数为PTHREAD_CANCEL的pthread_exit函数,但是,接收到取消请求的目标线程可以决定是否允许被取消以及如何取消,这分别由以下两个函数来控制:
int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldstate);
注: 注意pthread_cancel并不等待线程结束,它只是提出请求。

测试代码如下:
实现:创建一个线程,用来实现返回字符串并且main输出

#include <stdio.h>

void *function1(void *argc)
{
	static char ret[20] = "hello world";//可以返回字符串join函数接收
	
	printf("fun1:%ld thread is creat\n",(unsigned long)pthread_self);//打印出线程id
	printf("fun1:param = %d\n",*((int*)argc));//打印出线程输入的参数

	pthread_exit((void*)ret);//退出线程并且返回ret变量值
}

int main()
{
	int ret ;
	int param = 100;//作为函数输入参数
	pthread_t t1;//用来存放线程id的值
	int *pret;//存放线程exit返回的值

	ret = pthread_create(&t1,NULL,func1,(void *)&param);//创建线程
	if(ret == 0)//成功返回0
	{
		printf("main:creat successful!\n");
	}
	
	pthread_join(t1,(void **)&pret);//接收exit函数返回的指针值,阻塞主线程等待创建线程安全退出

	printf("main:ret = %d\n",pret);//
	return 0;
}

<2>线程互斥量函数
~临界区:临界区是必须以互斥方式执行的代码段,也就是说在临界区的范围内只能有一个活动的执行线程,在计算机系统中有许多共享资源不允许用户并行使用。例如打印机,如果它同时进行两份文档打印,它的输出就会产生交错,从而都无法获得正确的文档。像打印机这样的共享设备被称为“排它性资源”,因为它一次只能由一个执行流访问。执行流必须以互斥的方式执行访问排它性资源的代码。

~互斥量: 互斥量本质是一把锁,在访问公共资源前对互斥量设置(加锁),确保同一时间只有一个线程访问数据,在访问完成后再释放(解锁)互斥量。在互斥量加锁之后,其他线程试图对该互斥量再次加锁时都会被阻塞,知道当前线程释放互斥锁。如果释放互斥量时有一个以上的互斥量,那么所有在该互斥量上阻塞的线程都会变成可运行状态,第一个变成运行的线程可以对互斥量加锁,其他线程看到互斥量依然是锁着的,只能再次阻塞等待该互斥量。

(1)如果互斥锁是锁定的,就是一个特定的线程持有这个互斥锁
(2)如果没有线程持有这个互斥锁,那么这个互斥锁就处于解锁状态

互斥量用pthread_mutex_t数据类型表示,在使用互斥量之前,必须用pthread_mutex_init函数对它进行初始化,注意,使用完毕后需调用pthread_mutex_destroy来销毁锁。

互斥量API详解:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);//初始化互斥锁函数,成功返回0失败返回错误码
//参数1:第一个为互斥锁的变量名(注意取地址)
//参数2:为互斥锁的属性
//注:处理调用函数之外也可以用宏来初始化互斥锁如:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER。
int pthread_mutex_destroy(pthread_mutex_t *mutex);//销毁锁函数,成功返回0失败返回错误码
//参数为互斥锁名的地址

int pthread_mutex_lock(pthread_mutex_t *mutex);//对共享资源上锁函数
int pthread_mutex_trylock(pthread_mutex_t *mutex);//对共享资源上锁函数
int pthread_mutex_unlock(pthread_mutex_t *mutex);//对共享资源解锁函数
/*
注:pthread_mutex_lock以原子操作给一个互斥锁加锁。如果目标互斥锁已经被加锁,则pthread_mutex_lock则被阻塞,直到该互斥锁占有者把它给解锁。
  pthread_mutex_trylock和pthread_mutex_lock类似,不过它始终立即返回,而不论被操作的互斥锁是否加锁,是pthread_mutex_lock的非阻塞版本。当目标互斥锁未被加锁时,pthread_mutex_trylock进行加锁操作;否则将返回EBUSY错误码。注意:这里讨论的pthread_mutex_lock和pthread_mutex_trylock是针对普通锁而言的,对于其他类型的锁,这两个加锁函数会有不同的行为。
  pthread_mutex_unlock以原子操作方式给一个互斥锁进行解锁操作。如果此时有其他线程正在等待这个互斥锁,则这些线程中的一个将获得它。
  */

互斥量使用代码:
实现对test的打印,并且打印的时候不能被其他线程所打断:

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

pthread_mutex_t mutex;
int cnt = 0;

void *func1(void *argc)
{
	pthread_mutex_lock(&mutex);
	while(1)
	{
		printf("t1:test\n");
		cnt++;
		sleep(1);
		if(cnt == 3){
			cnt = 0;
			pthread_mutex_unlock(&mutex);
			pthread_exit(NULL);
		}	
	}

}


void *func2(void *argc)
{
	pthread_mutex_lock(&mutex);

         while(1)
        {
                printf("t2:test\n");
                cnt++;
				sleep(1);
                if(cnt == 3){
						cnt = 0;
                        pthread_mutex_unlock(&mutex);
						pthread_exit(NULL);
                }
        }
	
}

int main()
{
	int ret1=100,ret2=100;
	int param = 100;
	pthread_t t1;
	pthread_t t2;
	pthread_mutex_init(&mutex,NULL);
	
	ret1 = pthread_create(&t1,NULL,func1,(void *)&param);//shuxing
	if(ret1 == 0)
	{
		printf("t1 creat successful!\n");
	} 
	ret2 = pthread_create(&t2,NULL,func2,(void *)&param);//shuxing
        if(ret2 == 0)
        {
                printf("t2 creat successful!\n");
        }

	pthread_join(t1,NULL);
	pthread_join(t2,NULL);

	pthread_mutex_destroy(&mutex);

	return 0;
}

运行结果如下:
在这里插入图片描述
<3>条件变量:条件变量是利用线程间共享的全局变量进行同步的一种机制。

条件变量是线程可用的一种同步机制,条件变量给多个线程提供了一个回合的场所,条件变量和互斥量一起使用,允许线程以无竞争的方式等待特定的条件发生。条件变量本事是由互斥体保护的,线程在改变条件状态之前必须首先锁住互斥量,其他线程在获取互斥量之前就不会觉察到这种变化,因为互斥量必须锁定之后才改变条件。

使用条件变量可以以原子方式阻塞线程,直到某个特定条件为真为止。条件变量始终与互斥锁一起使用,对条件的测试是在互斥锁(互斥)的保护下进行的。

如果条件为假,线程通常会基于条件变量阻塞,并以原子方式释放等待条件变化的互斥锁。如果另一个线程更改了条件,该线程可能会向相关的条件变量发出信号。

条件变量相关API:

pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);//和互斥锁一样是条件变量名和相关属性,NULL为默认
//和互斥锁一样条件变量也可以用宏来初始化:pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
pthread_cond_destroy(pthread_cont_t *cond);//销毁条件变量成功返回0,否则返回错误码

int pthread_cond_broadcast(pthread_cond_t *cond);// 用广播的形式唤醒所有等待条件变量的线程成功返回0,否则返回错误码
int pthread_cond_signal(pthread_cond_t *cond);// 用于唤醒一个等待条件变量的线程,至于哪个线程被唤醒,取决于线程的优先级和调度机制。有时候需要唤醒一个指定的线程,但pthread没有对该需要提供解决方法。可以间接实现该需求:定义一个能够唯一表示目标线程的全局变量,在唤醒等待条件变量的线程前先设置该变量为目标线程,然后以广播形式唤醒所有等待条件变量的线程,这些线程被唤醒后都检查改变量是否是自己,如果是就开始执行后续代码,否则继续等待。成功返回0,否则返回错误码
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);// 成功返回0,否则返回错误码
   // 传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住互斥量传给函数,函数然后自动把调用线程放到等待条件的线程列表上,对互斥量解锁。这就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait函数返回时,互斥量再次被锁住。
   

条件变量代码:
先执行线程2,并且打印全局变量的值,等全局变量到3时解除互斥量的条件保护等线程1拿到互斥锁并且运行一遍线程1代码;最后对互斥量继续进行条件保护(循环执行)

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

pthread_mutex_t mutex;
pthread_cond_t cond;
int data = 0;

void *func1(void *argc)
{

	while(1)
	{
		pthread_cond_wait(&cond,&mutex);//对指定的互斥量进行条件保护
		printf("fun1 quit!\n");
		data = 0;
	}
}

void *func2(void *argc)
{

	while(1){
		printf("fun2:data = %d\n",data);
		pthread_mutex_lock(&mutex);
		data ++;
		pthread_mutex_unlock(&mutex);//对共享资源访问时加上互斥锁
		if(data == 3)
		{
			pthread_cond_signal(&cond);//唤醒条件变量等待的线程(拿不到锁依然等待)
		}
		sleep(1);	
	}
}

int main()
{
	int ret1=100,ret2=100;
	int param = 100;
	pthread_t t1;
	pthread_t t2;

	pthread_mutex_init(&mutex,NULL);
	pthread_cond_init(&cond,NULL);

	ret1 = pthread_create(&t1,NULL,func1,(void *)&param);//shuxing

	ret2 = pthread_create(&t2,NULL,func2,(void *)&param);//shuxing


	pthread_join(t1,NULL);//等待线程退出
	pthread_join(t2,NULL);

	pthread_mutex_destroy(&mutex);//销毁互斥量
	pthread_cond_destroy(&cond);//销毁条件变量

	return 0;
}

注:原子操作:
原子操作(atomic operation)指的是由多步操作组成的一个操作。如果该操作不能原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。

现代操作系统中,一般都提供了原子操作来实现一些同步操作,所谓原子操作,也就是一个独立而不可分割的操作。在单核环境中,一般的意义下原子操作中线程不会被切换,线程切换要么在原子操作之前,要么在原子操作完成之后。更广泛的意义下原子操作是指一系列必须整体完成的操作步骤,如果任何一步操作没有完成,那么所有完成的步骤都必须回滚,这样就可以保证要么所有操作步骤都未完成,要么所有操作步骤都被完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值