自学 Linux 13—Linux 下的线程详细知识点

Linux 下的线程

  线程的概念早在 20 世纪 60 年代就被提出,但是在操作系统中真正使用多线程是在 20 世纪 80 年代的中期。在使用线程方面,Solaris 是其中的先驱。在传统的 UNIX 系统中,线程的概念也被使用,但是一个线程对应着一个进程,因此多线程变成了多进程,线程的真正优点没有得到发挥。现在,多线程的技术在操作系统中已经得到普及,被很多操作系统所采用,其中包括 Windows 操作系统和 Linux 系统。与传统的进程相比较,用线程来实现相同的功能有如下的优点:

  █ 系统资源消耗低;
  █ 速度快;
  █ 线程间的数据共享比进程间容易得多。

1. 多线程编程实例

  Linux 系统下的多线程遵循 POSIX 标准,叫做 pthread,读者可以使用 man pthreadLinux 系统下査看系统对线程的解释。编写 Linux 下的线程需要包含头文件 pthread.h,在生成可执行文件的时候需要链接库 libpthread.a 或者 libpthread.so

  下面首先给出一个简单的多线程的例子,引入多线程的概念:

/* pthread.c 线程实例 */
#include <stdio.h>
#include <pthread.h>
#include <unistd.h> 
static int run = 1;	/* 运行状态参数 */
static int retvalue; /* 线程返回值 */
void *start_routine(void *arg) /* 线程处理函数 */
{
	int *running = arg;	/* 获取运行状态指针 */
	printf("子线程初始化完毕,传入参数为:%d\n",*running); /* 打印信息 */
	while(*running)	/* 当 running 控制参数有效 */
	{
		printf("子线程正在运行\n"); /* 打印运行信息 */
		usleep(1); /* 等待 */
	}
	printf("子线程退出\n"); /* 打印退出信息 */
	
	retvalue =8; /* 设置退出值 */
	pthread exit((void*)Sretvalue);) /* 线程退出并设置退出值 */
}
int main(void)
{
	pthread一t pt;
	int ret = -1;
	int times = 3;
	int i =0;
	int *ret_join = NULL;
	
	ret = pthread_create(&pt, NULL, (void*)start_routine, &run); /* 建立线程 */
	
	if(ret != 0) /* 建立线程失败 */
	{
		printf("建立线程失败\n"); /* 打印信息 */
		return 1; /* 返回 */
	}
	usleep (1); /* 等待 */
	for(;i<times;i++) /* 进行 3 次打印 */
	{
		printf("主线程打印\n"); /* 打印信息 */
		usleep(1); /* 等待 */
	}
	run = 0; /* 设罝线程退出控制值,让线程退出 * /
	pthread_join(pt,(void*)&rat_join); /* 等待线程退出 */
	printf("线程返回值为:%d\n", *ret_join); /* 打印线程的退出值 */
	return 0;
}

  上面的代码在一个进程中调用函数 pthread_create() 建立一个子线程。主线程在建立子线程之后打印 “ 主线程打印 ”,子线程建立成功之后打印 “ 子程序正在运行 ”。当标志参数 running 不为 0 的时候,子线程会一直打印上述的消息。

  主线程在打印上述的 “ 主线程打印 ” 3 次之后,设置标志参数的值为 0,然后调用 pthread_join() 函数等待线程退出。子线程处理函数 start_routine()running0 之后设置退出值为 8,调用函数 pthread_exit() 退出,然后主线程的 pthread_join() 函数会返回,程序结束。

  将上述代码保存到文件 pthread.c 中使用如下命令进行编译后,生成可执行文件 pthread,在编译的时候链接线程库 libpthread

gcc -o pthread pthread.c -lpthread

  运行 pthread,可以发现,当子线程初始化成功后和主线程交替执行:

$ ./pthread
子线程初始化完毕,传入参数为:1
子线程正在运行
主线程打印
子线程正在运行
子线程正在运行
主线程打印
子线程正在运行
子线程正在运行
主线程打印
子线程正在运行
子线程退出
线程返回值为:8

  再次运行此程序,得到如下结果:

$ ./pthread
子线程初始化完毕,传入参数为:1
子线程正在运行
主线程打印
子线程正在运行
主线程打印
子线程正在运行
子线程正在运行
主线程打印
子线程正在运行
子线程退出
线程返回值:8

  前后两次结果不一致,主要是两个线程争夺 CPU 资源造成的。

