Bootstrap

Linux C语言 21-多线程

Linux C语言 21-线程

本节关键字:Linux、C语言、线程,pthread库的使用,POSIX
相关C库函数:pthread_create、pthread_exit、pthread_cancel、pthread_join …

什么是线程?

linux内核中是没有线程这个概念的,而是轻量级进程的概念:LWP。一般我们所说的线程概念是C库当中的概念。

线程(英语:thread)是操作系统能够进行运算调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并行多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。

一个进程可以有很多线程来处理,每条线程并行执行不同的任务。如果进程要完成的任务很多,这样需很多线程,也要调用很多核心,在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见的,即提高了程序的执行吞吐率。

POSIX线程

POSIX线程(POSIX threads),简称Pthreads,是线程的POSIX标准。该标准定义了创建和操纵 线程的一整套API。在类Unix操作系统(Unix、Linux、Mac OS X等)中,都使用Pthreads作为操作系统的线程
1)是操作系统能够进行运算调度的最小单位
2)进程内部同时可以存在多条线程
3)同一进程中的多条线程将共享该进程中的全部系统资源
4)线程是并发执行

时间片

时间片(timeslice)又称为“量子(quantum)”或“处理器片(processor slice)”是分时操作系统分配给每个正在运行的进程微观上的一段CPU时间(在抢占内核中是:从进程开始运行直到被抢占的时间)。
由于时间片通常很短(在Linux上为5ms-800ms),用户不会感觉到。
时间片由操作系统内核的调度程序分配给每个进程。首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时间,当所有进程都处于时间片耗尽的状态时,内核会重新为每个进程计算并分配时间片,如此往复。
通常状况下,一个系统中所有的进程被分配到的时间片长短并不是相等的,尽管初始时间片基本相等(在Linux系统中,初始时间片也不相等,而是各自父进程的一半),系统通过测量进程处于“睡眠”和“正在运行”状态的时间长短来计算每个进程的交互性,交互性和每个进程预设的静态优先级(Nice值)的叠加即是动态优先级,动态优先级按比例缩放就是要分配给那个进程时间片的长短。一般地,为了获得较快的响应速度,交互性强的进程(即趋向于IO消耗型)被分配到的时间片要长于交互性弱的(趋向于处理器消耗型)进程。

执行态:正在占用CPU的状态
挂起态:缺少某些资源而让出CPU的状态
就绪态:资源准备继续等待CPU调度的状态

线程分为:用户级线程和内核级线程。
在计算机科学中,任务(task)的上下文(英语:context)是一个任务所必不可少的一组数据(该任务可以是进程、线程)。这些数据允许任务中断,在这之后仍可在同一个地方继续执行。上下文的这一概念在中断的任务的场景下具有重大意义,其中,任务在被中断之后,处理器保存上下文并提供中断处理(interrupt service routine)。因此,上下文越小,延迟越小。
上下文的数据可能存放于处理器寄存器、任务所利用的内存、某些操作系统管理的任务所使用的控制寄存器(control registers)。

线程的优缺点

线程的优点:

- 创建一个新线程的代价要比创建一个新进程小的多
- 线程之间的切换相较于进程之间的切换需要操作系统做的工作很少
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 等待慢速 IO操作结束以后,程序可以执行其他的计算任务
- 计算(CPU)密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- IO密集型应用,为了提高性能,将IO操作重叠,线程可以等待不同的IO操作。

线程的缺点:

