linux多线程--POSIX Threads Programming

571 查看

linux多线程自己从接触很久也有不少实践,但总是觉得理解不够深刻,不够系统。借这篇文章试着再次系统学习一下linux多线程编程,理解编程的concept,细致看一下POSIX pthread API的实现。还是凭借强大的google search,找到几篇不错的文章和教程附在最后。我在这篇文章中的总结大多都是基于这些材料的学习和自己实践经验的一点总结。

Thread基本知识

Process 地址空间

Thread附着在process内部,先看一下process在CPU上是个什么样的吧。启动一个linux process,OS会开辟一块内存用来装载code,保存data和process状态。看一下进程的地址空间。


根据访问权限,进程地址空间分为user space和kernel space。32bit系统中高1G为kernel space,低3G为user space,具体划分为:

  • Process control block(从高1G kernel space中分配)

  • stack

  • memory mapping segment

  • heap

  • bss and data

  • text

1G的kernel space是这台机器上所有processes共享的,每个进程的PCB存在这个空间中,一般应用程序是没有办法直接访问修改的,但是kernel 通过/proc 提供给应用程序一个接口可以查看PCB的信息,部分内容还可以修改,详细可以看一下/proc。剩下的stack/heap/text/...都驻留在process user space,是属于process私有空间。详细的kernel如何管理进程memory还可以再开一篇。

Thread是什么?

process是个重型的运行实体,以process为单位切分任务和调度,os的开销太大了。我们可以把process这个单位再切小些,thread的概念就诞生了。好,我们来看一下怎样把这个单位切小的。简单来讲,thread共享大部分的process的内容,只维护必需的一小部分作为私有内容。

Thread自己维护的私有内容

  1. Kernel space

    • Stack pointer

    • Registers

    • Scheduling properties (such as policy or priority)

    • Set of pending and blocked signals

    • Thread specific data.

  2. User space

    • stack

其他诸如PCB中进程信息,用户空间中的text/data/heap/...都是同一个process下所有Threads共享的。有了这些thread自己私有的信息,os就可以以thread为单位去调度了。因为它是轻量级的,所以相比process,thread一般具有更好的性能,更快的响应速度。但是thread的稳定性和编程复杂度要比process差些,要考虑的内容比较多。

Threads通信和同步

正因为同一个process内的threads间天然共享了大量的内存,thread间的信息交互要比较高效,同时也增加了复杂度,总要处理好共享内存间的互斥。当然process间也可以共享内存,比如通过进程父子关系,或者通过/dev/shm mmap特定物理内存到进程空间内或者其他。

线程间通信


所有的IPC(inter process communication)方法都适用于thread间的通信。比较全的IPC总结,可以参考IPC。比较常用的我们会涉及到message queue,sharememory,semaphore,socket,signal等。semaphore是共享资源互斥的方法,其他都是冗余的方式进行通信。互斥是个比较复杂的话题,我们单开一节讨论一下。

共享资源的互斥

为什么要保护共享资源做互斥访问,这里不罗嗦了。通过对共享资源(临界区)加锁可以实现互斥访问,互斥锁(mutex)也有多种类型。

  • simple blocking
    一方拿到临界区锁后,其它人再来拿锁都会挂起。

  • Recursive(递归型)
    允许锁的拥有者多次申请锁而不被挂起,对递归调用有用。

  • Reader/Writer
    允许多个reader同时share读锁,如果有reader在读,writer申请锁会block直到所有reader释放。可以理解为一写多读,写时互斥。这种锁有写饿死的风险。

其中POSIX的pthread库支持recursive和reader/writer类型的锁。

共享访问带来的风险和挑战

共享访问中有写操作,必然要考虑互斥。互斥有风险,使用需谨慎。如果你最终不可避免的要使用互斥锁,要关注互斥锁的这些风险。

  1. deadlock(死锁)
    死锁一般发生在双方或者多方在申请两个以上的互斥锁,然后大家各拿了部分,互不相让。开发者要尽量避免这种编程场景发生,如果真的需要可以编程要么同时获得,要么一个都不要,做人要有骨气!

  2. race condition(竞争条件)
    共享资源在没有互斥机制保护时,由于线程调度的不确定性会导致共享的资源变化无序无规律,程序的输出也就不确定了。共享资源无互斥保护,线程间竞争访问,输出无法保证。这要求开发者要特别小心识别出程序中的那些共享资源,加锁保护。尤其是第三方的开源软件,多线程调用时要注意是否是线程安全的。

  3. priority reversion(优先级反转)
    优先级反转是个很有意思的问题,尤其是在嵌入式实时OS上,进程/线程的调度是抢占式的,高优先级的任务ready时可以直接抢占CPU,这事再加上互斥就容易出问题了。比如三个任务H,M,L,优先级递减,同时H和L共享资源R。当L先申请到互斥锁访问临界区还没释放R的时候,H这时候申请R访问导致自己挂起,这么巧M变ready了,OS调度让M抢占了L的cpu。如果L一直得不到执行并释放R,这样就造成了高优先级的H得不到执行,反而一些比H优先级低的M们能得到CPU。这就是优先级反转。实时OS的高优先级任务一般都是比较重要的任务需要马上处理,得不到处理意味着可能要出大事。所以这个问题的影响还是挺大的,比较著名的例子就是火星探路者的故事,可以参考一下火星探路者故障分析。解决方法也有不少

    • 尽量避免不同优先级的任务共享资源,可以通过信息容易做任务间通信。

    • 访问临界区时关闭中断,保证临界区的代码执行不被强占。嵌入式编程中常用。

    • 优先级继承,当有高优先级任务想要访问共享资源时,提高正在执行的低优先级任务的优先级到高优先级级别直至退出临界区。上面的探路者修正程序使用了该方法。

    • 随机提高ready且持有锁的任务优先级,windows用了该方法。

