多线程

进程ID和线程ID
  在Linux中,目前的线程实现是 Native POSIX Thread Libaray,简称NPTL。在这种实现下,线程⼜被称为轻量级进程(Light Weighted Process),每⼀个⽤户态的线程,在内核中都对应⼀个调度实体,也拥有⾃⼰的进程描述符 (task_struct结构体)。
  没有线程之前,⼀个进程对应内核⾥的⼀个进程描述符,对应⼀个进程 ID。但是引⼊线程概念之后,情况发⽣了变化,⼀个⽤户进程下管辖 N个⽤户态线程,每个线程作为⼀个独⽴的调度实体在内核态都有⾃⼰的进程描述符,进程和内核的描述符⼀下⼦就变成了 1:N关系,POSIX标准⼜要求进程内的所有线程调⽤ getpid函数时返回相同的进程 ID,所以Linux内核引⼊了线程组的概念。

struct task_struct {
    ...
    pid_t pid;
    pid_t tgid;
    ...
    struct task_struct *group_leader;
    ...
    struct list_head thread_group;
    ...
};

  多线程的进程,⼜被称为线程组,线程组内的每⼀个线程在内核之中都存在⼀个进程描述符(task_struct)与之对应。进程描述符结构体中的pid,表⾯上看对应的是进程 ID,其实不然,它对应的是线程ID;进程描述符中的tgid,含义是Thread Group ID,该值对应的是⽤户层⾯的进程 ID。
这里写图片描述
  Linux提供了gettid系统调⽤来返回其线程 ID,可是glibc并没有将该系统调⽤封装起来,在开放接⼝来共程序员使⽤。如果确实需要获得线程 ID,可以采⽤如下⽅法:

 #include <sys/syscall.h>
   pid_t tid;
   tid = syscall(SYS_gettid);

  线程组内的第⼀个线程,在⽤户态被称为主线程 (main thread),在内核中被称为group leader,内核在创建第⼀个线程时,会将线程组的 ID的值设置成第⼀个线程的线程 ID,group_leader指针则指向⾃⾝,既主线程的进程描述符。所以线程组内存在⼀个线程 ID等于进程ID,⽽该线程即为线程组的主线程。

/* 线程组ID等于线程ID,group_leader指向⾃⾝ */
    p->tgid = p->pid;
    p->group_leader = p;
    INIT_LIST_HEAD(&p->thread_group);

  ⾄于线程组其他线程的 ID则有内核负责分配,其线程组 ID总是和主线程的线程组 ID⼀致,⽆论是主线程直接创建线程,还是创建出来的线程再次创建线程,都是这样。

if ( clone_flags & CLONE_THREAD )
        p->tgid = current->tgid;
    if ( clone_flags & CLONE_THREAD ) {
        P->group_lead = current->group_leader;
        list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
    }

线程和进程不⼀样,进程有⽗进程的概念,但在线程组⾥⾯,所有的线程都是对等关系。
线程ID及进程地址空间布局
  pthread_ create函数会产⽣⼀个线程 ID,存放在第⼀个参数指向的地址中。该线程 ID和前⾯说的线程ID不是⼀回事。
  前⾯讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最⼩单位,所以需要⼀个数值来唯⼀表⽰该线程。
  pthread_ create函数产⽣并标记在第⼀个参数指向的地址中的线程 ID中,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程 ID来操作线程的。
线程库NPTL提供了pthread_ self函数,可以获得线程⾃⾝的 ID:

pthread_t pthread_self(void);

这里写图片描述
线程同步与互斥
mutex (互斥量)
  ⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程⽆法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
多个线程并发的操作共享变量,会带来⼀些问题。

// 操作共享变量会有问题的售票系统代码
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
 #include <pthread.h>
 int ticket = 100;
 void *route(void *arg)
 {
    char *id = (char*)arg;
    while ( 1 ) {
        if ( ticket > 0 ) {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        } else {
            break;
        }
    }
 }
int main( void )
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, NULL, route, "thread 1");
    pthread_create(&t2, NULL, route, "thread 2");
    pthread_create(&t3, NULL, route, "thread 3");
    pthread_create(&t4, NULL, route, "thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
}

一次执行结果:
这里写图片描述
为什么可能⽆法获得争取结果?
  if语句判断条件为真以后,代码可以并发的切换到其他线程;
  usleep这个模拟漫⻓业务的过程,在这个漫⻓的业务过程中,可能有很多个线程会进⼊该代码段;
  –ticket操作本⾝就不是⼀个原⼦操作。