2. Linux 下线程创建函数 pthread_create()

  在前面的例子中,用到了两个线程相关的函数 pthread_create()pthread_join()。函数 pthread_create() 用于创建一个线程,函数 pthread_join() 等待一个线程的退出。

  在 pthread_create() 函数调用时,传入的参数有线程属性、线程函数、线程函数变量,用于生成一个某种特性的线程,线程中执行线程函数。创建线程使用函数 pthread_create(),它的原型为:

int pthread_create(pthread_t *thread,
	pthread_attr_t *attr,
	void *(*start_routine)(void *),
	void *arg);

  █ thread:用于标识一个线程,它是一个 pthread_t 类型的变量,在头文件 pthreadtypes.h 中定义,如下所示。

typedef unsigned long int pthread_t;

  █ attr:这个参数用于设置线程的属性,本例中设置为空,采用了默认属性。
  █ start_routine:当线程的资源分配成功后,线程中所运行的单元,上例中设置为自己编写的一个函数 start_routine()
  █ arg:线程函数运行时传入的参数,上例将一个 run 的参数传入用于控制线程的结束。

  当创建线程成功时,函数返回 0;若不为 0,则说明创建线程失败,常见的错误返回代码为 EAGAINEINVAL。错误代码 EAGAIN 表示系统中的线程数量达到了上限,错误代码 EINVAL 表示线程的属性非法。

  线程创建成功后,新创建的线程按照参数 3 和参数 4 确定一个运行函数,原来的线程在线程创建函数返回后继续运行下一行代码。

3. 线程的结束函数 pthread_join() 和 pthread_exit()

  函数 pthread_join() 用来等待一个线程运行结束。这个函数是阻塞函数,一直到被等待的线程结束为止,函数才返回并且收回被等待线程的资源。函数原型为:

extern int pthread_join _P ((pthread_t _th, void **_thread_return));

  █ _th:线程的标识符,即 pthread_create() 函数创建成功的值。
  █ _thread_return:线程返回值,它是一个指针,可以用来存储被等待线程的返回值。

  如上例中的 ret_join,当线程返回时可以返回一个指针,pthread_join() 在等待的线程返回时,获得此值。这个参数是一个指向指针的指针类型参数,在调用此函数来获得线程参数传出的时候需要注意,通常用一个指针变量的地址来表示。

  上面的代码中先建立一个 int 类型的指针,int *ret_join = NULL,然后调用函数 pthread_join() 来获得线程退出时的传出值 pthread_join(pt,(void)&ret_join)*。

  线程函数(在上例中为函数 start_routine())的结束方式有两种,一种是线程函数运行结束,不用返回结果;另一种方式是通过函数 pthread_exit() 来实现,将结果传出。它的函数原型为:

extern void pthread_exit _P ((void *_retval)) _attribute_ ((_noreturn_));

  参数 _retval 是函数的返回值,这个值可以被 pthread_join() 函数捕获,通过 _thread_retrun 参数获得此值,如上例所示。

4. 线程的属性

  在上例中,用 pthread_create() 函数创建线程时,使用了默认参数,即将该函数的第 2 个参数设为 NULL。通常来说,建立一个线程的时候,使用默认属性就够了,但是很多时候需要调整线程的厲性,特别是线程的优先级。

1. 线程的属性结构

  线程的属性结构为 pthread_attr_t,在头文件 <pthreadtypes.h> 中定义,代码如下:

typedef struct _pthread_attr_s	
{
	int		_detachstate;	/* 线程的终止状态 */
	int		_schedpolicy;	/* 调度优先级 */
	struct _sched_param _schedparam;	/* 参数 */
	int		_inheritsched;	/* 继承 */
	int		_scope;	/* 范围 */
	size_t	_guardsize;	/* 保证尺寸 */
	int		_stackaddr_set;	/* 运行栈 */
	void	*_stackaddr;	/* 线程运行栈地址 */
	size_t	_stacksize;	/* 线程运行栈大小 */
} pthread_attr_t;

  但是线程的属性值不能直接设置,须使用相关函数进行操作。线程属性的初始化函数为 pthread_attr_init(),这个函数必须在 pthread_create() 函数之前调用。

  属性对象主要包括线程的摘取状态、调度优先级、运行栈地址、运行栈大小、优先级。

2. 线程的优先级

  线程的优先级是经常设置的属性,由两个函数进行控制:pthread_attr_getschedparam() 函数获得线程的优先级设置;函数 pthread_attr_setschedparam() 设置线程的优先级。

