线程

线程的概念

线程:light weight process,轻量级的进程,Linux环境下本质上仍是进程。和进程的区别是,进程有独立的地址空间和PCB,而线程有PCB但是没有独立的地址空间。在Linux环境下,线程是最小的执行单位,而进程是最小分配资源单位。

线程共享资源:

  1. 文件描述符表。
  2. 每种信号的处理方式。
  3. 当前工作目录。
  4. 用户ID和组ID。
  5. 内存地址空间(.text/.data/.bss/heap/共享库)。

线程非共享资源:

  1. 线程id。
  2. 处理器现场和栈指针(内核栈)。
  3. 独立的栈空间(用户空间栈)。
  4. errno变量。
  5. 信号屏蔽字。
  6. 进程调度优先级。

线程的优缺点
优点:1,提高程序并发性。2,开销小。3,数据通信,共享数据方便。
缺点:1,库函数,稳定性稍低。2,调试、编写困难,gdb不支持。3,对信号支持不好。

线程的创建和回收

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

struct person{
    char name[20];
    int age;
};
typedef struct person* Person;

void *func(void *arg){
    Person p = (Person)arg;
    printf("name = %s,age = %d\n",p->name,p->age);
    Person ret = (Person)malloc(sizeof(struct person));
    strcpy(ret->name,p->name);
    ret->age = p->age+1;
    return (void *)ret;
}

int main(int argc, char const *argv[])
{
    pthread_t tid;
    Person p = (Person)malloc(sizeof(struct person));
    strcpy(p->name,"Jack");
    p->age = 34;
    pthread_create(&tid,NULL,func,(void *)p);
    Person ret;
    pthread_join(tid,(void**)&ret);
    printf("name = %s,age = %d\n",ret->name,ret->age);
    free(ret);
    system("pause");
    return 0;
}

由于线程共享的东西比较多,如果不使用同步,线程之间是竞争的关系,所以,我们无法预测线程之间的执行顺序,并且,在线程执行的过程中,也可能切换线程。下面是一个例子:

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

int i = 0;

void *func(void *arg){
    while(1){
        i++;
        i++;
    }
}

int main(int argc, char const *argv[])
{
    pthread_t tid;
    pthread_create(&tid,NULL,func,(void *)NULL);
    while(1){
        i++;
        i++;
        if(i % 2 != 0){
            printf("error! i = %d is odd\n",i);
            break;
        }
    }
    return 0;
}

这个程序每次运行的结果都不一样,这就是与时间有关的错误。产生这种错误有三个条件:

  1. 共享数据。
  2. 多个对象竞争。
  3. 没有合理的同步机制。

要解决这种问题,需要使用同步的机制。

线程使用注意事项

  1. 需要主线程退出其他线程不退出,主线程应调用pthread_exit方法。
  2. 要避免僵尸线程,使用pthread_join显示回收,或者使用pthread_detach分离线程或者在pthread_create中指定分离属性。
  3. malloc和mmap申请的内存可以被其他线程释放。
  4. 应避免在多线程中调用fork,除非马上exec,子进程中只有调用fork的线程存在,其他线程在子进程中均pthread_exit。

互斥量

Linux中提供一把互斥锁mutex(也称之为互斥量)。每个线程在对资源操作前都尝试先加锁,成功加锁会才能操作,操作结束解锁。资源还是共享的,线程间也还是竞争的,但通过锁,就将资源的访问变成互斥操作,而后与时间有关的错误也不会产生了。互斥锁只有一把,所以同一个时刻只有一个线程拥有这把锁。

互斥锁实质上是操作系统提供的一把”建议锁“(又称”协同锁“),建议程序中有多线程访问共享资源的时候使用该机制,但,并没有强制限定。因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。如,当A线程对某个全局变量加锁访问,B在访问之前尝试加锁,拿不到锁,B阻塞。C线程不去加锁,而直接访问该全局变量,依然能够访问,但会出现数据混乱。

mutex有以下的一些函数:

/* 获得一个初始化好的锁 */
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
/* 初始化锁 */
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
/* 加锁,阻塞 */
int pthread_mutex_lock(pthread_mutex_t *mutex);
/* 加锁,不阻塞 */
int pthread_mutex_trylock(pthread_mutex_t *mutex);
/* 解锁 */
int pthread_mutex_unlock(pthread_mutex_t *mutex);
/* 释放锁 */
int pthread_mutex_destroy(pthread_mutex_t *mutex);

上面的程序可以改成下面的形式:

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

pthread_mutex_t mutex;
int i = 0;

