多线程的最大优点之一是数据的共享性,各个进程共享父进程处沿袭的数据段,可以方便的获得、修改数据。但这也给多线程编程带来了许多问题。
我们必须当心有多个不同的进程访问相同的变量。许多函数是不可重入的,即同时不能运行一个函数的多个拷贝(除非使用不同的数据段)。
在函数中声明的静态变量常常带来问题,函数的返回值也会有问题。因为如果返回的是函数内部静态声明的空间的地址,
则在一个线程调用该函数得到地址后使用该地址指向的数据时,别的线程可能调用此函数并修改了这一段数据。
为了保护变量,我们必须使用信号量、互斥等方法来保证我们对变量的正确使用。在Poxi标准中提供了互斥量,读写锁,条件变量实现互斥访问,下面意义探讨。
互斥量
假设多个线程向同一个文件写入数据,若对写入的顺序不加以管理和控制,那么最终生成的文件肯定是无法解析的。所以必须用互斥锁来保证一段时间内只有一个线程在写入文件,当一个线程写入完成后再给下一线程写入。
锁的创建
锁可以被动态或静态创建,可以用宏PTHREAD_MUTEX_INITIALIZER来静态的初始化锁,采用这种方式比较容易理解,互斥锁是pthread_mutex_t的结构体,而这个宏是一个结构常量,如下可以完成静态的初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
另外锁可以用pthread_mutex_init函数动态的创建,函数原型如下:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t * attr)
NULL参数表明使用默认属性,通常使用默认属性。
锁的属性
- 互斥锁的范围:可以指定是该进程与其他进程的同步还是同一进程内不同的线程之间的同步。可以设置为PTHREAD_PROCESS_SHARE和PTHREAD_PROCESS_PRIVATE。默认是后者,表示进程内使用锁。
#include <pthread.h>
int pthread_mutexattr_getpshared(const pthread_mutexattr_t *
restrict attr, int *restrict pshared);
int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,
int pshared);
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr); //~necessary, or weird EINVAL error occurs when operating on the mutex
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(mtx, &attr);
- 互斥锁的类型:PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
int pthread_mutexattr_settype(pthread_mutexattr_t *attr , int type)
int pthread_mutexattr_gettype(pthread_mutexattr_t *attr , int *type)
锁操作
//上锁
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_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。
锁销毁
int pthread_mutex_destory(pthread_mutex_t *mutex)
当锁没有被锁定时。可以通过调用pthread_mutex_destory释放锁占用的资源。
使用示列
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mutex ;
void *print_msg(void *arg){
int i=0;
pthread_mutex_lock(&mutex);
for(i=0;i<15;i++){
printf("output : %d\n",i);
usleep(100);
}
pthread_mutex_unlock(&mutex);
}
int main(int argc,char** argv){
pthread_t id1;
pthread_t id2;
pthread_mutex_init(&mutex,NULL);
pthread_create(&id1,NULL,print_msg,NULL);
pthread_create(&id2,NULL,print_msg,NULL);
pthread_join(id1,NULL);
pthread_join(id2,NULL);
pthread_mutex_destroy(&mutex);
return 1;
}
读写锁
多个线程可以同时获得读锁(Reader-Writer lock in read mode),但是只有一个线程能够获得写锁(Reader-writer lock in write mode)。
读写锁总共有三种状态:
1. 一个或者多个线程获得读锁,其他线程无法获得写锁
2. 一个线程获得写锁,其他线程无法获得读锁
3. 没有线程获得此读写锁
#include <pthread.h>
//初始化一个读写锁,pthread_rwlockattr_t通常设为NULL
int pthread_rwlock_init(
pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr)
//销毁一个读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
//获取读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//获取写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//读写解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
条件变量
适用的场景为条件满足立刻被唤醒,否则挂起等待。条件需要被mutex保护。
条件变量类型为pthread_cond_t,必须被初始化为PTHREAD_COND_INITIALIZER,等价于调用pthread_cond_init(…, NULL)
#include <pthread.h>
//初始化一个条件变量,和互斥锁一样我们可以用它来设置条件变量是进程内可用还是进程间可用,默认值是 PTHREAD_ PROCESS_PRIVATE,即此条件变量被同一进程内的各个线程使用。
int pthread_cond_init(
pthread_cond_t *restrict cond,
const pthread_condxattr_t *restrict attr)
//销毁一个条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
//等待条件发生
int pthread_cond_wait(
pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(
pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict timeout);
/*
pthread_cond_timedwait类似,只是当等待超时的时候返回一个错误值ETIMEDOUT。超时的时间用timespec结构指定。
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
注意timespec的时间是绝对时间而非相对时间,因此需要先调用gettimeofday函数获得当前时间,再转换成timespec结构,加上偏移量。
*/
//条件满足后唤醒等待的线程
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
/*两者的区别是前者会唤醒单个线程,而后者会唤醒多个线程。*/
示列
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t counter_lock;
pthread_cond_t counter_nonzero;
int counter = 0;
int estatus = -1;
void *decrement_counter(void *argv);
void *increment_counter(void *argv);
/*
thd1启动等待某个条件满足后,才开始运行。
thd2当thd1运行的条件满足后,开始唤醒thd1运行。
*/
int main(int argc, char **argv)
{
printf("counter: %d\n", counter);
pthread_t thd1, thd2;
int ret;
ret = pthread_create(&thd1, NULL, decrement_counter, NULL);
if(ret){
perror("del:\n");
return 1;
}
ret = pthread_create(&thd2, NULL, increment_counter, NULL);
if(ret){
perror("inc: \n");
return 1;
}
int counter = 0;
while(counter != 2){
printf("counter(main): %d\n", counter);
sleep(4);
counter++;
}
return 0;
}
void *decrement_counter(void *argv)
{
pthread_mutex_lock(&counter_lock);
printf("counter(decrement): %d decrement_counter lock! \n", counter);
while(counter == 0)
pthread_cond_wait(&counter_nonzero, &counter_lock); //进入阻塞(wait),等待唤醒信号
printf("counter--(before): %d\n", counter);
counter--; //等待signal激活后再执行
printf("counter--(after): %d\n", counter);
printf("decrement_counter will unlock! \n");
pthread_mutex_unlock(&counter_lock);
return &estatus;
}
void *increment_counter(void *argv)
{
//模拟thd1比等待线程后启动
sleep(1);
pthread_mutex_lock(&counter_lock);
printf("counter(increment): %d increment_counter lock!\n", counter);
printf("counter++(before): %d\n", counter);
counter++;
printf("counter++(after): %d\n", counter);
//thd1运行的条件满足后,开始唤醒挂起的thd1
if(counter != 0)
pthread_cond_signal(&counter_nonzero);
printf("increment_counter will unlock!\n");
pthread_mutex_unlock(&counter_lock);
return &estatus;
}
gcc -g -pthread cond.c -lpthread -o test 运行test后,输出如下:
counter: 0
counter(main): 0
counter(decrement): 0 decrement_counter lock!
counter(increment): 0 increment_counter lock!
counter++(before): 0
counter++(after): 1
increment_counter will unlock!
counter--(before): 1
counter--(after): 0
decrement_counter will unlock!
counter(main): 1
是不是觉得上面的输出有点不可思议?明明decrement_counter lock为什么increment_counter lock还能成功。关键在于 pthread_cond_wait(&counter_nonzero, &counter_lock)在等待是,实际上已经将锁unlock,并将线程挂起在等待队列,等待唤醒。pthread_cond_signal(&counter_nonzero); 实际上会对对应mutex一个lock操作。
线程数据
在单线程的程序里,有两种基本的数据:全局变量和局部变量。但在多线程程序里,还有第三种数据类型:线程数据(TSD: Thread-Specific Data)。
它和全局变量很象,在线程内部,各个函数可以象使用全局变量一样调用它,但它对线程外部的其它线程是不可见的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在 A线程里输出的很可能是B线程的出错信息。
要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。
/*
实现功能,创建5个线程,每个线程将日志记录到thread%d.log日志文件中
*/
#include <malloc.h>
#include <pthread.h>
#include <stdio.h>
static pthread_key_t thread_log_key;
void write_to_thread_log (const char* message){
//从键读取线程数据
FILE* thread_log = (FILE*) pthread_getspecific (thread_log_key);
fprintf (thread_log, "%s\n", message);
}
void close_thread_log (void* thread_log){
fclose ((FILE*) thread_log);
}
void* thread_function (void* args){
char thread_log_filename[20];
FILE* thread_log;
sprintf (thread_log_filename, "thread%d.log", (int) pthread_self ());
thread_log = fopen (thread_log_filename, "w+");
//为当前线程的键指定线程数据
pthread_setspecific (thread_log_key, thread_log);
//记录日志
write_to_thread_log ("Thread starting.");
return NULL;
}
int main (){
int i;
pthread_t threads[5];
//创建一个键,close_thread_log为线程退出时调用的析构函数
pthread_key_create (&thread_log_key, close_thread_log);
for (i = 0; i < 5; ++i)
pthread_create (&(threads[i]), NULL, thread_function, NULL);
for (i = 0; i < 5; ++i)
pthread_join (threads[i], NULL);
//销毁键
pthread_key_delete(thread_log_key);
return 0;
}