int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
int pthread_attr_getschedparam(const pthread_attr_t	*attr, struct sched_param *param);

  线程的优先级存放在结构 sched_param 中。其操作方式是先将优先级取出来,然后对需要设置的参数修改后再写回去,这是对复杂结构进行设置的通用办法,防止因为设置不当造成不可预料的麻烦。例如设置优先级的代码如下,因为结构 sched_param 在头文件 sched.h 中,所以要加入头文件 sched.h

#include <stdio.h>
#include <pthread.h> 
#include <sched.h>
pthread_attr_t attr;
struct sched_param sch;
pthread_t pt;
pthread_attr_init(&attr); /* 初始化属性设置 */
pthread _attr_getschedparam(&attr, &sch); /* 获得当前的线程属性 */
sch.sched_priority = 256; /* 设置线程优先级为 256 */ 
pthread _attr_setschedparam(&attr, &sch); /* 设置线程优先级 */
pthread _create(&pt, &attr, (void*)start_routine,&run); /* 建立线程,属性为上述设置 */
3. 线程的绑定状态

  设置线程绑定状态的函数为pthread_attr_setscope(),它有两个参数,第1个是指向属 性结构的指针,第2个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM (绑定 的)和PTHREAD_SCOPE_PROCESS (非绑定的)。下面的代码即创建了一个绑定的线程。

#include <pthread.h>
pthread_attr_t attr;
pthread_t tid;
/* 初始化属性值,均设为默认值 */
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM); /* 设置绑定的线程 */
pthread_create(&tid, &attr, (void *)my_function, NULL);

4. 线程的分离状态

  线程的分离状态决定线程的终止方法。线程的分离状态有分离线程和非分离线程两种。

  在上面的例子中,线程建立的时候没有设置属性,默认终止方法为非分离状态。在这种情况下,需要等待创建线程的结束。只有当 pthread_join() 函数返回时,线程才算终止,并且释放线程创建的时候系统分配的资源。

  分离线程不用其他线程等待,当前线程运行结束后线程就结束了,并且马上释放资源。线程的分离方式可以根据需要,选择适当的分离状态。设置线程分离状态的函数为:

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

  参数 detachstate 可以为分离线程或者非分离线程,PTHREAD_CREATE_DETACHED 用于设置分离线程,PTHREAD_CREATE_JOINABLE 用于设置非分离线程。

  当将一个线程设置为分离线程时,如果线程的运行非常快,可能在 pthread_create() 函数返回之前就终止了。由于一个线程在终止以后可以将线程号和系统资源移交给其他的线程使用,此时再使用函数 pthread_create() 获得的线程号进行操作会发生错误。

5. 线程间的互斥

  互斥锁是用来保护一段临界区的,它可以保证某时间段内只有一个线程在执行一段代码或者访问某个资源。下面一段代码是一个生产者/消费者的实例程序,生产者生产数据,消费者消耗数据,它们共用一个变量,每次只有一个线程访问此公共变量。

1. 线程互斥的函数介绍

  与线程互斥有关的函数原型和初始化的常量如下,主要包含互斥的初始化方式宏定义、互斥的初始化函数 pthread_mutex_init()、互斥的锁定函数 pthread_mutex_lock()、互斥的预锁定函数 pthread_mutex_trylock()、互斥的解锁函数 pthread_mutex_unlock()、互斥的销毁函数 pthread_mutex_destroy()

#include <pthread.h>
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER__NP;
pthread_mutex_t errchkmutex = PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr); /* 互斥初始化 */
int pthread_mutex_lock(pthread_mutex_t *mutex);	/* 锁定互斥 */
int pthread_mutex_trylock(pthread_mutex_t *mutex);	/* 互斥预锁定 */
int pthread_mutex_unlock(pthread_mutex_t *mutex);	/* 解锁互斥 */
int pthread_mutex_destroy(pthread_mutex_t *mutex);	/* 销毁互斥 */

  函数 pthread_mutex_init(),初始化一个 mutex 变量,结构 pthread_mutex_t 为系统内部私有的数据类型,在使用时直接用 pthread_rmuex_t 就可以了,因为系统可能对其实现进行修改。在上例中属性为 NULL,表明使用默认属性。

  pthread_mutex_lock() 函数声明开始用互斥锁上锁,此后的代码直至调用 pthread_mutex_unlock() 函数为止,均不能执行被保护区域的代码,也就是说,在同一时间内只能有一个线程执行。当一个线程执行到 pthread_mutex_lock() 函数处时,如果该锁此时被另一个线程使用,此线程被阻塞,即程序将等待另个线程释放此互斥锁。

  互斥锁使用完毕后记得要释放资源,调用 pthread_mutex_destroy() 函数进行释放。