Multi Threads应用场景

写了这么多,那到底什么时候可以应用多线程来解决问题呢?根据经验,一般下面一些场景我们可以考虑使用多线程。

  • 多核处理器,任务比较容易切分为并行处理的小块。如果是计算密集型的,线程数量可以考虑跟core的数量相当。

  • 有delay比较多的IO操作,可以考虑将IO的操作分离给单独的线程。

  • 有人机交互和实时响应等实时性要求较高任务,可以考虑分离为优先级较高的线程。

  • 有大量实时要求不高的计算,可以考虑分离为优先级较低的后台任务。

Thread编程模型

实事求是,具体问题具体分析是放之四海而皆准的问题解决之道,所以没有普适的编程模型。下面列举3种应用比较多的模型以供学习。

  1. Thread Pool (Master/Worker)
    通过线程池维护一组可用的线程,master作为主线程负责管理维护worker线程,同时负责对外接口和工作的分发。

  2. Peer (Workcrew)
    跟master/worker类似,只是master在启动线程池后退化为普通一员,大家一起分担任务,没有主从的星形拓扑结构。

  3. Pipeline
    跟CPU的pipline技术类似,将一个工作流分成很多串行的部分,每一部分都由不同的线程负责,大家各司其职,我做完我的工作就转交给下一个线程,齐心协力最后完成整个工作。流水线如果拍的好可以很好的提高工作效率,但是这种模型风险也比较大,一定要处理好工作的切分,和线程间的交互。

POSIX API详解

Thread management

pthread_create (thread,attr,start_routine,arg) #创建thread
pthread_exit (status) # thread退出
pthread_cancel (thread) # 退出指定的thread
pthread_attr_init (attr) #初始化thread属性
pthread_attr_destroy(attr)
pthread_setaffinity_np or sched_setaffinity # 设置thread可运行的CPU,也就是绑定CPU
pthread_join (threadid,status) # 阻塞等待threadid指定的thread完成
pthread_detach (threadid) # 线程创建默认是joinable,调用该函数设置线程的状态为detached,则该线程运行结束后会自动释放所有资源,别人再join等待它时候不会阻塞了。
pthread_attr_setdetachstate (attr,detachstate)
pthread_attr_getdetachstate (attr,detachstate)
pthread_self () # 返回自己所在的线程id
pthread_equal (thread1,thread2) # 比较两个线程

大部分API见名思意比较简单,详细看一下pthread_create.

   #include <pthread.h>
   int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
   
   参数说明:
   thread: 指针,所指向的内存存储新创建thread的属性,返回给caller来标识该线程
   attr: thread的配置参数集
   start_routine: thread创建后执行的处理函数,thread的主体
   arg: start_routine的入参
   
   功能说明:
   创建thread API,成功后返回0. 创建的thread跟创建者是平行关系,没有等级继承关系。
   thread有以下属性
        Detached or joinable state
        Scheduling inheritance
        Scheduling policy
        Scheduling parameters
        Scheduling contention scope
        Stack size
        Stack address
        Stack guard (overflow) size
   

Mutexes

pthread_mutex_init (mutex,attr) # 动态生成一个mutex变量
pthread_mutex_destroy (mutex) # 释放mutex变量
pthread_mutexattr_init (attr) # 设置mutex属性
pthread_mutexattr_destroy (attr)
pthread_mutex_lock (mutex) # lock操作,如果mutex已经lock调用者会阻塞
pthread_mutex_trylock (mutex) # 尝试lock,非阻塞调用
pthread_mutex_unlock (mutex) # unlock操作

Condition variables

pthread_cond_init (condition,attr)
pthread_cond_destroy (condition)
pthread_condattr_init (attr)
pthread_condattr_destroy (attr)
pthread_cond_wait (condition,mutex) # 调用者阻塞直到condition条件成立,注意调用者阻塞时会自动释放mutex,唤醒时会自动lock mutex。调用前确保lock mutex,调用后确保调用unlock mutex
pthread_cond_signal (condition) # 通知对方条件满足,调用前确保lock mutex,调用后确保调用unlock mutex
pthread_cond_broadcast (condition)