void *func(void *arg){
    while(1){
        pthread_mutex_lock(&mutex);
        i++;
        i++;
        pthread_mutex_unlock(&mutex);
    }
}

int main(int argc, char const *argv[])
{
    pthread_t tid;
    pthread_mutex_init(&mutex,NULL);
    pthread_create(&tid,NULL,func,(void *)NULL);
    while(1){
        pthread_mutex_lock(&mutex);
        i++;
        i++;
        if(i % 2 != 0){
            printf("error! i = %d is odd\n",i);
            break;
        }
        pthread_mutex_unlock(&mutex);
    }
    pthread_mutex_destroy(&mutex);
    return 0;
}

在做这个demo的时候,犯了一个错误,把unlock写到了检查是否为奇数之前,也会出现上面那种现象,原因在于,有可能恰好在检查前失去CPU,然后执行一次i++之后又得到了CPU(此时虽然已经上锁了,但是不会去检查,会继续执行)。

死锁及其解决方案

一个线程可以通过某种形式的加锁机制来防止别的线程在互斥还没有释放的时候就访问这个资源。值得注意的是,加锁是阻塞的,所以可能会出现这种情况:某个线程在等待另一个线程,而后者也在等待别的线程,这样一直下去,直到这个链条上的线程又在等待第一个线程释放锁。这得到一个任务之间互相等待的连续循环,没有哪个线程能够继续,这被称为死锁。最简单的情况就是自己等自己,如下面的这个程序:

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

int main(int argc, char const *argv[])
{
    pthread_mutex_t pit = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_lock(&pit);
    printf("Lock again\n");
    pthread_mutex_lock(&pit);
    return 0;
}

再次上锁的时候,需要先释放锁,但是线程本身是无法释放锁的,所以就会进入死循环。

死锁的产生需要同时满足以下4个条件:

  1. 互斥条件。线程使用的资源中至少有一个是不能共享的。
  2. 至少有一个线程它必须持有一个资源且正在等待获取一个当前被别的线程持有的资源。
  3. 资源不能被抢占,只能等待其他线程释放资源。
  4. 必须有循环等待。

这些条件需要全部满足才会产生死锁,所以,为了避免死锁,只需要破坏其中一个条件即可。在程序中,防止死锁最容易的方法是破坏第4个条件。也就是说,我们要注意资源的获取顺序,最好是一致的。如:A获取顺序1,2,3;B顺序也是1,2,3,则不会发生死锁。而如果B的顺序是3,2,1,则容易出现死锁。因为后者会出现互相等待的情况。下面以一个哲学家吃饭的例子来说明这个问题:

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

pthread_mutex_t mutex[5];

void* philosopher(void *arg){
    int id = (int)arg;
    int left,right;
    if(id<4){
        left = id;
        right = id + 1;
    }else if(id == 4){
        /* 这里会出现死锁,如果要避免死锁,交换left和right的值即可 */
        left = id;
        right = 0;
    }
    while(1){
        /* 先拿左边的,再拿右边的 */
        pthread_mutex_lock(&mutex[left]);
        pthread_mutex_lock(&mutex[right]);
        printf("philosopher %d eating\n",id);
        pthread_mutex_unlock(&mutex[left]);
        pthread_mutex_unlock(&mutex[right]);
    }
}

int main(int argc, char const *argv[])
{
    int i;
    pthread_t th[5];
    for(i=0;i<5;i++){
        mutex[i] = PTHREAD_MUTEX_INITIALIZER;
    }
    for(i=0;i<5;i++){
        pthread_create(&th[i],NULL,philosopher,(void *)i);
    }
    for(i=0;i<5;i++){
        pthread_join(th[i],NULL);
    }
    for(i=0;i<5;i++){
        pthread_mutex_destroy(&mutex[i]);
    }
    return 0;
}

条件变量及生产者消费者模型

条件变量本身不是锁!但它也可以造成线程阻塞。通常与互斥锁配合使用,给多线程提供一个会和的场所。

/* 获得一个初始化好的锁 */
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
/* 初始化条件变量 */
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
/* 唤醒一个等待该条件变量的线程 */
int pthread_cond_signal(pthread_cond_t *cond);
/* 唤醒全部等待的条件变量 */
int pthread_cond_broadcast(pthread_cond_t *cond);
/* 等待一个条件变量,并且将mutex解锁,唤醒后将mutex加锁 */
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
/* 和wait差不多,只不过到了绝对时间点会直接唤醒 */
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
/* 释放条件变量 */
int pthread_cond_destroy(pthread_cond_t *cond);

