在传统的UNIX模型中,当一个进程需要另一个实体来完成某事,它就fork一个子进程并让子进程去处理。但是fork的调用有如下缺点:
(1)fork的代价是 昂贵的。fork要把父进程的内存印象复制到子进程,并在子进程中复制所有描述符等。
(2)fork返回之后父子进程之间信息的传递需要进程通信机制。调用fork之前父进程向尚未存在的子进程传递信息相当容易,因为子进程将从父进程数据空间及所有描述符的一个副本开始运行,但是从子进程向父进程返回信息却比较费力。
针对这两点,多线程技术相应而生,它具有如下优越性:
(1)它是一种非常"节俭"的多任务操作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
(2)线程间方便的通信更加方便。由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。但是,同一进程内的所有线程共享相同的全局内存,这样线程之间的通信就变得相当简单,随之而来的就是同步问题。
一、基本线程函数
1.pthread_create函数,创建线程
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func)(void *), void *arg);
pthread_t *tid:一个进程内的各个线程是由线程ID标识的,如果新线程创建成功,返回tid指针。
const pthread_attr_t *attr:每个线程有多个属性,包括优先级、初始栈大小、是否是一个守护线程等等。
void *(*func)(void *):线程启动函数,线程从调用这个函数开始,或显示结束(调用pthread_exit()),或隐式结束(让该函数返回)。
void *arg:线程执行func函数的传递参数。
2.pthread_join函数,等待一个线程终止
int pthread_join(pthread_t *tid, void **status);
void **status:二级指针,如果status指针非空,那么所等待线程的返回值将存放在status指向的位置。
3.pthread_self函数,返回线程ID
int pthread_self(void);
跟进程比较,相当于getpid。
4.pthread_detach函数,线程分离
int pthread_detach(pthread_t tid);
线程或者是可汇合的(joinable),或者是脱离的(detach)。当可汇合的线程终止时,线程ID和退出状态将保留,知道另外一个线程调用pthread_join。脱离的线程终止时,释放所有的资源,因此我们不能等待它终止。若要一个线程知道另一个线程的终止时间,我们就要保留第二个线程的可汇合性。
5.pthread_exit函数,线程终止
int pthread_detach(pthread_t tid);
若线程未脱离,那么它的线程ID和退出状态将保留到另外一个线程调用pthread_join为止。
二、多线程的同步
有了上面的基本函数还不足以完成本题的要求,为什么呢?因为题目要求按照ABCABC...的方式打印,而3个线程却在抢占资源,所以无法控制排列顺序。这时就需要用到多线程编程中的同步技术。
对于多线程编程来说,同步就是同一时间只允许一个线程访问资源,而其他线程不能访问。多线程有3种同步方式:
- 互斥锁
- 条件变量
- 读写锁
2.1 互斥锁
互斥锁是最基本的同步方式,它用来保护一个“临界区”,保证任何时刻只由一个线程在执行其中的代码。这个“临界区”通常是线程的共享数据。
下面三个函数给一个互斥锁上锁和解锁:
int pthread_mutex_lock(pthread_mutex_t *mptr);
int pthread_mutex_trylock(pthread_mutex_t *mptr);
int pthread_mutex_unlock(pthread_mutex_t *mptr);
假设线程2要给已经被线程1锁住的互斥锁(mutex)上锁(即执行pthread_mutex_lock(mutex)),那么它将一直阻塞直到到线程1解锁为止(即释放mutex)。
如果互斥锁变量时静态分配的,通常初始化为常值PTHREAD_MUTEX_INITIALIZER,如果互斥锁是动态分配的,那么在运行时调用pthread_mutex_init函数来初始化。
2.2 条件变量
互斥锁用于上锁,而条件变量则用于等待,通常它都会跟互斥锁一起使用。
int pthread_cond_wait(pthread_cond_t *cptr,pthread_mutex_t *mptr);
int pthread_cond_signal(pthread_cond_t *cptr);
通常pthread_cond_signal只唤醒等待在相应条件变量上的一个线程,若有多个线程需要被唤醒呢,这就要使用下面的函数了:
int pthread_cond_broadcast(pthread_cond_t *cptr);
2.3 读写锁
互斥锁将试图进入连你姐去的其他简称阻塞住,而读写锁是将读和写作了区分,读写锁的分配规则如下:
(1)只要没有线程持有某个给定的读写锁用于写,那么任意数目的线程可以持有该读写锁用于读;
(2)仅当没有线程持有某个给定的读写锁用于读或用于写时,才能分配该读写锁用于写。
int pthread_rwlock_rdlock(pthread_relock_t *rwptr);
int pthread_rwlock_wrlock(pthread_relock_t *rwptr);
int pthread_rwlock_unlock(pthread_relock_t *rwptr);
三、多线程问题分析
分析此题:
1.主线程main创建3个线程tid0,tid1,tid2;
2.设一个全局变量num,互斥锁mutex保护此临界区保证每次只有一个线程访问num;
3.若抢占到资源的线程tid并不是我们需要的,那么让它阻塞;
4.若抢占到资源的线程tid正好是我们需要的,那么就打印相应字母;
5.解锁,唤醒其他两个等待线程;
6.main函数等待3个线程打印结束才结束。
代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<error.h>
#include<unistd.h>
#include<pthread.h>
int num=0;
static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
void *func(void *);
int main()
{
pthread_t tid[3];
int ret=0,i;
for(i=0;i<3;i++)
if((ret=pthread_create(&tid[i],NULL,func,(void*)i))!=0)
printf("create thread_%c error\n",i+'A');
for(i=0;i<3;i++)
pthread_join(tid[i],NULL);
printf("\n");
return 0;
}
void *func(void *argc)
{
int i;
for(i=0;i<10;i++)
{
pthread_mutex_lock(&mutex);
while(num!=(int)argc)
pthread_cond_wait(&cond,&mutex); //收到条件变量,则继续执行循环
printf("%c",num+'A');
num=(num+1)%3;
pthread_mutex_unlock(&mutex);
pthread_cond_broadcast(&cond);
}
pthread_exit(0);
}
四、死锁问题
死锁是指两个或两个以上的执行序在执行过程中, 因争夺资源而造成的一种互相等待的现象。例如: 一个线程 T1 已锁定了一个资源 R1, 又想去锁定资源 R2,而此时另一个线程 T2 已锁定了资源 R2,却想去锁定资源R1,两个线程都想得到对方的资源,而不愿释放自己的资源,造成两个线程都在等待,而无法执行的情况,如下图所示。
图4-1 死锁示意图