1.线程概述
1.1什么是线程
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属于一个进程的其他的线程共享进程拥有的全部资源。
1.2进程与线程
Linux进程创建一个新线程时,线程将拥有自己的栈(因为线程有自己的局部变量),但与它的创建者共享全局变量、文件描述符、信号句柄和当前目录状态。
Linux通过fork创建子进程与创建线程之间是有区别的:fork创建出该进程的一份拷贝,这个新进程拥有自己的变量和自己的PID,它的时间调度是独立的,它的执行几乎完全独立于父进程。
进程可以看成一个资源的基本单位,而线程是程序调度的基本单位,一个进程内部的线程之间共享进程获得的时间
1.3为什么要使用线程
1、和进程相比,它是一种非常"节俭"的多任务操作方式。在linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。
2、运行于一个进程中的多个线程,它们之间使用相同的地址空间,而且线程间彼此切换所需时间也远远小于进程间切换所需要的时间。据统计,一个进程的开销大约是一个线程开销的30倍左右。
3、线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过进程间通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进城下的线程之间贡献数据空间,所以一个线程的数据可以直接为其他线程所用,这不仅快捷,而且方便。
除以上优点外,多线程程序作为一种多任务、并发的工作方式,还有如下优点:
1、使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
2、改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序才会利于理解和修改。
1.4如何使用
Linux系统下的多线程遵循POSIX线程接口,称为 pthread 。编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库 libpthread.a.
如:
gcc main.c -lpthread -o main
2.多线程设计
2.1线程创建
#include <pthread.h>
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
参数说明:
thread:指向pthread_t类型的指针,用于引用新创建的线程。
attr:用于设置线程的属性,一般不需要特殊的属性,所以可以简单地设置为NULL。
*(*start_routine)(void *):传递新线程所要执行的函数地址。
arg:新线程所要执行的函数的参数。
调用如果成功,则返回值是0,如果失败则返回错误代码。
与创建进程不同,创建线程时可以指定一个工作函数,新线程将从这个函数开始执行,函数返回也就等价于线程退出。
工作函数必须有一个(void *)型参数,新线程开始执行时,这个参数的值就是pthread_create函数的arg参数的值,因此可以利用它来向线程传递数据。
工作函数必须有(void *)型的返回值,它代表线程的退出状态。
2.2线程ID获取
include <pthread.h>
pthread_t pthread_self(void);
返回当前线程的ID
可以使用pthread_equal()函数检查两个线程ID是否相同
include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
如果想相等则返回0
2.3线程终止
#include <pthread.h>
void pthread_exit(void *retval);
参数说明:
retval:返回指针,指向线程向要返回的某个对象。
线程通过调用pthread_exit函数终止执行,并返回一个指向某对象的指针。注意:绝不能用它返回一个指向局部变量的指针,因为线程调用结束后,这个局部变量就不存在了,这将引起严重的程序漏洞。
调用pthread_exit()等价于在工作函数中执行return,区别是pthread_exit()可以在工作函数调用的任何函数中被调用。
如果主线程调用pthread_exit(),而不是exit()或执行return,则其他线程将继续执行。
2.4线程等待
#include <pthread.h>
int pthread_join(pthread_t th, void **thread_return);
参数说明:
th:将要等待的线程,线程通过pthread_create返回的标识符来指定。
thread_return:一个指针,指向另一个指针,而后者指向线程的返回值。不需要返回值则为NULL
2.5线程分离
#include <pthread.h>
int pthread_detach(pthread_t thread);
成功返回0。失败返回错误值
一个线程可以使用下面的方式分离自己:
pthread_detach(pthread_self());
pthread_detach()不会导致调用者阻塞,也不会导致所操作的线程结束。如果调用pthread_detach()时线程已经结束,则清理其所占用的资源。
对于创建时处于未分离状态的线程,必须调用一次pthread_join()或pthread_detach(),否则线程结束后就会留下没有释放的资源。
2.6注意事项
除了局部变量以外,所有其他变量都将在一个进程中的所有线程之间共享。
线程没有像进程那样的父子关系,仅有属于同一个进程的“同组”关系(进程实际上代表的是一个线程组)。
在POSIX线程模型中,主线程可以创建一个新线程A,新线程A又可以创建另一个新线程B,线程A和B本身没有父子关系,只是同属于一个进程。
等待线程结束的pthread_join()操作可以由任何一个同组的线程发起,不必是主线程。另外,如果主线程退出,即进程退出,则所有的线程也会随之退出。
3.线程同步
当多个线程共享相同的内存时,需要确保每个线程看到一致的数据。如果每个线程使用的变量都是其他线程不会读取或修改的,那么就不存在一致性问题。同样地,如果变量是只读的,多个线程读取该变量也不会有一致性问题。但是,当某个线程可以修改变量,而其他线程也可以读取或者修改这个变量的时候,就需要对这些线程进行同步,以确保它们在访问变量的存储内容时不会访问到无效的数值。
3.1信号量
通常有两种:二进制信号量和计数信号量。二进制信号量只有0和1两种取值,计数信号量有更大的取值范围。
信号量一般用来保护一段代码,使其每次只能被一个执行线程运行,要完成这个工作,可以使用二进制信号量。
有时,希望可以允许有限数目的线程执行一段指定的代码,这时可以使用计数信号量
3.1.1信号量的创建
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化一个未命名的信号量(unnamed semaphore)。
sem指向需要初始化的信号量(sem_t类型)。
value指定信号量的初始值。
pshared表明信号量是在一个进程的多个线程之间共享还是在多个进程之间共享。若pshared为0,信号量被一个进程的多个线程共享,此时应该将信号量(sem_t)置于所有线程可见的位置(全局变量或动态分配)。
执行成功返回0,出错返回-1,并设置errno。
注意:初始化一个已经初始化了的信号量将导致未定义的行为。
3.1.2信号量控制
#include <semaphore.h>
int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
sem_post的作用是以原子操作的方式给信号量的值加1
sem_wait函数以原子操作的方式将信号量的值减1,但它会等待直到信号量有个非零值才会开始减法操作。例如,对值为2的信号量调用sem_wait,线程将继续执行,但信号量的值会减到1。如果对值为0的信号量调用sem_wait,这个函数就会等待,直到有其它线程增加了该信号量的值使其不再为0为止。
如果两个线程同时在sem_wait函数上等待同一个信号量变为非零值,那么当该信号量被第三个线程增加1时,只有其中一个等待线程将开始对信号量减1,然后继续执行,另外一个线程还将继续等待。
补充:还有另外一个信号量函数sem_trywait,它是sem_wait的非阻塞版本。
3.1.3信号量的销毁
#include <semaphore.h>
int sem_destroy(sem_t *sem);
这个函数的作用是,用完信号量后对它进行清理,清理该信号量所拥有的资源。如果你试图清理的信号量正被一些线程等待,就会收到一个错误。
3.1.4生产者与消费者模型
多线程并发应用程序有一个经典的模型,即生产者/消费者模型。系统中,产生消息的是生产者,处理消息的是消费者,消费者和生产者通过一个缓冲区进行消息传递。生产者产生消息后提交到缓冲区,然后通知消费者可以从中取出消息进行处理。消费者处理完信息后,通知生产者可以继续提供消息。
要实现这个模型,关键在于消费者和生产者这两个线程进行同步。也就是说:只有缓冲区中有消息时,消费者才能够提取消息;只有消息已被处理,生产者才能产生消息提交到缓冲区。
4.互斥量
互斥量(mutex)从概念上来说类似于一个二进制信号量,即初始值为1的信号量。互斥量被获取之后就不能再被获取,因此对互斥体的获取和释放操作常常称为加锁和解锁操作。
互斥量只能由获取它的线程进行释放,如果违反这一原则,则结果是未定义的。
互斥量从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥量。
4.1互斥量的初始化
互斥量用一个pthread_mutex_t型的变量表示。使用互斥量之前需要对它进行初始化,其接口函数原型如下:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
mutex参数指向要初始化的互斥量。attr参数指向一个描述互斥量属性的结构体。attr参数可以为NULL,表示使用默认属性。
4.2互斥量的操作函数
互斥量的主要操作函数如下:
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()用于对mutex参数指向的互斥量进行加锁。如果这时互斥量已经被锁,则调用这个函数的线程将被阻塞,直到互斥量成为未锁状态。函数返回时,表示这个互斥量已经变为已锁状态,同时,函数的调用者成为这个互斥量的拥有者。
pthread_mutex_trylock()也用于对mutex参数指向的互斥量进行加锁。如果这时互斥量已经被锁,则函数以错误状态返回。
pthread_mutex_unlock()用于对mutex参数指向的互斥量进行解锁。如果这时互斥量是未锁状态或不是当前线程所拥有的,则结果未定义。 因此,互斥量必须在同一线程上成对出现。
4.3销毁互斥量
互斥量不用以后,应该使用下面的函数进行销毁:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
4.4消费者生产者的进一步讨论
上述消费者/生产者模型比较简单,缓冲区中只能容纳一条消息。生产者每提交一条消息到缓冲区中,就会通知消费者,等消费者取走消息之后才能提交下一条消息。同样,消费者也必须等待生产者提交一条消息后才能进行处理。这种设计的效率是比较低下的。
改进方案:如果将缓冲区设计为一个先进先出的队列,可以同时容纳多条消息,那么只要缓冲区不满,生产者就可以提交消息;同时,只要缓冲区不空,消费者就可以取出消息进行处理。这将大大提高整个程序的效率。
实现方式:
实现时,可以利用信号量计数的特性,用信号量的值表示缓冲区中消息的个数及空闲空间的个数。但这时由于生产者和消费者可能同时访问缓冲区,故需要再用一个互斥量来进行保护。
综上,对一个缓冲区需要定义以下三个同步变量:
sem_t full; /* 表示缓冲区中消息的个数 */
sem_t empty;/* 表示缓冲区中的空闲空间(还能容纳的消息个数) */
pthread_mutex_t lock; /* 同步对缓冲区的访问 */
这些同步变量的初始化如下:
sem_init(&full, 0, 0); /* 缓冲区中消息数为0 */
sem_init(&empty, 0, N); /* 缓冲区中的空闲空间数为N,即缓冲区的容量 */
pthread_mutex_init(&lock, NULL); /* 初始化互斥量 */
4.5改进后的生产者
void *producer(void *arg)
{
item *item; // 消息
for ( ; ; ) {
item = produce_item(); // 生产一条消息
sem_wait(&empty); // 获得表示空闲空间的信号量
pthread_mutex_lock(&lock); // 加锁
insert_item(item); // 将消息放入缓冲区
pthread_mutex_unlock(&lock); // 解锁
sem_post(&full); // 释放表示消息个数的信号量
}
return NULL;
}
改进后的消费者
void *consumer(void *arg)
{
item *item; // 消息
for ( ; ; ) {
sem_wait(&full); // 获得表示消息个数的信号量
pthread_mutex_lock(&lock); // 加锁
item = remove_item(); // 取得消息
pthread_mutex_unlock(&lock); // 解锁
sem_post(&empty); // 释放表示空闲空间的信号量
consumer_item(item); // 处理消息
}
return NULL;
}
5.条件变量
假设有这样一种情况:线程正在等待共享数据内某个条件出现,这时必须先解锁,否则其他线程不可能更改共享数据。一种实现方式是,可以循环检测共享数据,但是在检测前要加锁,检测后又要解锁,这样效率会很低。
因此,在这种情况下,需要一种方法,使得当线程在等待某些条件的满足时进入睡眠状态,一旦条件满足,线程就应该被唤醒继续执行。这个方法就是使用POSIX条件变量。
变量初始化:
条件POSIX条件变量由一个pthread_cond_t型的变量表示。使用条件变量前需要对它进行初始化,所用的接口函数原型如下:
int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *attr);
cond参数指向要初始化的条件变量,attr参数指向描述条件变量属性的结构体。attr可以为NULL,表示使用默认属性。
等待:
当程序中需要等待一个条件变量时,可以用下面的函数:
int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex,
const struct timespec *abstime);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
这些函数都有一个指向互斥量的参数mutex,说明条件变量必须与互斥量搭配使用。 调用这些函数时,首先,指定的互斥量将被释放,然后线程将阻塞,等待条件变量的触发。函数返回前,互斥量重新被线程获取。
pthread_cond_timedwait与pthread_cond_wait的区别在于,前者有一个由abstime指定的超时时间限制,当线程阻塞的时间超过了这个时间就会自动醒来。
触发一个条件变量可以用以下函数:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_signal可以唤醒一个或多个正在等待cond参数所指向的条件变量的线程,而pthread_cond_broadcast则可以唤醒全部正在等待cond参数所指向的条件变量的线程。
注意:POSIX标准只规定pthread_cond_signal函数唤醒至少一个睡眠中的线程,并没有规定只唤醒一个。
销毁:
条件变量不用之后,应该用下面的函数进行销毁:
int pthread_cond_destroy(pthread_cond_t *cond);