2. 线程互斥函数的例子

  下面是一个线程互斥的例子。代码用线程互斥的方法构建了一个生产者和消费者的例子。代码中建立了两个线程,函数 producter_f() 用于生产,函数 consumer_f() 用于消费。

/* mutex.c 线程实例 */
#include <stdio.h> 
#include <pthread.h> 
#include <unistd.h>
#include <sched.h>
void *producter_f(void *arg);	/* 生产者 */
void *consumer_f(void *arg);	/* 消费者 */
int buffer_has_item=0;	/* 缓冲区计数值 */
pthread_mutex_t mutex;	/* 互斥区 */
int running = 1;	/* 线程运行控制 */

int main(void)
{
	pthread_t consumer_t;	/* 消费者线程参数 */
	pthread_t producter_t;  /* 生产者线程参数 */
	
	pthread_mutex_init(&mutex,NULL); /* 初始化互斥 */
	
	pthread_create(&producter_t, NULL,(void *)producter_f, NULL); /* 建立生产者线程 */
	
	pthread_create(&consumer_t, NULL,(void *)consumer_f, NULL); /* 建立消费者线程 */
	
	usleep(1);	/* 等待,线程创建完毕 */
	running = 0;	/* 设置线程退出值 */
	pthread_join(consumer_t, NULL);	/* 等待消费者线程退出 */
	pthread_join(producter_t,NULL);	/* 等待生产者线程退出 */
	pthread_mutex_destroy(&mutex);	/* 销毁互斥 */
	
	return 0;
}

void *producter_f(void *arg) /* 生产者线程程序 */
{
	while(running) /* 没有设置退出值 */
	{
		pthread_mutex_lock(&mutex);	/* 进入互斥区 */
		buffer_has_item++;	/* 增加计数值 */
		printf("生产,总数量:%d\n",buffer_has_item); /* 打印信息 */
		pthread_mutex_unlock(&mutex);	/* 离开互斥区 */
	}
}
void *consumer_f(void *arg) /* 消费者线程程序 */
{
	while(running) /* 没有设置退出值 */
	{
		pthread_mutex_lock(&mutex); /* 进入互斥区 */
		buffer_has_item--; /* 减小计数值 */
		printf("消费,总数量:%d\n",buffer_has_ietm); /* 打印信息 */
		pthread_mutex_unlock(&mutex); /* 离开互斥区 */
	}
}

  上例中声明了一个线程互斥变量 mutex,在线程函数 consumer_f() 和函数 producter_f() 中,用线程互斥锁函数 pthread_mutex_lock() 和函数 pthread_mutex_unlock() 来保护对公共变量 buffer_ has_item 的访问。

6. 线程中使用信号量

  线程的信号量与进程的信号量类似,但是使用线程的信号量可以高效地完成基于线程的资源计数。信号量实际上是一个非负的整数计数器,用来实现对公共资源的控制。在公共资源增加的时候,信号量的值增加;公共资源消耗的时候,信号量的值减少;只有当信号量的值大于 0 的时候,才能访问信号量所代表的公共资源。

  信号量的主要函数有信号量初始化函数 sem_init()、信号量的销毁函数 sem_destroy()、信号量的增加函数 sem_post()、信号量的减少函数 sem_wait() 等。还有一个函数 sem_ trywait(),它的含义与互斥的函数 pthread_mutex_trylock() 是一致的,先对资源是否可用进行判断。函数的原型在头文件 semaphore.h 中定义。

1. 线程信号量初始化函数 sem_init()

  **sem_init()**函数用来初始化一个信号量。它的原型为:

extern int sem_init _P((sem_t *_sem, int _pshared, unsigned int _value));

  其中的参数 sem 指向信号量结构的一个指针,当信号量初始化成功的时候,可以使用这个指针进行信号量的增加减少操作;参数 pshared 用于表示信号量的共享类型,不为 0 时这个信号量可以在进程间共享,否则这个信号量只能在当前进程的多个线程之间共享; 参数 value 用于设置信号量初始化的时候信号量的值。