取出ticket--部分的汇编代码
objdump -d a.out > test.objdump
152   40064b:   8b 05 e3 04 20 00       mov    0x2004e3(%rip),%eax        # 600b34
 <ticket>
153   400651:   83 e8 01                sub    $0x1,%eax
154   400654:   89 05 da 04 20 00       mov    %eax,0x2004da(%rip)        # 600b34
 <ticket>

操作并不是原⼦操作,⽽是对应三条汇编指令:
  load:将共享变量ticket从内存加载到寄存器中
  update:更新寄存器⾥⾯的值,执⾏ -1操作
  store:将新值,从寄存器写回共享变量 ticket的内存地址
要解决以上问题,需要做到三点:
  代码必须要有互斥⾏为:当代码进⼊临界区执⾏时,不允许其他线程进⼊该临界区。
  如果多个线程同时要求执⾏临界区的代码,并且临界区没有线程在执⾏,那么只能允许⼀个线程进⼊该临界区。
  如果线程不在临界区中执⾏,那么该线程不能阻⽌其他线程进⼊临界区。
要做到这三点,本质上就是需要⼀把锁。 Linux上提供的这把锁叫互斥量。
这里写图片描述
互斥量的接⼝
初始化互斥量
⽅法1,静态分配:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

⽅法2,动态分配:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr
_t *restrict attr);
    参数:
        mutex:要初始化的互斥量
        attr:NULL

销毁互斥量
销毁互斥量需要注意:
 使⽤PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁; 
 不要销毁⼀个已经加锁的互斥量;
 已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁。

int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

调⽤pthread_ lock时,可能会遇到以下情况 :
  互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  发起函数调⽤时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调⽤会陷⼊阻塞,等待互斥量解锁。
改进上⾯的售票系统 :

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
int ticket = 100;
pthread_mutex_t mutex;
void *route(void *arg)
{
    char *id = (char*)arg;
    while ( 1 ) {
        pthread_mutex_lock(&mutex);
        if ( ticket > 0 ) {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);
            // sched_yield(); 放弃CPU
        } else {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
}
int main( void )
{
    pthread_t t1, t2, t3, t4;
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&t1, NULL, route, "thread 1");
    pthread_create(&t2, NULL, route, "thread 2");
    pthread_create(&t3, NULL, route, "thread 3");
    pthread_create(&t4, NULL, route, "thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    pthread_mutex_destroy(&mutex);
}

条件变量
  当⼀个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
  例如⼀个线程访问队列时,发现队列为空,它只能等待,只到其它线程将⼀个节点添加到队列中。这种情况就需要⽤到条件变量。
条件变量函数:
初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *rest
rict attr);
参数:
    cond:要初始化的条件变量
    attr:NULL

销毁

int pthread_cond_destroy(pthread_cond_t *cond);

等待条件满⾜

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mute
x);
参数:
    cond:要在这个条件变量上等待
    mutex:互斥量,后⾯详细解释

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

为什么pthread_ cond_ wait 需要互斥量?
  条件等待是线程间同步的⼀种⼿段,如果只有⼀个线程,条件不满⾜,⼀直等下去都不会满⾜,所以必须要有⼀个线程通过某些操作,改变共享变量,使原先不满⾜的条件变得满⾜,并且友好的通知等待在条件变量上的线程。
  条件不会⽆缘⽆故的突然变得满⾜了,必然会牵扯到共享数据的变化。所以⼀定要⽤互斥锁来保护。没有互斥锁就⽆法安全的获取和修改共享数据。
这里写图片描述

  // 错误的设计
    pthread_mutex_lock(&mutex);
    while (condition_is_false) {
        pthread_mutex_unlock(&mutex);
        //解锁之后,等待之前,条件可能已经满⾜,信号已经发出,但是该信号可能被错过
        pthread_cond_wait(&cond);
        pthread_mutex_lock(&mutex);
    }
    pthread_mutex_unlock(&mutex);

  由于解锁和等待不是原⼦操作。调⽤解锁之后, pthread_ cond_ wait之前,如果已经有其他线程获取到互斥量,摒弃条件满⾜,发送了信号,那么 pthread_ cond_ wait将错过这个信号,可能会导致
线程永远阻塞在这个 pthread_ cond_ wait。所以解锁和等待必须是⼀个原⼦操作。
  int pthread_ cond_ wait(pthread_ cond_ t cond,pthread_ mutex_ t mutex);进⼊该函数后,会去看条件量等于0不?等于,就把互斥量变成 1,直到cond_ wait返回,把条件量改成 1,把互斥量恢复成原样。