- 性能损失( 一个计算密集型线程是很少被外部事件阻塞的,无法和其他线程共享同一个处理器,当计算密集型的线程的数量比可用的处理器多,那么就有可能有很大的性能损失,这里的性能损失是指增加了额外的同步和调度开销,二可用资源不变。)
- 健壮性降低(线程之间是缺乏保护性的。在一个多线程程序里,因为时间上分配的细微差距或者是共享了一些不应该共享的变量而造成不良影响的可能影响是很大的。)
- 缺乏访问控制( 因为进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响 。)
- 编程难度提高(编写和 调试一个多线程程序比单线程困难的多。 

线程操作相关库函数

在介绍线程相关函数之前,需要先了解一下线程属性。

#include <pthread.h>
// 注意:gcc编译时需要添加参数 -lpthread
// 线程属性
typedef struct
{
int            detachstatr;    // 线程分离的状态
int            schedpolicy;    // 线程调度策略
structsched_param    schedparam;    // 线程的调度参数
int            inheritsched;    // 线程的继承性
int            scope;        // 线程的作用域
size_t        guardsize;    // 线程栈末尾的警戒缓冲区大小
int            stackaddr_set;    // 线程堆栈设置
void*        stackaddr;    // 线程堆栈位置
size_t        stacksize;    // 线程栈的大小
} pthread_attr_t;

// 线程属性初始化,成功返回0,失败返回非零
int pthread_attr_init(pthread_attr_t *attr);

// 销毁线程属性(释放相关资源),成功返回0,失败返回非零
int pthrad_attr_destroy(pthread_att_t *attr);

// PTHREAD_CREATE_JOINABLE 以可连接状态创建线程(默认值);PTHREAD_CREATE_DETACHED 以分离状态创建线程
// 获取线程的分离状态,属性状态值存储在变量detachstate中,成功返回0,失败返回非零
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
// 设置线程的分离状态,将属性状态设置为参数detachstate,成功返回0,失败返回非零
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

// 获取线程栈属性对象的堆栈地址和堆栈大小,成功返回0,失败返回非零,栈属性值存储在stackaddr和stacksize中
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
// 设置线程属性对象的堆栈地址和堆栈大小,成功返回0,失败返回非零
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);

// 获取线程属性对象的栈大小,成功返回0,失败返回非零
int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
// 设置线程属性对象的栈大小,成功返回0,失败返回非零
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);

// 获取线程属性对象的保护大小属性,成功返回0,失败返回非零
int pthread_attr_getguardsize(pthread_attr_t *attr, size_t *guardsize);
// 设置线程属性对象的保护大小属性,成功返回0,失败返回非零
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);

// 获取线程属性对象的继承性,成功返回0,失败返回非零
int pthread_attr_getinheritsched(const pthread_attr_t*attr, int *inheritsched);
// 设置线程属性对象的继承性,成功返回0,失败返回非零
int pthread_attr_setinheritsched(pthread_attr_t *attr, intinheritsched);
// 继承性决定调度的参数是从创建的进程中继承还是使用在schedpolicy和schedparam属性中显式设置的调度信息。
// PTHREAD_INHERIT_SCHED 新线程继承创建线程的调度策略和参数;PTHREAD_EXPLICIT_SCHED 使用schedpolicy个schedparam属性中显示设置的调度策略和参数
// 如果需要显示的设置一个线程的调度策略或参数,那么必须在设置之前将inheritsched属性设置为PTHREAD_EXPLICIT_SCHED

// 获取线程属性对象的调度策略实行,成功返回0,失败返回非零
int pthread_attr_getschedpolicy(pthread_attr_t *attr, int *policy);
// 设置线程属性对象的调度策略实行,成功返回0,失败返回非零
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
// SCHED_FIFO 先进先出策略,允许一个线程运行直到有更高优先级的线程准备好,或者直到它自愿阻塞自己。当有一个线程准备好时,除非有平等或更高优先级的线程已经在运行,否则它会很快开始执行。
// SCHED_RR 轮转法策略,如果有一个SCHED_RR策略的线程执行了超过一个固定的时期(时间片间隔)没有阻塞,而另外的SCHED_RR或SCHBD_FIPO策略的相同优先级的线程准备好时,运行的线程将被抢占以便准备好的线程可以执行。
// SCHED_OTHER 其它调度策略

// 获取线程属性对象的最高最低优先级,SCHED_OTHER 是不支持优先级使用的,它最大和最小优先级都是0。
SCHED_FIFO 和 SCHED_RR 支持优先级的使用,他们分别为199,数值越大优先级越高 。
int sched_get_priority_max(int policy);
int sched_get_priority_min(int policy);

// 获取线程属性对象的调度参数,成功返回0,失败返回非零
int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
struct sched_param {
int sched_priority;    /* Scheduling priority */
};
// 结构sched_param的子成员sched_priority控制一个优先权值,大的优先权值对应高的优先权。系统支持的最大和最小优先权值可以用sched_get_priority_max函数和sched_get_priority_min函数分别得到。
// 注意:如果不是编写实时程序,不建议修改线程的优先级。
因为,调度策略是一件非常复杂的事情,如果不正确使用会导致程序错误,从而导致死锁等问题。如:在多线程应用程序中为线程设置不同的优先级别,有可能因为共享资源而导致优先级倒置。