条件变量是另外一种线程间同步的方式,其实是一种挂起和唤醒的通信方式。可以理解为定义一个条件变量定义了一个线程间的通信通道,wait这个变量一方其实是在等待有人在这个通道上发个信号来,如果没有人发信号他就一直阻塞挂起。它需要跟mutex配合使用,直接通过一个例子感受一下。条件变量的存在就是让wait的这一方睡起来直到有人通知它条件满足可以起来干活了,否则没有条件变量只用mutex做同步,这个wait的一方需要不断的查询是否条件满足,低效浪费。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>

#define NUM_THREADS  3
#define TCOUNT 10
#define COUNT_LIMIT 12

int     count = 0;
pthread_mutex_t count_mutex;
pthread_cond_t count_threshold_cv;

void *inc_count(void *t) 
{
  int i;
  long my_id = (long)t;

  for (i=0; i < TCOUNT; i++) {
    pthread_mutex_lock(&count_mutex);
    count++;

    /* 
    Check the value of count and signal waiting thread when condition is reached.  Note that this occurs while mutex is locked. 
    */
    if (count == COUNT_LIMIT) {
      printf("inc_count(): thread %ld, count = %d  Threshold reached. ",
             my_id, count);
      pthread_cond_signal(&count_threshold_cv);
      printf("Just sent signal.\n");
      }
    printf("inc_count(): thread %ld, count = %d, unlocking mutex\n", 
       my_id, count);
    pthread_mutex_unlock(&count_mutex);

    /* Do some work so threads can alternate on mutex lock */
    sleep(1);
    }
  pthread_exit(NULL);
}
void *watch_count(void *t) 
{
  long my_id = (long)t;

  printf("Starting watch_count(): thread %ld\n", my_id);

  /*
  Lock mutex and wait for signal.  Note that the pthread_cond_wait routine
  will automatically and atomically unlock mutex while it waits. 
  Also, note that if COUNT_LIMIT is reached before this routine is run by
  the waiting thread, the loop will be skipped to prevent pthread_cond_wait
  from never returning.
  */
  pthread_mutex_lock(&count_mutex);
  while (count < COUNT_LIMIT) {
    printf("watch_count(): thread %ld Count= %d. Going into wait...\n", my_id,count);
    pthread_cond_wait(&count_threshold_cv, &count_mutex);
    printf("watch_count(): thread %ld Condition signal received. Count= %d\n", my_id,count);
    printf("watch_count(): thread %ld Updating the value of count...\n", my_id,count);
    count += 125;
    printf("watch_count(): thread %ld count now = %d.\n", my_id, count);
    }
  printf("watch_count(): thread %ld Unlocking mutex.\n", my_id);
  pthread_mutex_unlock(&count_mutex);
  pthread_exit(NULL);
}

int main(int argc, char *argv[])
{
  int i, rc; 
  long t1=1, t2=2, t3=3;
  pthread_t threads[3];
  pthread_attr_t attr;

  /* Initialize mutex and condition variable objects */
  pthread_mutex_init(&count_mutex, NULL);
  pthread_cond_init (&count_threshold_cv, NULL);

  /* For portability, explicitly create threads in a joinable state */
  pthread_attr_init(&attr);
  pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
  pthread_create(&threads[0], &attr, watch_count, (void *)t1);
  pthread_create(&threads[1], &attr, inc_count, (void *)t2);
  pthread_create(&threads[2], &attr, inc_count, (void *)t3);

  /* Wait for all threads to complete */
  for (i = 0; i < NUM_THREADS; i++) {
    pthread_join(threads[i], NULL);
  }
  printf ("Main(): Waited and joined with %d threads. Final value of count = %d. Done.\n", 
          NUM_THREADS, count);

  /* Clean up and exit */
  pthread_attr_destroy(&attr);
  pthread_mutex_destroy(&count_mutex);
  pthread_cond_destroy(&count_threshold_cv);
  pthread_exit (NULL);

}

Synchronization

pthread_rwlock_destroy
pthread_rwlock_init
pthread_rwlock_rdlock
pthread_rwlock_timedrdlock
pthread_rwlock_timedwrlock
pthread_rwlock_tryrdlock
pthread_rwlock_trywrlock
pthread_rwlock_unlock
pthread_rwlock_wrlock
pthread_rwlockattr_destroy
pthread_rwlockattr_getpshared
pthread_rwlockattr_init
pthread_rwlockattr_setpshared

上面提到的读写锁。允许多个reader同时share读锁,如果有reader在读,writer申请锁会block直到所有reader释放。

参考文章
POSIX Threads Programming
Multithreaded Programming (POSIX pthreads Tutorial)
Multi-Threaded Programming With POSIX Threads
POSIX thread (pthread) libraries