条件变量使⽤规范
等待条件代码

  pthread_mutex_lock(&mutex);
    while (条件为假)
        pthread_cond_wait(cond, mutex);
    修改条件
    pthread_mutex_unlock(&mutex);

给条件发送信号代码

  pthread_mutex_lock(&mutex);
    设置条件为真
    pthread_cond_signal(cond);
    pthread_mutex_unlock(&mutex);

⽣产者消费者模型
这里写图片描述
代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#define CONSUMERS_COUNT 2 
#define PRODUCERS_COUNT 2
struct msg{
    struct msg *next;
    int num;
};
struct msg *head = NULL;
pthread_cond_t cond;
pthread_mutex_t mutex;
pthread_t threads[CONSUMERS_COUNT+PRODUCERS_COUNT];
void *consumer(void *p)
{
    int num = *(int*)p;
    free(p);
    struct msg *mp;
    for ( ; ; ) {
        pthread_mutex_lock(&mutex);
        while ( head == NULL )  {
            printf("%d begin wait a condition...\n", num);
            pthread_cond_wait(&cond, &mutex);
        }
        printf("%d end wait a condition...\n", num);
        printf("%d begin consume product...\n", num);
        mp = head;
        head = mp->next;
        pthread_mutex_unlock(&mutex);
        printf("Consume %d\n", mp->num);
        free(mp);
        printf("%d end consume product...\n", num);
        sleep(rand()%5);
    }
}
void *producer(void *p)
{
    struct msg *mp;
    int num = *(int*)p;
    free(p);
    for ( ; ; ) {
        printf("%d begin produce product...\n", num);
        mp = (struct msg*)malloc(sizeof(struct msg));
        mp->num = rand()%1000 + 1;
        printf("produce %d\n", mp->num);
        pthread_mutex_lock(&mutex);
        mp->next = head;
        head = mp;
        printf("%d end produce product...\n", num);
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mutex);
        sleep(rand()%5);
    }
}
int main( void )
{
    srand(time(NULL));
    pthread_cond_init(&cond, NULL);
    pthread_mutex_init(&mutex, NULL);
    int i;
    for(i=0; i<CONSUMERS_COUNT; i++) {
        int *p = (int*)malloc(sizeof(int));
        *p = i;
        pthread_create(&threads[i], NULL, consumer, (void*)p);
    }
    for(i=0; i<PRODUCERS_COUNT; i++) {
        int *p = (int*)malloc(sizeof(int));
        *p = i;
        pthread_create(&threads[CONSUMERS_COUNT+i], NULL, producer, (void*)p);
    }
    for (i=0; i<CONSUMERS_COUNT+PRODUCERS_COUNT; i++)
        pthread_join(threads[i], NULL);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
}

POSIX信号量
POSIX信号量和SystemV信号量作⽤相同,都是⽤于同步操作,达到⽆冲突的访问共享资源目的。 但POSIX可以⽤于线程间同步。
初始化信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
    pshared:0表⽰线程间共享,⾮零表⽰进程间共享
    value:信号量初始值

销毁信号量

int sem_destroy(sem_t *sem);

等待信号量

功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem);

发布信号量

功能:发布信号量,表⽰资源使⽤完毕,可以归还资源了。将信号量值加1int sem_post(sem_t *sem);

上节⽣产者-消费者的例⼦是基于链表的 ,其空间可以动态分配,现在基于固定⼤⼩的环形队列重写这个程序(POSIX信号量):

#include <unistd.h>
#include <sys/types.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
        do \
        { \
                perror(m); \
                exit(EXIT_FAILURE); \
        } while(0)
