1.简介

多进程有一个明显的好处,即进程与进程之间的地址空间是相互独立的,这可以消除很多错误。

另一方面,相互独立的进程通信更加困难,而且因为需要保存较多信息,其上下文切换的开销更大。

线程相比进程来说更加轻量,它是运行在进程上下文中的逻辑流,进程中的线程共享了自身的虚拟地址空间。

线程和进程一样,由操作系统进行调度,其上下文包括线程ID、栈、栈指针、PC、通用目的寄存器和条件码。

2.API

2.1.创建线程

  1. #include <pthread.h>
  2. typedef void *(func)(void *);
  3. int pthread_create(pthread_t *tid,pthread_arrt_t *attr,func *f,void *arg);

其中tid为线程id、attr为线程属性,常用NULL作为默认、f为运行线程例程,arg为参数。

成功时返回0,否则完成错误码。

2.2.线程退出

  1. #include <pthread.h>
  2. int pthread_exit(void* retval);

线程结束时调用该函数,可以保证线程安全、干净地退出。

该函数通过retval向回收者传递退出信息。

2.3.线程回收

  1. #include <pthread.h>
  2. int pthread_join(pthread_t thread,void ** retval);

thread为线程tid,retval为退出信息。

2.4.终止线程

  1. #include <pthread.h>
  2. int pthread_cancel(pthread_t thread);
  3. int pthread_setcancelstate(int state,int *oldstate);
  4. int pthread_setcanceltype(int type,int *oldtype);

可以通过设置2、3函数设置线程是否允许被终止,以及如何终止。

3.线程控制

3.1.信号量

信号量本质上就是一个计数器,其值表示资源的控制权数量。若线程能够获取,则表示可以对临界区资源进行操控;否则需要进行阻塞或其他工作。

信号量相关的API有五个:

  1. #include <semaphore.h>
  2. int sem_init(sem_t* sem,int pshared,unsigned int value);
  3. int sem_destory(sem_t* sem);
  4. int sem_wait(sem_t* sem);
  5. int sem_trywait(sem_t* sem);
  6. 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时,与互斥锁相同。

  1. class sem
  2. {
  3. public:
  4. sem()
  5. {
  6. if( sem_init( &m_sem, 0, 0 ) != 0 )
  7. {
  8. throw std::exception();
  9. }
  10. }
  11. ~sem()
  12. {
  13. sem_destroy( &m_sem );
  14. }
  15. bool wait()
  16. {
  17. return sem_wait( &m_sem ) == 0;
  18. }
  19. bool post()
  20. {
  21. return sem_post( &m_sem ) == 0;
  22. }
  23. private:
  24. sem_t m_sem;
  25. };

3.2.互斥锁

用于独占式的访问临界区资源。

相关API如下:

  1. #include <pthread.h>
  2. int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t *mutexattr);
  3. int pthread_mutex_destory(pthread_mutex_t* mutex);
  4. int pthread_mutex_lock(pthread_mutex_t* mutex);
  5. int pthread_mutex_trylock(pthread_mutex_t* mutex);
  6. int pthread_mutex_unlock(pthread_mutex_t* mutex);

与信号量类似。

  1. class locker
  2. {
  3. public:
  4. locker()
  5. {
  6. if( pthread_mutex_init( &m_mutex, NULL ) != 0 )
  7. {
  8. throw std::exception();
  9. }
  10. }
  11. ~locker()
  12. {
  13. pthread_mutex_destroy( &m_mutex );
  14. }
  15. bool lock()
  16. {
  17. return pthread_mutex_lock( &m_mutex ) == 0;
  18. }
  19. bool unlock()
  20. {
  21. return pthread_mutex_unlock( &m_mutex ) == 0;
  22. }
  23. private:
  24. pthread_mutex_t m_mutex;
  25. };

3.3.条件变量

条件变量的思想是,与其轮询某一个状态是否为真,不如利用条件变量在该状态为非真时将其阻塞,并在为真时唤醒并运行。

条件变量本质是一种阻塞线程,从而减少对互斥锁的竞争的手段。对于一个临界区,与其获取锁去检查状态,不如让正在对临界区进行操作的线程在满足条件时,再通知其他进程可以对临界区的数据进行修改。

条件变量本身是由互斥锁保护的,线程在改变条件变量前必须锁住互斥锁,其他线程在获取互斥锁之前不会察觉到条件变量的变化。

其相关API如下:

  1. #include <pthread.h>
  2. int pthread_cond_init(pthread_cond_t* cond,const pthread_condattr_t* cond_attr);
  3. int pthread_cond_destory(pthread_cond_t* cond);
  4. int pthread_cond_broadcast(pthread_cond_t* cond);
  5. int pthread_cond_signal(pthread_cond_t* cond);
  6. int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex);
  7. int pthread_cond_timedwait(pthread_cond_t* cond,
  8. pthread_mutex_t* mutex,
  9. 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并处理信号。