// 获取线程属性对象的作用域属性,成功返回0,失败返回非零
int pthread_attr_getscope(pthread_attr_t *attr, int *scope);
int pthread_attr_setscope(pthread_attr_t *attr, int scope);
// PTHREAD_SCOPE_SYSTEM 系统级竞争资源;PTHREAD_SCOPE_PROCESS 进程内竞争资源

// 创建进程中所有线程都可见的线程特定数据密钥,成功返回0,失败返回非零
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
// 这个键对进程中所有的线程都是可见的。刚创建线程数据键时,在所有线程中和这个键相关联的值都是NULL。
// 函数成功返回后,分配的键放在key参数指向的内存中,必须保证key参数指向的内存区的有效性。
// 如果指定了解析函数destructor,那么当线程结束时并且将非空的值绑定在这个键上,系统将调用destructor函数,参数就是相关线程与这个键绑定的值。绑定在这个键上的内存块可由destructor函数释放。
// 销毁线程密钥,释放相关资源,成功返回0,失败返回非零
int pthread_key_delete(pthread_key_t key);

// 获取线程当前绑定键的值,返回特定数据值,没有特定数据值时返回NULL
void *pthread_getspecific(pthread_key_t key);
// 设置线程的键和相关数据相关联,成功返回0,失败返回非零
int pthread_setspecific(pthread_key_t key, const void *value);

// 创建线程,成功返回0,失败返回错误码
// EAGAIN 系统资源不够,或者创建线程的个数超过系统对一个进程中线程总数的限制
// EINVAL 第二个参数attr值不合法
// EPERM 没有合适的权限来设置调度策略或参数
// 注意:不要使用临时变量传入参数arg,建议使用生命周期长或者堆上开辟的变量
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

// 将非分离的线程设置为分离线程,成功返回0,失败返回非零
int pthread_detach(pthread_t tid);
// 调用线程分离后,通知线程库在指定的线程终止时回收线程占用的内存等资源。注意:在一个线程上多次调用phtread_detach的结果是不可预期的。

// 获取线程ID,返回当前线程的ID号
pthread_t pthread_self(void);

// 判断两个线程ID是否对应同一个线程,相同返回0,不同返回非零
int pthread_equal(pthread_t t1, pthread_t t2);

// 取消线程,如何响应退出请求取决于目标线程的状态,成功返回0,失败返回非零
int pthread_cancel(pthread_t thread);

// 等待线程退出,阻塞调用该接口的线程,直到参数tid指定的线程结束,成功返回0,失败返回非零
int pthread_join(pthread_t tid, void **status);
// 指定的线程必须在当前的进程中,同时tid指定的线程必须是非分离的,status不为NULL时,接收线程tid的退出状态或返回值

// 退出当前线程,所有绑定在线程数据键上的内存将被释放
void pthread_exit(void *status);
// 如果当前线程是非分离的,那么这个线程的标示符合退出代码将被保留,直到其他线程用pthread_join来等待当前线程的终止;如果当前线程是分离的,status将被忽略,线程标示符将被立即回收。status不为NULL时,将返回值设置为status参数指向的值。

线程操作库函数使用示例

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

#define handle_error_en(en, msg) \
    do { errno = en; perror(msg); exit(EXIT_FAILURE); } while (0)

pthread_key_t key;

void *start_routine1(void *arg)
{
    int i, status=1, num=10;
    pthread_setspecific(key, &num);
    
    for (i=0; i<5; i++)
    {
        sleep(1);
        printf("this is thread1 data:%d\n",*(int *)pthread_getspecific(key));
    }
    pthread_exit(&status);
}

void *start_routine(void *arg)
{
    int i, status=1, num=20;
    pthread_setspecific(key, &num);
    
    for (i=0; i<5; i++)
    {
        sleep(1);
        printf("this is thread2 data:%d\n",*(int *)pthread_getspecific(key));
    }
    pthread_exit(&status);
}