线程同步典型的案例即为生产者消费者模型,而借助条件变量来实现这一模型,是比较常见的一种方法。假定有两个线程,一个模拟生产者行为,一个模拟消费者行为。两个线程操作一个共享资源,生产者向其中添加产品,消费者从中消费产品。具体的代码如下:

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

int i = -1;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond_empty = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond_full = PTHREAD_COND_INITIALIZER;

void *cfunc(void *arg){
    int id = (int)arg;
    while(1){
        /* 要访问i,必须先加锁 */
        pthread_mutex_lock(&lock);
        while(i == -1){
            /* 消费者等待满 */
            pthread_cond_wait(&cond_full,&lock);
        }
        printf("consumer %d consume %d\n",id,i);
        i = -1;
        /* 解锁之后通知生产者空了 */
        pthread_mutex_unlock(&lock);
        pthread_cond_signal(&cond_empty);
    }
}

void *pfunc(void *arg){
    int id = 0;
    while(1){
        /* 要访问i,必须加锁 */
        pthread_mutex_lock(&lock);
        while(i != -1){
            /* 生产者等待空 */
            pthread_cond_wait(&cond_empty,&lock);
        }
        printf("producer procduce %d\n",++id);
        i = id;
        /* 解锁后通知消费者满了 */
        sleep(1);
        pthread_mutex_unlock(&lock);
        pthread_cond_signal(&cond_full);
    }
}

int main(int argc, char const *argv[])
{
    pthread_t cid1,cid2,pid;
    pthread_create(&cid1,NULL,cfunc,(void *)1);
    pthread_create(&cid2,NULL,cfunc,(void *)2);
    pthread_create(&pid,NULL,pfunc,NULL);
    
    pthread_join(cid1,NULL);
    pthread_join(cid2,NULL);
    pthread_join(pid,NULL);
    
    pthread_cond_destroy(&cond_empty);
    pthread_cond_destroy(&cond_full);
    pthread_mutex_destroy(&lock);  
    return 0;
}

相较于mutex而言,条件变量可以减少竞争。如直接使用mutex,除了生产者,消费者之间要竞争互斥量以外,消费者之间也需要竞争互斥量,但如果没有产品,消费者之间竞争互斥锁是无意义的。有了条件变量机制以后,只有生产者完成生产,才会引起消费者之间的竞争,提高了程序效率。在我看来,阻塞相当于while循环,而wait是sleep,唤醒之后才会继续执行。

在JAVA中有一个CountDownLatch类,可以用来同步一个或多个线程,强制它们等待由其他线程执行的一组操作,这里我用C语言简单实现了一个类似的场景,就是5个线程同时准备好了,才可以执行后面的操作。

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

pthread_mutex_t mutex;
pthread_cond_t cond;
int n;

void *func(void *arg){
    int id = (int)arg;
    printf("thread %d preparing\n",id);
    sleep(id);
    pthread_mutex_lock(&mutex);
    n--;
    if(n==0){
        pthread_cond_broadcast(&cond);
    }else{
        pthread_cond_wait(&cond,&mutex);
    }
    pthread_mutex_unlock(&mutex);
    printf("thread %d working\n",id);
    return (void *)NULL;
}

int main(int argc, char const *argv[])
{
    mutex = PTHREAD_MUTEX_INITIALIZER;
    cond = PTHREAD_COND_INITIALIZER;
    n = 5;
    pthread_t pt[5];
    int i;
    for(i=0;i<5;i++){
        pthread_create(&pt[i],NULL,func,(void *)(i+1));
        pthread_detach(pt[i]);
    }
    pthread_exit(NULL);
    return 0;
}

信号量

信号量是进化版的互斥锁,也就是从1变成了N。由于互斥锁的粒度较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将真个数据对象锁住,这样虽然达到了多线程操作共享数据正确性的目的,却无形中导致线程的并发性下降。线程从并行执行,变成了串行执行。与字节使用单线程无异。信号量,是相对折中的一种处理方式,既能保证同步,数据不混乱,又能提高线程并发。信号量常用的有以下一些函数:

/* 初始化信号量,值为value,可以设定共享还是非共享 */
int sem_init(sem_t *sem, int pshared, unsigned int value);
/* 释放锁 */
int sem_destroy(sem_t *sem);
/* 信号量减1,如果小于0,就阻塞等待 */
int sem_wait(sem_t *sem);
/* 和wait类似,不阻塞 */
int sem_trywait(sem_t *sem);
/* 和wait类似,到了时间自动解除阻塞 */
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
/* 使信号量加1,如果有等待sem的线程,就唤醒这些线程 */
int sem_post(sem_t *sem);

下面是一个使用信号量实现生产者消费者模型的例子:

#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>
#define MAX 100

