1、线程概述
线程是计算机中独立运行的最小单位,运行时占用很少的系统资源。在用户看来,多个线程是同时执行,但从操作系统调度来看,各个线程是交替执行。系统不停的在各个线程之间切换,每个线程只有在系统分配给它的时间片内才能取得CPU的控制权,执行线程中的代码。(对于单CPU单核的情况)
那么为什么在支持多进程的情况下又引入多线程呢?
- 节约资源,节约时间。与每个进程都有独立的地址空间不同,同一进程内的线程共享进程的地址空间,故创建新线程花费时间少,线程间的切换速度也比进程快。
- 可以提高应用程序的响应速度。
- 可以提高多处理器的效率
- 可以改善程序的结构
虽然线程在进程内部共享地址空间,打开的文件描述符等资源,但线程也有其私有的数据信息,包括:
- 线程号(thread ID)
- 寄存器 [程序计数器,堆栈指针]
- 堆栈
- 信号掩码
- 优先级
- 线程的私有存储空间
2、创建线程
线程的创建通过函数 pthread_create 来完成,该函数的声明如下:
#incldue<pthread.h>
int pthread_creat(pthred_t *thread,pthread_attr_t *attr,
void *(*start_routine)(void *), void arg);
其作用是:创建线程号为thread,线程属性为attr,执行参数为arg的start_routine函数的线程。
新创建的线程去运行指针指向的函数,而原线程继续运行。
创建线程其他系统函数:
函数 | 说明 |
---|---|
pthread_t pthread_self(void) | 类似于getpid(),获取线程自身线程ID |
int pthread_equal(pthread_t thread1,pthread_t thread2) | 判断两个进程是否为同一进程 |
int pthread_once(pthread_once_t * once_control,void(*int_routine)(void)) | 保证该函数仅执行一次 |
下面来看看如何创建进程,如 createThread.c
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
int *get_thid(void);
int main(int argc,char **argv)
{
pthread_t thid; //声明进程ID变量
printf("parent pthread is me,my thid is %lu\n",pthread_self( ));
if(pthread_create(&thid,NULL,(void *)get_thid,NULL) != 0)
{
printf("Error!\n"); //调用函数进行进程的创建
return 0;
}
sleep(1);
return 0;
}
int *get_thid(void) //创建进程时,被调用的函数
{
pthread_t thid;
thid = pthread_self( );
if(thid < 0)
{
printf("Error!\n");
exit(0);
}
printf("I'm child pthread,my thid is %lu\n",thid);
return NULL;
}
运行结果如下:
由于pthread库不是标准linux库, 需在编译命令后面添加 -lpthread
3、线程终止
在Linux环境下,有两种方式实现线程的终止
- 调用return函数,实现线程终止
- 使用POSIX标准的接口API,pthread_exit函数
这两个函数主要的区别之处在于在主线程中调用的区别:
在主线程中调用return/exit,会使主线程结束,进而整个线程结束,全部线程消亡
如果是调用pthread_exit( )函数,则主线程消亡后,其他线程并不会受到影响,知道所有线程结束,进程才会结束
线程的终止时最重要的问题就是关于资源的释放问题,特别是一些临界资源
临界资源在同一时间只能被其中一个线程所使用,如若被多个线程使用,则会导致资源混乱。而如果临界资源给一个线程所使用,该线程退出时没有释放临界资源,则其他线程会一直认为该临界资源还在被其他线程所占用,就会导致死锁问题的出现。死锁问题的出现,在程序设计的过程中,往往是灾难性的,所以为了妥善处理线程结束时临界资源的释放问题,Linux系统提供了一对函数:pthread_cleanup_push()、pthread_cleanup_pop()用于自动释放资源。
#include<pthread.h>
#define pthread_cleanup_push(routine ,arg) \
{
struct _pthread_cleanup_buffer buffer; \
_pthread_cleanup_push(&buffer,(routine),(arg));
#dedine pthread_cleanup_pop \
_pthread_clean_pop(&buffer,(exeute));
}
线程终止时另外一个要注意的问题是线程间的同步问题
一般情况下,进程中各个线程的运行是相互独立的,线程的终止并不会相互通知,终止的线程资源仍归线程独有。所以资源的同步十分重要,同进程中的wait函数,在线程中所使用的是pthread_join( )函数,其声明如下:
#include<pthred.h>
void phread_exit(void *retval);
int pthread_join(pthread_t thid,void *thread_return);
int pthread_detach(pthread_t thid);
函数pthread_join用来使调用者挂起等待thid线程的结束
注意一个线程只能被另一个线程所等待,若被多个线程等待,其中一个线程恢复恢复就绪状态后,其他线程便进入了死锁,并且被等待的线程必须处于可join的状态,即它不能被设定为DETACHED(处于DETACHED状态的线程是指内核不关心线程返回值,线程结束后,内核自动回收的分离模式)所以,为了防止内存泄漏,并且完成线程同步,所有的线程结束时,都要设定为DETACHED或者 pthread_join( )等待
如以下线程终止实例 jointhread.c
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
void test(void);
int main(int argc,char **argv)
{
pthread_t thid;
int status;
pthread_create(&thid,NULL,(void *)test,NULL);
pthread_join(thid,(void *)&status); //使主线程进行阻塞,等待子线程结束
printf("I (%lu) have waited for a long time %d",pthread_self( ) ,status);
return 0;
}
void test(void)
{
printf("I am for test !\n");
sleep(20); //用sleep来延时函数
printf("I have achieved!\n");
pthread_exit(0) ;
}
运行结果如下:
以上结果即可看出,调用函数对目标函数完成了挂起等待。
4、私有数据
区别于之前提到的私有的数据信息,此处私有数据指的是多个线程中操作不同的数据。不同的线程对私有数据的访问对彼此之间是不可见的,操作互不影响,即键同名且全局但访问内存空间不同。
在这里举一个特殊的的例子:errno全局变量,它返回标准的出错代码。理论上errno应该是任何线程都够访问的全局变量,但是如若errno中保存的值还没有被使用,便被其他线程更改了其中的值,同样也会影响使用。像这种全局变量,即是我们此处要讨论的私有数据,即都能访问的全局变量,但是在各个线程中又是不一样的值。
私有数据的实现方式借用了:一键多值。对这个键可以理解为:一个数据管理器,在各个线程中,调用时,键会被告诉在此线程中应该使用什么值。
操作线程私有数据的函数的声明如下:
#include<pthread.h>
int pthread_key_creat (pthread_key_t *key,void (*destr funcation) (void *));
int pthread_setspecific (pthred_key_t *key,const void *pointer);
void *pthread_getspecific (pthread_key_t key);
int pthread_key_delete (pthread_key_t key);
- Creat函数是用来创建键的
- setspecific函数用来将线程的私有数据与键绑定,在线程自身中调用
- getspecific函数用来获取键值中绑定的私有数据
- delete函数用来销毁键
注意:在pthread_key_creat函数中使用了析构函数。所谓析构函数指的是用来在键值使用完成之后
清除并释放与键值绑定的私有数据所占的内存空间。键值对与私有数据所占用的并不是相同的数据空间,所以要分开进行释放。一旦在键值对释放时,未释放私有数据所占据的空间,则会导致内存泄漏,灾难性的后果。所以调用析构函数有其一定的必要性,当为NULL,会调用内核自身的清理函数。
一般情况下,线程调用malloc为私有数据分配内存空间
示例 tsd.c
#include<unistd.h>
#include<stdio.h>
#include<pthread.h>
#include<string.h>
pthread_key_t key; //定义全局变量库--键
void *thread1(void *arg); //线程1
void *thread2(void *arg); //线程2
int main(void)
{
pthread_t tid; //线程ID
printf("main thread begins running!\n");
pthread_key_create(&key,NULL); //参数为键地址,以及析构函数(用于私有数据的内存清理),如果为NULL,则调用系统的清理函数
pthread_create(&tid,NULL,thread1,NULL); //四个参数依次是线程ID,线程属性,调用函数,函数参数
sleep(10); //睡眠以使主线程等待
pthread_key_delete(key); //销毁键,私有数据的销毁必须在其之前,不然会内存泄漏
printf("mian pthread ends \n");
return 0;
}
void *thread1(void *arg)
{
int tsd = 5; //pthread中的私有数据
pthread_t thid_1; //分配新的线程号
printf("pthread 1 %lu is running!\n",pthread_self( ));
pthread_setspecific(key,(void *)tsd); //使键与私有数据绑定
pthread_create(&thid_1,NULL,thread2,NULL); //创建新线程
printf("thread1 %lu ends,pthread's tsd is %d\n",pthread_self( ),pthread_getspecific(key));
sleep(5); //睡眠以等待新线程结束
}
void *thread2(void *arg)
{
int tsd = 0;
printf("pthread 2 %lu is running\n",pthread_self( ));
pthread_setspecific(key,(void *)tsd); //绑定键值与私有数据
printf("Thread %lu ends,thread's tsd is %d\n",pthread_self( ),pthread_getspecific(key));
}
运行结果如下:
5、线程同步
线程最大的特点是资源的共享性,其中的同步问题十分重要。以下是Linux中处理同步问题的常用方式。
5.1、互斥锁
互斥锁通过锁机制来实现线程间的同步,在同一个时刻它通常只允许一个线程执行一个关键部分的代码。
1、使用互斥锁前必须先进行初始化操作。初始化有两种方式,一种是静态赋值法,将宏结构常量赋给互斥锁,另外一种方式是通过pthread_mutex_init
函数初始化互斥锁。
2、初始化后就可以给给互斥锁加锁了。加锁有两个函数:pthread_mutex_lock()
和pthread_mutex_trylock()
。用pthread_mutex_lock()
加锁的时候,如果mutex已经被锁住,当前尝试加锁的进程就会阻塞,直到互斥锁被其他线程释放,当pthread_mutex_lock
函数返回时,说明互斥锁已经被当前进程成功加锁。pthread_mutex_trylock
函数则不同,如果mutex已经被加锁,它将立即返回,返回的错误码为EBUSY,而不是阻塞等待。
3、用pthread_mutex_unlock
函数解锁时,要满足两个条件:一是互斥锁必须处于加锁状态,二是调用本函数的线程必须是给互斥锁加锁的线程。解锁后如果有其他线程在等待互斥锁,等待队列中的第一个将获得互斥锁
4、当一个互斥锁使用完毕后,必须进行清除,清除互斥锁使用函数pthread_mutex_destroy
。
清除一个互斥锁意味着释放它所占用的资源。清除锁时要求当前处于开放状态,若锁处于锁定状态,函数放回EBUSY,该函数成功之行时返回0。由于在Linux中,互斥锁并不占用内存,因此pthread_mutex_destroy()
除了解除互斥锁的状态外没有其他操作。
5.2、条件变量
条件变量是利用线程见共享的全局变量进行同步的一种机制。条件变量宏观上类似if语句,符合条件就能执行某段程序,否则只能等待条件成立。
使用条件变量主要包括两个动作:一个等待使用资源的线程等待”条件变量被设置为真”;另一个线程在使用完资源后”设置条件为真”,这样就可以保证线程间的同步了。这样就存在一个关键问题,这就是要保证条件变量能被正确的修改,条件变量要受到特殊的保护,实际使用中互斥锁扮演者这样一个保护者的角色。Linux也提供了一系列对条件变量操作的函数。
1、与互斥锁一样,条件变量的初始话也有两种方式,一种是静态赋值法,将宏结构常量PTHREAD_COND_INITIALIZER赋给互斥锁。另一种方式是使用函数pthread_cond_init
。
2、pthread_cond_wait
函数释放有mutex指向的互斥锁,同时使当前线程关于cond所指向的条件变量阻塞,直到条件被信号唤醒。通常条件表达式在互斥锁的保护下求值,如果条件表达式为假,那么线程基于条件变量阻塞。当一个线程改变条件变量的同时,条件变量获得一个信号,使得条件变量的线程退出阻塞状态。
pthread_cond_timedwait
函数和pthread_cond_wait
函数用法类似,差别在于pthread_cond_timedwait
函数将阻塞直到条件变量获得信号或者经过abstime指定的时间,也就是说,如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待。
3、线程被条件变量阻塞后,可以通过函数pthread_cond_signal
和pthread_cond_broadcast
激活。
pthread_cond_signal
激活一个等待条件成立的线程,存在多个等待线程时,按入队顺序激活其中一个,而pthread_cond_broadcast
则激活所有等待线程。
4、当一个条件变量不再使用时,需要将其清除。清除一个条件变量通过调用pthread_cond_destroy()
实现。pthread_cond_destroy
函数清除由cond指向的条件变量。注意:只有在没有线程等待该条件变量的时候才能清除这个条件变量,否则返回EBUSY。
示例代码 condition.c
#include <stdio.h>
#include <pthread.h>
#include "stdlib.h"
#include "unistd.h"
pthread_mutex_t mutex;
pthread_cond_t cond;
void hander(void *arg)
{
free(arg);
(void)pthread_mutex_unlock(&mutex);
}
void *thread1(void *arg)
{
pthread_cleanup_push(hander, &mutex);
while (1) {
printf("thread1 is running\n");
pthread_mutex_lock(&mutex); //条件变量使用时配合互斥锁使用
pthread_cond_wait(&cond,&mutex);
printf("thread1 applied the condition\n");
pthread_mutex_unlock(&mutex);
sleep(4);
}
pthread_cleanup_pop(0);
}
void *thread2(void *arg)
{
while (1) {
printf("thread2 is running\n");
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond,&mutex);
printf("thread2 applied the condition\n");
pthread_mutex_unlock(&mutex);
sleep(1);
}
}
int main()
{
pthread_t thid1,thid2;
printf("condition variable study!\n");
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
pthread_create(&thid1,NULL,thread1,NULL);
pthread_create(&thid2,NULL,thread2,NULL);
sleep(1);
do {
pthread_cond_signal(&cond);
} while(1);
sleep(20);
pthread_exit(0);
return 0;
}
5.3、异步信号
在Linux系统中,线程是在内核外实现的,它不像进程那样在内核中实现,Linux线程本质上是轻量级的进程。信号可以被进程用来进行相互通信,一个进程通过信号通知另一个进程发生了某件事件,比如该进程所需要的输入数据已经就绪。线程同进程一样也可以接收和处理信号,信号也是一种线程同步的手段。
信号于任何线程都是异步的,也就是说信号到达线程的时间是不定的。如果有多个线程可以接收异步信号,则只有一个被选中,如果并发的多个同样的信号被送到一个进程,每一个将被不同的线程处理,如果所有的线程都屏蔽该信号,则这些信号将被挂起,直到有信号解除屏蔽来处理它们。其中函数pthread_kill
用来向特定的线程发送信号signal,函数pthread_sigmask
用来设置线程的信号屏蔽码,但对不允许屏蔽的Cancel信号和不允许相应的Restart信号进行了保护,函数sigwait
用来阻塞线程。