int main(int argc, char const* argv[])
{
    int i, ret=0;
    void *retval=(void *)0;
    int arg=10l;
    pthread_t thread, thread1;

    pthread_key_create(&key, NULL);
    ret = pthread_create(&thread, NULL, start_routine, (void *)&arg);
    ret = pthread_create(&thread1, NULL, start_routine1, (void *)&arg);
    if (ret != 0)
        handle_error_en(ret, "pthread_create");
    
    for (i=0; i<5; i++)
    {
        printf("main thread id is %lu\n", thread);
        sleep(1);
    }
    
    pthread_join(thread, &retval);
    pthread_join(thread1, &retval);
    pthread_key_delete(key);
    return 0;
}

线程安全相关库函数

// 互斥锁 定义及初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALZER;
int pthread_mutex_init(
pthread_mutex_t *restrict mutex, /* 要初始化的互斥量 */
const pthread_mutex_t *restrict attr /* 设置互斥量的属性,默认为NULL */
); /* 调用pthread_mutex_init()后,互斥量处于没有加锁状态 */
// 互斥锁 销毁,使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量无须销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 互斥锁 阻塞加锁
int pthread_nutex_lock(pthread_mutex_t *mutex);
// 互斥锁 非阻塞加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_timedlock(
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout /* 加锁等待时间,超出时间返回失败 */
);
// 互斥锁 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); /* 将互斥锁中的计数器从0变为1,其它线程可获取 */

// 读写锁 
PTHREAD_RWLOCK_PREFER_READER_NP /* 读者优先 */
PTHREAD_RWLOCK_PREFER_WRITER_NP /* 很唬人,但也是读者优先 */
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP /* 写者优先 */
PTHREAD_RWLOCK_DEFAULT_NP = PTHREAD_RWLOCK_PREFER_READER_NP /* 默认读者优先 */
int pthread_rwlock_init(
pthread_rwlock_t *restrict rwlock, /* 读写锁地址 */
const pthread_rwlockattr_t *restrict attr /* 读写锁属性,默认NULL,PTHREAD_PROCESS_PRIVATE进程内部竞争读写锁,PTHREAD_RWLOCK_PREFER_READER_NP读者优先 */
);
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 ptrhead_rwlock_trywrlock(pthread_rwlock_t *rwlock); /* 非阻塞写加锁 */
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

// 条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init(
pthread_cont_t *cond, /* 条件变量地址 */
const pthread_condattr_t *attr /* 条件变量属性,默认为NULL */
);
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, consr struct timespc *restrict abstime); /* 在abstime时间内等待 */
int pthread_cond_signal(pthread_cond_t *cond); /* 唤醒等待在条件变量上的一个线程 */ 
int pthread_cond_broadcast(pthread_cond_t *cond); /* 广播唤醒等待在条件变量的所有线程 */
int pthread_cond_destroy(pthread_cond_t *cond); /* 使用PTHREAD_COND_INITIALIZER初始化的调教变量不需要销毁 */

线程安全相关库函数使用示例

// 生产者和消费者模型
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>


int g_Runflag = 1;
pthread_mutex_t mutex;
pthread_cond_t  producer;

void *producer_thread(void *arg)
{
    int *resource = (int*)arg;
    int i;
    
    while ( g_Runflag )
    {
        for (i=1; i<=5; i++)
        {    
            pthread_mutex_lock(&mutex);
            *resource += i;
            printf("producer production resources, total resources: %d\n", *resource);
            pthread_mutex_unlock(&mutex);
            
            pthread_cond_signal(&producer);
            sleep(i);
        }
        
    }
    return NULL;
}