sem_t sp,sc;
int arr[5];

void *produce(void *arg){
    int i = 0;
    int n = 1;
    while(n<=MAX){
        sem_wait(&sp);/* 拿空位置 */
        arr[i] = n++;
        i = (i+1)%5;
        sem_post(&sc);/* 产品数目加1 */
    }
    return (void *)NULL;
}

void *consume(void *arg){
    int i = 0;
    int n = 0;
    while(n<MAX){
        sem_wait(&sc);
        n = arr[i];
        i = (i+1)%5;
        printf("consume %d\n",n);
        sem_post(&sp);
    }
    return (void *)NULL;
}

int main(int argc, char const *argv[])
{
    sem_init(&sp,0,5);/* 开始有5个空位置 */
    sem_init(&sc,0,0);/* 开始一个产品也没有 */
    pthread_t pt[2];
    pthread_create(&pt[0],NULL,produce,NULL);
    pthread_create(&pt[1],NULL,consume,NULL);
    pthread_join(pt[0],NULL);
    pthread_join(pt[1],NULL);
    sem_destroy(&sp);
    sem_destroy(&sc);
    return 0;
}

读写锁

与互斥量类似,但读写锁允许更高的并行性。读写锁是一把锁,只不过读写锁有两个不同的加锁方式:可以以读的方式加锁,也可以以写的方式加锁。读写锁有以下的特性:

  1. 读写锁是”写模式加锁“时,解锁前,所有对该锁加锁的线程都会被阻塞。
  2. 读写锁是“读模式加锁”时,如果线程以读模式对其加锁会成功;如果以写模式进行加锁会阻塞。
  3. 读写锁是“读模式加锁”时,既有视图以写模式加锁的线程,也有试图以读模式加锁的线程,那么读写锁会阻塞随后的读模式锁。读锁、写锁并行阻塞,写锁优先级高。

总结起来就一句话:写独占,读共享,写锁优先级高。

读写锁有以下的一些常用函数:

/* 初始化读写锁 */
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_tryrdlock(pthread_rwlock_t *rwlock); 
/* 以写的方式加锁,阻塞 */
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); 
/* 以写的方式加锁,不阻塞 */
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
/* 解锁 */
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); 

下面以一个简单的例子来说明读写锁的特性:

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

pthread_rwlock_t rwlock;
int n;

void *reader(void *arg){
    int id = (int)arg;
    while(n <= 100){
        pthread_rwlock_rdlock(&rwlock);
        printf("reading thread %d reading data %d\n",id,n);
        sleep(1);
        printf("reading thread %d finish reading\n",id);   
        pthread_rwlock_unlock(&rwlock);
    }
    return NULL;
}

void *writer(void *arg){
    int id = (int)arg;
    while(n <= 100){
        pthread_rwlock_wrlock(&rwlock);
        printf("writing thread %d writing data %d\n",id,++n);
        sleep(1);
        printf("writing thread %d finish reading\n",id);   
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
    return NULL;
}

int main(int argc, char const *argv[])
{
    n = 0;
    pthread_rwlock_init(&rwlock,NULL);
    int i;
    pthread_t tid[3];
    for(i=0;i<2;i++){
        pthread_create(&tid[i],NULL,reader,(void *)(i+1));
    }
    pthread_create(&tid[2],NULL,writer,(void *)3);
    for(i=0;i<3;i++){
        pthread_join(tid[i],NULL);
    }
    return 0;
}

可以看到的现象是,读模式加锁时,其他读线程可以进入,写线程加锁的时候,所有的线程都阻塞。值得注意的是,如果写线程后面不sleep一秒的话,就写线程一个人自己玩,因为解锁之后马上加锁,写线程很容易抢到这把锁。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341

推荐阅读更多精彩内容

  • Q:为什么出现多线程? A:为了实现同时干多件事的需求(并发),同时进行着下载和页面UI刷新。对于处理器,为每个线...
    幸福相依阅读 1,565评论 0 2
  • 转载自:https://halfrost.com/go_map_chapter_one/ https://half...
    HuJay阅读 6,110评论 1 5
  • 线程 在linux内核那一部分我们知道,线程其实就是一种特殊的进程,只是他们共享进程的文件和内存等资源,无论如何对...
    大雄good阅读 661评论 0 2
  • 线程 线程的概念 典型的UNIX进程可以看成只有一个控制线程:一个进程在同一时刻只做一件事。有了多个控制线程后,在...
    ColdWave阅读 1,446评论 0 0
  • 多线程系列文章源码头文件内容: #include #include #include 作为程序员,就是要减少重复劳...
    batbattle阅读 903评论 0 1