1.简介
多进程有一个明显的好处,即进程与进程之间的地址空间是相互独立的,这可以消除很多错误。
另一方面,相互独立的进程通信更加困难,而且因为需要保存较多信息,其上下文切换的开销更大。
线程相比进程来说更加轻量,它是运行在进程上下文中的逻辑流,进程中的线程共享了自身的虚拟地址空间。
线程和进程一样,由操作系统进行调度,其上下文包括线程ID、栈、栈指针、PC、通用目的寄存器和条件码。
2.API
2.1.创建线程
#include <pthread.h>
typedef void *(func)(void *);
int pthread_create(pthread_t *tid,pthread_arrt_t *attr,func *f,void *arg);
其中tid为线程id、attr为线程属性,常用NULL作为默认、f为运行线程例程,arg为参数。
成功时返回0,否则完成错误码。
2.2.线程退出
#include <pthread.h>
int pthread_exit(void* retval);
线程结束时调用该函数,可以保证线程安全、干净地退出。
该函数通过retval向回收者传递退出信息。
2.3.线程回收
#include <pthread.h>
int pthread_join(pthread_t thread,void ** retval);
thread为线程tid,retval为退出信息。
2.4.终止线程
#include <pthread.h>
int pthread_cancel(pthread_t thread);
int pthread_setcancelstate(int state,int *oldstate);
int pthread_setcanceltype(int type,int *oldtype);
可以通过设置2、3函数设置线程是否允许被终止,以及如何终止。
3.线程控制
3.1.信号量
信号量本质上就是一个计数器,其值表示资源的控制权数量。若线程能够获取,则表示可以对临界区资源进行操控;否则需要进行阻塞或其他工作。
信号量相关的API有五个:
#include <semaphore.h>
int sem_init(sem_t* sem,int pshared,unsigned int value);
int sem_destory(sem_t* sem);
int sem_wait(sem_t* sem);
int sem_trywait(sem_t* sem);
int sem_post(sem_t* sem);
sem_init用于初始化信号量、pshared用于指定信号量的类型,若为0则为局部信号量,否则可以在多个进程间共享、value为初始值。
sem_destory用于摧毁信号量;
sem_wait原子地将信号量减1,若为0则阻塞,直到有非0值;sem_trywait是sem_wait的非阻塞版本,若返回值为-1时表示无法对信号量-1。
sem_post原子地将信号量加1。
当信号量的初始值为1时,与互斥锁相同。
class sem
{
public:
sem()
{
if( sem_init( &m_sem, 0, 0 ) != 0 )
{
throw std::exception();
}
}
~sem()
{
sem_destroy( &m_sem );
}
bool wait()
{
return sem_wait( &m_sem ) == 0;
}
bool post()
{
return sem_post( &m_sem ) == 0;
}
private:
sem_t m_sem;
};
3.2.互斥锁
用于独占式的访问临界区资源。
相关API如下:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t *mutexattr);
int pthread_mutex_destory(pthread_mutex_t* mutex);
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
与信号量类似。
class locker
{
public:
locker()
{
if( pthread_mutex_init( &m_mutex, NULL ) != 0 )
{
throw std::exception();
}
}
~locker()
{
pthread_mutex_destroy( &m_mutex );
}
bool lock()
{
return pthread_mutex_lock( &m_mutex ) == 0;
}
bool unlock()
{
return pthread_mutex_unlock( &m_mutex ) == 0;
}
private:
pthread_mutex_t m_mutex;
};
3.3.条件变量
条件变量的思想是,与其轮询某一个状态是否为真,不如利用条件变量在该状态为非真时将其阻塞,并在为真时唤醒并运行。
条件变量本质是一种阻塞线程,从而减少对互斥锁的竞争的手段。对于一个临界区,与其获取锁去检查状态,不如让正在对临界区进行操作的线程在满足条件时,再通知其他进程可以对临界区的数据进行修改。
条件变量本身是由互斥锁保护的,线程在改变条件变量前必须锁住互斥锁,其他线程在获取互斥锁之前不会察觉到条件变量的变化。
其相关API如下:
#include <pthread.h>
int pthread_cond_init(pthread_cond_t* cond,const pthread_condattr_t* cond_attr);
int pthread_cond_destory(pthread_cond_t* cond);
int pthread_cond_broadcast(pthread_cond_t* cond);
int pthread_cond_signal(pthread_cond_t* cond);
int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex);
int pthread_cond_timedwait(pthread_cond_t* cond,
pthread_mutex_t* mutex,
const struct timespec *restrict rsptr);
pthread_cond_broadcast利用广播的方式唤醒所有等待条件变量的线程,而pthread_cond_signal用于唤醒一个等待条件变量的线程,但无法唤醒一个特定的线程。
唯一唤醒特定线程的方式是通过间接的方式,如用一个全局变量标记目标线程的tid,然后广播并在函数中进行让线程与该全局变量进行比对。
pthread_cond_wait用于等待目标条件变量,在调用钱需要保证互斥锁已经加锁。在该函数执行时,它会先讲线程放入该条件变量的等待队列中,然后解锁互斥锁。
当该函数返回时,互斥锁会被再次锁上。
class cond
{
public:
cond()
{
if( pthread_mutex_init( &m_mutex, NULL ) != 0 )
{
throw std::exception();
}
if ( pthread_cond_init( &m_cond, NULL ) != 0 )
{
pthread_mutex_destroy( &m_mutex );
throw std::exception();
}
}
~cond()
{
pthread_mutex_destroy( &m_mutex );
pthread_cond_destroy( &m_cond );
}
bool wait()
{
int ret = 0;
pthread_mutex_lock( &m_mutex );
ret = pthread_cond_wait( &m_cond, &m_mutex );
pthread_mutex_unlock( &m_mutex );
return ret == 0;
}
bool signal()
{
return pthread_cond_signal( &m_cond ) == 0;
}
private:
pthread_mutex_t m_mutex;
pthread_cond_t m_cond;
};
3.4.自旋锁
自旋锁在获取不到锁之前会处于忙等状态,即不断轮询锁的状况。
自旋锁可以用于锁持有的时间短,且线程不希望在重新调度上花费太多时间成本。
自旋锁在非抢占式内核中很有用,除了提供互斥,还会阻塞中断,从而终端处理程序不会让系统陷入死锁状态。
但是在用户层中,因为调度机制,在线程拥有自旋锁的时候可能会进入休眠,则其他需要获取自旋锁的线程可能会阻塞比预想更多的时间。
#include <pthread.h>
int pthread_spin_init(pthread_spin_t* cond,int pshared);
int pthread_spin_destory(pthread_spin_t* lock);
int pthread_spin_lock(pthread_spin_t* lock);
int pthread_spin_trylock(pthread_spin_t* lock);
int pthread_spin_unlock(pthread_spin_t* lock);
3.5.其他
读写锁是细分读写粒度,拥有更高并行性的互斥锁。
屏障是用户协调多个线程并行工作的同步机制,允许每个线程等待,知道所有的合作线程都到某一点,然后再继续执行。
4.进程与线程
由于若一个线程执行fork之后,子进程会获得与进程同样的状态副本,因此这也包括锁的上锁状态,因此可能就会造成错误。
pthread提供了一个函数:
#include <pthread.h>
int pthread_atfork(void (*prepare)(void),void (*parent)(void),void (*child)(void));
- prepare在fork调用创建出子进程前进行,可以用于锁住所有互斥锁
- parent在fork调用创建出子进程后进行,用于释放所有prepare中锁住的互斥锁,在父进程中执行
- child在fork与parent一样,但是在子进程中执行
#include <pthread.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <wait.h>
pthread_mutex_t mutex;
void* another( void* arg )
{
printf( "in child thread, lock the mutex\n" );
pthread_mutex_lock( &mutex );
sleep( 5 );
pthread_mutex_unlock( &mutex );
}
void prepare()
{
pthread_mutex_lock( &mutex );
}
void infork()
{
pthread_mutex_unlock( &mutex );
}
int main()
{
pthread_mutex_init( &mutex, NULL );
pthread_t id;
pthread_create( &id, NULL, another, NULL );
//pthread_atfork( prepare, infork, infork );
sleep( 1 );
int pid = fork();
if( pid < 0 )
{
pthread_join( id, NULL );
pthread_mutex_destroy( &mutex );
return 1;
}
else if( pid == 0 )
{
printf( "I anm in the child, want to get the lock\n" );
pthread_mutex_lock( &mutex );
printf( "I can not run to here, oop...\n" );
pthread_mutex_unlock( &mutex );
exit( 0 );
}
else
{
pthread_mutex_unlock( &mutex );
wait( NULL );
}
pthread_join( id, NULL );
pthread_mutex_destroy( &mutex );
return 0;
}
5.线程与信号
线程也可以独立的设置信号掩码,但所有线程共享信号处理函数。
同时,多线程环境下,无法确定哪个线程收到了信号。因此,若要捕捉信号,应该单独创建一个线程用于处理信号。
- 1.主线程在创建出线程时,利用pthread_sigmask设置信号掩码
- 2.在一个专门处理信号的线程中调用sigwait并处理信号。