#define CONSUMERS_COUNT 1
#define PRODUCERS_COUNT 1
#define BUFFSIZE 10
int g_buffer[BUFFSIZE];
unsigned short in = 0;
unsigned short out = 0;
unsigned short produce_id = 0;
unsigned short consume_id = 0;
sem_t g_sem_full;
sem_t g_sem_empty;
pthread_mutex_t g_mutex;
pthread_t g_thread[CONSUMERS_COUNT+PRODUCERS_COUNT];
void* consume(void *arg)
{
    int i;
    int num = *(int*)arg;
    free(arg);
    while (1)
    {
        printf("%d wait buffer not empty\n", num);
        sem_wait(&g_sem_empty);
        pthread_mutex_lock(&g_mutex);
        for (i=0; i<BUFFSIZE; i++)
        {
            printf("%02d ", i);
            if (g_buffer[i] == -1)
                printf("%s", "null");
            else
                printf("%d", g_buffer[i]);
            if (i == out)
                printf("\t<--consume");
            printf("\n");
        }
        consume_id = g_buffer[out];
        printf("%d begin consume product %d\n", num, consume_id);
        g_buffer[out] = -1;
        out = (out + 1) % BUFFSIZE;
        printf("%d end consume product %d\n", num, consume_id);
        pthread_mutex_unlock(&g_mutex);
        sem_post(&g_sem_full);
        sleep(1);
    }
    return NULL;
}
void* produce(void *arg)
{
    int i;
    int num = *(int*)arg;
    free(arg);
    while (1)
    {
        printf("%d wait buffer not full\n", num);
        sem_wait(&g_sem_full);
        pthread_mutex_lock(&g_mutex);
        for (i=0; i<BUFFSIZE; i++)
 {
            printf("%02d ", i);
            if (g_buffer[i] == -1)
                printf("%s", "null");
            else
                printf("%d", g_buffer[i]);
            if (i == in)
                printf("\t<--produce");
            printf("\n");
        }
        printf("%d begin produce product %d\n", num, produce_id);
        g_buffer[in] = produce_id;
        in = (in + 1) % BUFFSIZE;
        printf("%d end produce product %d\n", num, produce_id++);
        pthread_mutex_unlock(&g_mutex);
        sem_post(&g_sem_empty);
        sleep(5);
    }
    return NULL;
}
int main(void)
{
    int i;
    for (i=0; i<BUFFSIZE; i++)
        g_buffer[i] = -1;
    sem_init(&g_sem_full, 0, BUFFSIZE);
    sem_init(&g_sem_empty, 0, 0);
    pthread_mutex_init(&g_mutex, NULL);
    for (i=0; i<CONSUMERS_COUNT; i++) {
        int *p = (int*)malloc(sizeof(int));
        *p = i;
        pthread_create(&g_thread[i], NULL, consume, (void*)p);
    }
    for (i=0; i<PRODUCERS_COUNT; i++) {
        int *p = (int*)malloc(sizeof(int));
        *p = i;
        pthread_create(&g_thread[CONSUMERS_COUNT+i], NULL, produce, (void*)p);
    }
 for (i=0; i<CONSUMERS_COUNT+PRODUCERS_COUNT; i++)
        pthread_join(g_thread[i], NULL);
    sem_destroy(&g_sem_full);
    sem_destroy(&g_sem_empty);
    pthread_mutex_destroy(&g_mutex);
    return 0;
}

读写锁
在编写多线程的时候,有⼀种情况是⼗分常⻅的。那就是,有些公共数据修改的机会⽐较少。相⽐较改写,它们读的机会反⽽⾼的多。通常⽽⾔,在读的过程中,往往伴随着查找的操作,中间耗时很⻓。给这种代码段加锁,会极⼤地降低我们程序的效率。所以引入了读写锁,读写锁本质上是⼀种⾃旋锁(⻓时间等⼈和短时间等⼈的例⼦)。
这里写图片描述
读写锁接⼝
初始化

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr
_t *restrict attr);

销毁

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

加锁和解锁

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

读写锁案例:

//创建5个读者线程,3个写者线程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
int counter;
pthread_rwlock_t rwlock;
void *route_write(void *arg)
{
    int t;
    int i = *(int*)arg;
    free(arg);
    while ( 1 ) {
        t = counter;
        usleep(1000);
        pthread_rwlock_wrlock(&rwlock);
        printf("write:%d:%#X: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);
        pthread_rwlock_unlock(&rwlock);
        usleep(5000);
    }
}
void *route_read(void *arg)
{
    int t;
    int i = *(int*)arg;
    free(arg);
    while ( 1 ) {
        pthread_rwlock_rdlock(&rwlock);
        printf("read :%d:%#X: counter=%d\n", i, pthread_self(), counter);
        pthread_rwlock_unlock(&rwlock);
        usleep(900);
    }
}
int main( void )
{
    int i;
    pthread_t tid[8];
    pthread_rwlock_init(&rwlock, NULL);
    for (i=0; i<3; i++) {
        int *p = (int*)malloc(sizeof(int));
        *p = i;
        pthread_create(&tid[i], NULL, route_write, (void*)p);
    }
    for (i=0; i<5; i++) {
        int *p = (int*)malloc(sizeof(int));
        *p = i;
        pthread_create(&tid[i+3], NULL, route_read, (void*)p);
    }
    for (i=0; i<8; i++)
        pthread_join(tid[i], NULL);
    pthread_rwlock_destroy(&rwlock);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值