void *consumer_thread(void *arg)
{
    int *resource = (int*)arg;
    
    while ( g_Runflag )
    {
        pthread_mutex_lock(&mutex);
    
        while (*resource <= 0)
        {
            printf("consumer is waiting for resources ...\n");
            pthread_cond_wait(&producer, &mutex);
        }
        
        *resource -= 1;
        printf("consumers obtain resources once, remaining resources %d\n", *resource);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main(int argc, char const* argv[])
{
    int ret, nCount=0, aResource=0;
    pthread_t producer_tid, comsumer_tid;
    pthread_attr_t attr;
    
    
    if (pthread_mutex_init(&mutex, NULL))
        perror("pthread_mutex_init");
    if (pthread_cond_init(&producer, NULL))
        perror("pthread_cond_init");
    
    ret = pthread_attr_init(&attr);
    if (ret)
    {
        printf("pthread_attr_init, rc:%d,%s\n", ret, strerror(ret));
        return 0;
    }
    
    ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if (ret) 
    {
        printf("pthread_attr_setdetachstate, rc:%d,%s\n", ret, strerror(ret));
        return 0;
    }
    sleep(1);
    
    ret = pthread_create(&producer_tid, &attr, producer_thread, (void*)&aResource);
    if (ret) {
        printf("pthread_ceate producer_thread, rc:%d,%s\n", ret, strerror(ret));
        return 0;
    }
    sleep(1);
    
    ret = pthread_create(&producer_tid, &attr, consumer_thread, (void*)&aResource);
    if (ret) {
        printf("pthread_ceate consumer_thread, rc:%d,%s\n", ret, strerror(ret));
        return 0;
    }
    sleep(1);
    
    
    for ( ; ; )
    {
        sleep(1);
        
        printf("nCount: %d\n", ++nCount);
        if (nCount >= 30)
        {
            printf("exit!\n");
            g_Runflag = 0;
            break;
        }
    }
    
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&producer);
    ret = pthread_attr_destroy(&attr);
    if (ret)
        printf("pthread_attr_destroy, rc:%d, %s\n", ret, strerror(ret));
    
    return 0;
}

/** 运行结果:
producer production resources, total resources: 1
producer production resources, total resources: 3
consumers obtain resources once, remaining resources 2
consumers obtain resources once, remaining resources 1
consumers obtain resources once, remaining resources 0
consumer is waiting for resources ...
producer production resources, total resources: 3
consumers obtain resources once, remaining resources 2
consumers obtain resources once, remaining resources 1
consumers obtain resources once, remaining resources 0
consumer is waiting for resources ...
nCount: 1
nCount: 2
nCount: 3
producer production resources, total resources: 4
consumers obtain resources once, remaining resources 3
consumers obtain resources once, remaining resources 2
consumers obtain resources once, remaining resources 1
consumers obtain resources once, remaining resources 0
consumer is waiting for resources ...
nCount: 4
nCount: 5
nCount: 6
nCount: 7
producer production resources, total resources: 5
consumers obtain resources once, remaining resources 4
consumers obtain resources once, remaining resources 3
consumers obtain resources once, remaining resources 2
consumers obtain resources once, remaining resources 1
consumers obtain resources once, remaining resources 0
consumer is waiting for resources ...
nCount: 8
nCount: 9
nCount: 10
nCount: 11
nCount: 12
producer production resources, total resources: 1
consumers obtain resources once, remaining resources 0
consumer is waiting for resources ...
nCount: 13
producer production resources, total resources: 2
consumers obtain resources once, remaining resources 1
consumers obtain resources once, remaining resources 0
consumer is waiting for resources ...
nCount: 14
nCount: 15
producer production resources, total resources: 3
consumers obtain resources once, remaining resources 2
consumers obtain resources once, remaining resources 1
consumers obtain resources once, remaining resources 0
consumer is waiting for resources ...
nCount: 16
nCount: 17
nCount: 18
producer production resources, total resources: 4
consumers obtain resources once, remaining resources 3
consumers obtain resources once, remaining resources 2
consumers obtain resources once, remaining resources 1
consumers obtain resources once, remaining resources 0
consumer is waiting for resources ...
nCount: 19
nCount: 20
nCount: 21
nCount: 22
producer production resources, total resources: 5
consumers obtain resources once, remaining resources 4
consumers obtain resources once, remaining resources 3
consumers obtain resources once, remaining resources 2
consumers obtain resources once, remaining resources 1
consumers obtain resources once, remaining resources 0
consumer is waiting for resources ...
nCount: 23
nCount: 24
nCount: 25
nCount: 26
nCount: 27
producer production resources, total resources: 1
consumers obtain resources once, remaining resources 0
consumer is waiting for resources ...
nCount: 28
producer production resources, total resources: 2
consumers obtain resources once, remaining resources 1
consumers obtain resources once, remaining resources 0
consumer is waiting for resources ...
nCount: 29
nCount: 30
exit!
*/

线程池

// 待补充
;