多线程编程
2018年12月27日
19:49
一、什么是线程
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属于一个进程的其他的线程共享进程拥有的全部资源。
二、多线程程序设计
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、线程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
3、线程终止
#include <pthread.h>
void pthread_exit(void *retval);
参数说明:
retval:返回指针,指向线程向要返回的某个对象。
线程通过调用pthread_exit函数终止执行,并返回一个指向某对象的指针。注意:绝不能用它返回一个指向局部变量的指针,因为线程调用结束后,这个局部变量就不存在了,这将引起严重的程序漏洞。
调用pthread_exit()等价于在工作函数中执行return,区别是pthread_exit()可以在工作函数调用的任何函数中被调用。
如果主线程调用pthread_exit(),而不是exit()或执行return,则其他线程将继续执行。
4、线程等待
#include <pthread.h>
int pthread_join(pthread_t th, void **thread_return);
参数说明:
th:将要等待的线程,线程通过pthread_create返回的标识符来指定。
thread_return:一个指针,指向另一个指针,而后者指向线程的返回值。不需要返回值则为NULL
5、线程分离
#include <pthread.h>
int pthread_detach(pthread_t thread);
成功返回0。失败返回错误值
一个线程可以使用下面的方式分离自己:
pthread_detach(pthread_self());
pthread_detach()不会导致调用者阻塞,也不会导致所操作的线程结束。如果调用pthread_detach()时线程已经结束,则清理其所占用的资源。
对于创建时处于未分离状态的线程,必须调用一次pthread_join()或pthread_detach(),否则线程结束后就会留下没有释放的资源。
6、注意事项
除了局部变量以外,所有其他变量都将在一个进程中的所有线程之间共享。
线程没有像进程那样的父子关系,仅有属于同一个进程的“同组”关系(进程实际上代表的是一个线程组)。
在POSIX线程模型中,主线程可以创建一个新线程A,新线程A又可以创建另一个新线程B,线程A和B本身没有父子关系,只是同属于一个进程。
等待线程结束的pthread_join()操作可以由任何一个同组的线程发起,不必是主线程。另外,如果主线程退出,即进程退出,则所有的线程也会随之退出。
三、线程同步
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。
注意:初始化一个已经初始化了的信号量将导致未定义的行为。
信号量控制:
#include <semaphore.h>
int sem_post(sem_t *sem); // v
int sem_wait(sem_t *sem); // p
sem_post的作用是以原子操作的方式给信号量的值加1
sem_wait函数以原子操作的方式将信号量的值减1,但它会等待直到信号量有个非零值才会开始减法操作。例如,对值为2的信号量调用sem_wait,线程将继续执行,但信号量的值会减到1。如果对值为0的信号量调用sem_wait,这个函数就会等待,直到有其它线程增加了该信号量的值使其不再为0为止。
如果两个线程同时在sem_wait函数上等待同一个信号量变为非零值,那么当该信号量被第三个线程增加1时,只有其中一个等待线程将开始对信号量减1,然后继续执行,另外一个线程还将继续等待。
补充:还有另外一个信号量函数sem_trywait,它是sem_wait的非阻塞版本。
信号量销毁:
#include <semaphore.h>
int sem_destroy(sem_t *sem);
这个函数的作用是,用完信号量后对它进行清理,清理该信号量所拥有的资源。如果你试图清理的信号量正被一些线程等待,就会收到一个错误。
生产者与消费者模型:
#include <semaphore.h>
int sem_destroy(sem_t *sem);
这个函数的作用是,用完信号量后对它进行清理,清理该信号量所拥有的资源。如果你试图清理的信号量正被一些线程等待,就会收到一个错误。
2、互斥量
初始化:
互斥量用一个pthread_mutex_t型的变量表示。使用互斥量之前需要对它进行初始化,其接口函数原型如下:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
mutex参数指向要初始化的互斥量。attr参数指向一个描述互斥量属性的结构体。attr参数可以为NULL,表示使用默认属性。
操作函数:
互斥量的主要操作函数如下:
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参数指向的互斥量进行解锁。如果这时互斥量是未锁状态或不是当前线程所拥有的,则结果未定义。 因此,互斥量必须在同一线程上成对出现。
销毁:
互斥量不用以后,应该使用下面的函数进行销毁:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
3、生产者与消费者模型 的进一步讨论(互斥量改进)
改进方案:
如果将缓冲区设计为一个先进先出的队列,可以同时容纳多条消息,那么只要缓冲区不满,生产者就可以提交消息;同时,只要缓冲区不空,消费者就可以取出消息进行处理。这将大大提高整个程序的效率。
实现方式:
实现时,可以利用信号量计数的特性,用信号量的值表示缓冲区中消息的个数及空闲空间的个数。但这时由于生产者和消费者可能同时访问缓冲区,故需要再用一个互斥量来进行保护。
综上,对一个缓冲区需要定义以下三个同步变量:
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); /* 初始化互斥量 */
改进后的生产者:
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 *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;
}
四、条件变量
1、条件变量初始化
POSIX条件变量由一个pthread_cond_t型的变量表示。使用条件变量前需要对它进行初始化,所用的接口函数原型如下:
int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *attr);
cond参数指向要初始化的条件变量,attr参数指向描述条件变量属性的结构体。attr可以为NULL,表示使用默认属性。
2、等待
当程序中需要等待一个条件变量时,可以用下面的函数:
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指定的超时时间限制,当线程阻塞的时间超过了这个时间就会自动醒来。
3、触发
触发一个条件变量可以用以下函数:
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函数唤醒至少一个睡眠中的线程,并没有规定只唤醒一个。
4、销毁
条件变量不用之后,应该用下面的函数进行销毁:
int pthread_cond_destroy(pthread_cond_t *cond);