linux多线程--POSIX Threads Programming

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

Thread基本知识

Process 地址空间

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

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

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

  • stack安全

  • memory mapping segment多线程

  • heapapp

  • bss and datasocket

  • text函数

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

Thread是什么?

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

clipboard.png

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特定物理内存到进程空间内或者其余。

线程间通讯

clipboard.png
全部的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

相关文章
相关标签/搜索