2. 线程信号量增加函数 sem_post()

  sem_post() 函数的作用是增加信号量的值,每次增加的值为 1。当有线程等待这个信号量的时候,等待的线程将返回。函数的原型为:

#include<semaphore.h>
int sem_post(sem_t *sem);
3. 线程信号量等待函数 sem_wait()

  sem_wait() 函数的作用是减少信号量的值,如果信号量的值为 0,则线程会一直阻塞到信号量的值大于 0 为止。sem_wait() 函数每次使信号最的值减少 1,当信号量的值为 0 时不再减少。函数原型为:

#include<semaphore.h>
int sem_wait(sem_t *sem);
4. 线程信号量销毀函数 sem_destroy()

  sem_destroy() 函数用来释放信号量 sem,函数原型为:

#include<semaphore.h>
int sem_destroy(sen_t *sem);
5. 线程信号量的例子

  下面来看一个使用信号量的例子。在 mutex 的例子中,使用了一个全局变量来计数,在这个例子中,使用信号暈来做相同的工作,其中一个线程增加信号量来模仿生产者,另一个线程获得信号量来模仿消费者。

/* sem.c 线程实例 */
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
void *producter_f (void *arg); /* 生产者线程函数 */
void *consumer_f (void *arg); /* 消费者线程函数 */
sem_t sem;
int running = 1;

int main (void)
{
	pthread_t consumer_t;  /* 消费者线程参数 */
	pthread_t producter_t; /* 生产者线程参数 */
	sem_init(&sem, 0, 16); /* 信号量初始化 */
	pthread_create(&producter_t, NULL,(void *)producter_f, NULL); /* 建立生产者线程 */
	pthread_create(&consumer_t, NULL, (void *)consumer_f, NULL); /* 建立消赉者线程 */
	sleep(1);	/* 等待 */
	running = 0;	/* 设置线程退出 */
	pthread_join(consumer_t,NULL); /* 等待消费者线程退出 */
	pthread_join(producter_t,NULL); /* 等待生产者线程退出 */
	sem_destroy(&sem); /* 销毁信号量 */
	
	return 0;
}
void *producter_f(void *arg)	/* 生产者处理程序代码 */
{
	int semval=0;	/* 信号量的初始值为 0 */
	while(running)  /* 运行状态为可运行 */
	{
		usleep(1);	/* 等待 */
		sem_post(&sem);	/* 信号量增加 */
		sem_getvalue(&sem, &semval); /* 获得信号量的值 */
		printf("生产,总数量:%d\n", semval); /* 打印信息 */
	}
}
void *consumer_f(void *arg) /* 消费者处理程序代码 */
{
	int semval=0;	/* 信号量的初始值为 0 */
	while(running) /* 运行状态为可运行 */
	{
		usleep(1);	/* 等待 */
		sem_wait(&sem);	/* 等待信号量 */
		sem_getvalue(&sem,&semval);	/* 获得信号量的值 */
		printf("消费,总数量: %d\n", semval);	/* 打印信息 */
	}
}

  在 Linux 下,用命令 “ gcc -Ipthread sem.c -o sem ” 生成可执行文件 sem。运行 sem,得到如下结果:

......
生产,总数量:100
生产,总数量:101
生产,总数量:102
消费,总数量:101
生产,总数量:102
消费,总数量:101
消费,总数量:100
生产,总数量:101
......

  从执行结果中可以看出,上述程序建立的各个线程间存在竞争关系。而数值并未按产生一个消耗一个的顺序显示出来,而是以交叉的方式进行,有的时候产生多个后再消耗多个,造成这种现象的原因是信号量的产生和消耗线程对 CPU 竞争的结果。

6. 小结

  进程、线程和程序概念的异同主要集中于进程和程序之间的动态和静态、进程和线程之间的运行规模的大小之分。

  进程的产生和消亡从系统的层面来看对应着资源的申请、变量的设置、运行时的调度,以及消亡时的资源释放和变量重置。

  进程间的通信和同步的方法主要有管道消息队列信号量共享内存,以及信号机制。这些机制都是 UNIX 系统 IPC 的典型方法。

  Linux 的线程是一个轻量级的进程,使用线程来编写程序比进程对系统的负载要低, 重要的是共用变量和资源要比进程的方法简单得多。

  Linux 下的线程并不是现代线程的典型概念,例如没有 POSIX 所规定的挂起、恢复运行等机制,它是在 Linux 进程基础上的一个发展延伸。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值