Table of Contents
一、概要介绍
多线程开发在 Linux 平台上已经有成熟的 Pthread 库支持。其涉及的多线程开发的最基本概念主要包含三点:线程,互斥锁,条件变量。其中,线程操作主要有线程的创建,退出,分离和joinable 。互斥锁则包括 4 种操作,分别是创建,销毁,加锁和解锁。条件操作有 5 种操作:创建,销毁,触发,广播和等待。其他的一些线程扩展概念,如信号灯等,都可以通过上面的三个基本元素的基本操作封装出来。简单可以参考下表:
对象 | 操作 | Linux Pthread API |
---|---|---|
线程 | 创建 | pthread_create |
退出 | pthread_exit | |
等待 | pthread_join | |
互斥锁 | 创建 | pthread_mutex_init |
销毁 | pthread_mutex_destroy | |
加锁 | pthread_mutex_lock | |
解锁 | pthread_mutex_unlock | |
条件 | 创建 | pthread_cond_init |
销毁 | pthread_cond_destroy | |
触发 | pthread_cond_signal | |
广播 | pthread_cond_broadcast | |
等待 | pthread_cond_wait / pthread_cond_timedwait |
这里记录下一些pthread的接口,供复习使用。
二、pthread线程
线程创建
int pthread_create(pthread_t *thread, pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
参数:
pthread_t *thread: the actual thread object that contains pthread id
pthread_attr_t *attr: attributes to apply to this thread
void *(*start_routine)(void *): the function this thread executes
void *arg: arguments to pass to thread function above
线程创建时应保证线程ID thread被成功创建(检查返回值);线程属性attr默认值为NULL,可设置为其他属性;start_routine是创建线程后所执行的函数体;arg是传入的参数。
线程等待和终止
void pthread_exit(void *value_ptr);
int pthread_join(pthread_t thread, void **value_ptr);
pthread_exit() 终止线程,并且提供指针变量*value_ptr给pthread_join()调用
pthread_join() 阻塞调用线程调用,并等待线程结束,得到可选返回值*value_ptr。
示例1:
//注:pthread_join()是阻塞的,可接收pthread_exit()返回的线程结果信息
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
static int NUM = 2;
typedef struct _thread_data{
int thread_id;
char str[10];
}thread_data;
void *func(void *arg) {
thread_data *data = (thread_data*) arg;
printf("self-defined thread id is: %d.\n", data->thread_id);
pthread_exit(NULL);
}
int main(int argc, char **argv) {
pthread_t thr[NUM];
thread_data thr_data[NUM];
int i, ret;
for (i = 0; i < NUM; i++) {
thr_data[i].thread_id = i;
if ((ret = pthread_create(&thr[i], NULL, func, &thr_data[i])) != 0) {
fprintf(stderr, "pthread create error, error num: %d.\n", ret);
exit(EXIT_FAILURE);
}
}
for (i = 0; i < NUM; i++)
pthread_join(thr[i], NULL);
return 0;
}
线程属性
属性的初始化、属性的设置set、属性的获取get等等。
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
int pthread_attr_setscope(pthread_attr_t *attr, int contentionscope);
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
示例2:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
static int NUM = 2;
typedef struct _thread_data{
int thread_id;
char str[10];
}thread_data;
void *func(void *arg) {
thread_data *data = (thread_data*) arg;
printf("self-defined thread id is: %d.\n", data->thread_id);
pthread_exit(NULL);
}
int main(int argc, char **argv) {
// initialize and set thread detached.
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_t thr[NUM];
thread_data thr_data[NUM];
int i, ret;
for (i = 0; i < NUM; i++) {
thr_data[i].thread_id = i;
if ((ret = pthread_create(&thr[i], NULL, func, &thr_data[i])) != 0) {
fprintf(stderr, "pthread create error, error num: %d.\n", ret);
exit(EXIT_FAILURE);
}
}
pthread_attr_destroy(&attr);
//sleep(5);
return 0;
}
示例1和示例2中代码的区别在于pthread的属性不同:一个是默认创建的joinable,另一个是手动设置的detached状态(通过设置线程属性PTHREAD_CREATE_DETACHED)。这对应了两种线程资源回收的情况。当线程状态为joinable的时候,需要对所有joinable的线程使用pthread_join来回收线程栈资源;当线程状态为detached的时候,在线程结束exit之后,系统会自动回收线程栈的资源。如果对线程状态的设置不合理,会导致内存泄漏,后面会专门写一篇通过valgrind检查内存泄漏的博客。
总结一下,pthread_create创建线程的时候默认的状态是joinable。如果不需要对获取线程结束后的返回状态,就把线程状态设为detached,让系统自动回收栈资源;如果需要获取线程结束后的返回,那么记得对每一个joinable的线程执行pthread_join,手动回收线程的栈空间。
三、互斥锁
//动态初始化,在结束的时候必须要执行destroy释放资源
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; //静态初始化,不需要执行destroy
int pthread_mutex_destroy(pthread_mutex_t *mutex); //mutex 指向要销毁的互斥锁的指针
//acquire a lock on the specified mutex variable. If the mutex is already locked by another thread, this call will block the calling thread until the mutex is unlocked.
int pthread_mutex_lock(pthread_mutex_t *mutex);
// attempt to lock a mutex or will return error code if busy. Useful for preventing deadlock conditions.
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//unlock a mutex variable. An error is returned if mutex is already unlocked or owned by another thread.
int pthread_mutex_unlock(pthread_mutex_t *mutex);
示例3:
通过互斥锁,保证多线程对互斥信号量的访问不会冲突。这里使用锁保证两个线程分别对count值执行+1操作不会产生冲突。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
int count = 0;
void *counter(void *arg) {
pthread_mutex_lock(&mutex1);
count++;
printf("count value: %d.\n", count);
pthread_mutex_unlock(&mutex1);
}
int main(int argc, char **argv) {
pthread_t thread1, thread2;
if (pthread_create(&thread1, NULL, counter, NULL) != 0)
printf("thread1 create error!\n");
if (pthread_create(&thread2, NULL, counter, NULL) != 0)
printf("thread2 create error!\n");
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// Wait till threads are complete before main continues.
return 0;
}
四、条件变量
//动态初始化,在结束使用的时候必须要执行destroy操作
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //静态初始化,不需要执行destroy
int pthread_cond_destroy(pthread_cond_t *cond); //销毁条件变量
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); //可以理解为P操作
int pthread_cond_signal(pthread_cond_t *cond); //可以理解为V操作
int pthread_cond_broadcast(pthread_cond_t *cond); //广播
示例4:
通过设置静态变量,让两个线程按照设置输出1-10十个数字。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
const int COUNT_DONE = 10;
const int COUNT_LOWER = 3;
const int COUNT_HIGHER = 6;
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond1 = PTHREAD_COND_INITIALIZER;
void *func1(void *arg);
void *func2(void *arg);
int count = 0;
int main(int argc, char **argv) {
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, func1, NULL);
pthread_create(&thread2, NULL, func2, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("final count is: %d.\n", count);
return 0;
}
// write number 1-3 and 8-10
void *func1(void *arg) {
while (1) {
pthread_mutex_lock(&mutex1);
// use while-loop instead of if-loop to prevent spurious wakeups.
// the different is while-loop judge one more time than if-loop.
pthread_cond_wait(&cond1, &mutex1);
count++;
printf("add count by func1, now count is: %d.\n", count);
pthread_mutex_unlock(&mutex1);
if (count >= COUNT_DONE)
return NULL;
}
}
// write number 4-7
void *func2(void *arg) {
while (1) {
pthread_mutex_lock(&mutex1);
if (count >= COUNT_LOWER && count <= COUNT_HIGHER) {
count++;
printf("add count by func2, now count is: %d.\n", count);
}
else {
pthread_cond_signal(&cond1);
}
pthread_mutex_unlock(&mutex1);
if (count >= COUNT_DONE)
return NULL;
}
}
这是条件变量的一个简单应用,但是没有显示出条件变量中的一些问题,下面会在总结中详细分析。
五、总结
条件变量中的虚假唤醒问题
Linux文档中提到spurious wakeups(虚假唤醒):
在多核处理器下,pthread_cond_signal可能会激活多于一个线程(阻塞在条件变量上的线程)。
On a multi-processor, it may be impossible for an implementation of pthread_cond_signal()
to avoid the unblocking of more than one thread blocked on a condition variable.
结果是,当一个线程调用pthread_cond_signal()后,多个调用pthread_cond_wait()pthread_cond_timedwait()
的线程返回。这种效应成为”虚假唤醒”(spurious wakeup)。
The effect is that more than one thread can return from its call to pthread_cond_wait() or
pthread_cond_timedwait() as a result of one call to pthread_cond_signal(). This effect is
called “spurious wakeup”. Note that the situation is self-correcting in that the number of
threads that are so awakened is finite; for example, the next thread to callpthread_cond_wait()
after the sequence of events above blocks.
虽然虚假唤醒在pthread_cond_wait函数中可以解决,为了发生概率很低的情况而降低边缘条件(fringe
condition)效率是不值得的,纠正这个问题会降低对所有基于它的所有更高级的同步操作的并发度。所以
pthread_cond_wait的实现上没有去解决它。
While this problem could be resolved, the loss of efficiency for a fringe condition that
occurs only rarely is unacceptable, especially given that one has to check the predicate
associated with a condition variable anyway. Correcting this problem would unnecessarily
reduce the degree of concurrency in this basic building block for all higher-level
synchronization operations.
由于spurious wakeups现象,所以每次wait被唤醒都需要检查一遍状态,然后决定是否继续执行。现在有两个选择:while 和 if ,
while (条件不满足){
condition_wait(cond, mutex);
}
而不是:
if (条件不满足){
condition_wait(cond,mutex);
}
如果选择 if ,那么wait被唤醒了之后,就会直接执行下面的逻辑,这样是不对的。因此应该选择while循环,通过多一次的检查,决定是否继续执行后续逻辑,如果满足则执行,不满足就继续wait。
值得注意的是,虚假唤醒在Linux的多处理器系统中可能会发生。在Windows系统和JAVA虚拟机上也存在。在POSIX Threads中:
David R. Butenhof 认为多核系统中 条件竞争(race condition)导致了虚假唤醒的发生,并且认为完全消除虚假唤醒本质上会降低了条件变量的操作性能。因此,现在主流的解决方式是在wait前面添加while循环来避免虚假唤醒造成的错误。
pthread_cond_wait() 函数
使用条件变量时,wait和signal函数基本对应之前经常讲的P操作和V操作。在wait和signal操作前,线程都会先对申请加锁。如果signal先加锁,那么wait进行到pthread_mutex_lock的时候就阻塞住了,等signal解锁之后,wait才继续往下执行;如果wait先加锁,那么signal也同样会在lock处阻塞。这是互斥量的作用,互斥量加锁阻塞的是一段代码。
如果线程在wait处阻塞的话,如果该线程仍然持有锁并且不断定期检查自己条件是否满足,这样非常浪费资源。红圈2中也告诉哦我们,如果一个线程处于wait状态的话,就不应该继续持有锁。所以我们来分析下pthread_cond_wait() 的动作。
调用pthread_cond_wait()函数时,系统使调用线程进入等待状态后释放锁(所以我们必须先加锁后调用wait函数)。
在这一步操作中的检查条件和进入等待是原子操作,所以线程不会错过条件的变化。当wait函数返回时,mutex会再次
被加锁。
所以,pthread_cond_wait() 会休眠并且释放锁,等被signal唤醒之后才重新获得锁。
joinabler和detach
pthread中有pthread_attr_t 类型,通过该类型变量可以对线程进程设置。线程资源释放有两种方式:自动回收和手动释放。pthread_create创建线程的时候默认的状态是joinable。如果不需要对获取线程结束后的返回状态,就把线程状态设为detached,让系统自动回收栈资源;如果需要获取线程结束后的返回,那么记得对每一个joinable的线程执行pthread_join,手动回收线程的栈空间。