多线程编程

多线程编程之Linux环境下的多线程(一)
多线程编程之Linux环境下的多线程(二)
多线程编程之Linux环境下的多线程(三)

1. Linux环境下的线程

相对于其他操作系统,Linux系统内核只提供了轻量级进程的支持,并未实现线程模型。Linux是一种“多进程单线程”的操作系统,Linux本身只有进程的概念,而其所谓的“线程”本质上在内核里仍然是进程。

进程是资源分配的单位,同一进程中的多个线程共享该进程的资源(如作为共享内存的全局变量)。Linux中所谓的“线程”只是在被创建时clone了父进程的资源,因此clone出来的进程表现为“线程”,这一点一定要弄清楚。因此,Linux“线程”这个概念只有在打引号的情况下才是最准确的。

目前Linux中最流行的线程机制为LinuxThreads,所采用的就是线程-进程“一对一”模型,调度交给核心,而在用户级实现一个包括信号处理在内的线程管理机制。LinuxThreads由Xavier Leroy负责开发完成,并已绑定在GLIBC中发行,它实现了一种BiCapitalized面向Linux的Posix 1003.1c “pthread”标准接口。Linuxthread可以支持Intel、Alpha、MIPS等平台上的多处理器系统。

需要注意的是,Linuxthread线程模型存在一些缺陷,尤其是在信号处理、调度和进程间同步原语方面都存在问题。并且,这个线程模型也不符合POSIX标准的要求。为了解决LinuxThread的缺陷,RedHat开发了一套符合POSIX标准的新型线程模型:NPTL(Native POSIX Thread Library)。关于Linuxthread与NPTL的比较,请参考文章:Linux 线程模型的比较:LinuxThreads 和 NPTL

2 Linux环境下的多线程编译支持

按照POSIX 1003.1c 标准编写的程序与Linuxthread 库相链接即可支持Linux平台上的多线程,在程序中需包含头文件pthread. h,在编译链接时使用命令:

gcc -D -REENTRANT -lpthread xxx. c

其中-REENTRANT宏使得相关库函数(如stdio.h、errno.h中函数) 是可重入的、线程安全的(thread-safe),-lpthread则意味着链接库目录下的libpthread.a或libpthread.so文件。

在一个多线程程序里,默认情况下,只有一个errno变量供所有的线程共享。在一个线程准备获取刚才的错误代码时,该变量很容易被另一个线程中的函数调用所改变。类似的问题还存在于fputs之类的函数中,这些函数通常用一个单独的全局性区域来缓存输出数据。

为解决这个问题,需要使用可重入的例程。可重入代码可以被多次调用而仍然工作正常。编写的多线程程序,通过定义宏_REENTRANT来告诉编译器我们需要可重入功能,这个宏的定义必须出现于程序中的任何#include语句之前。

_REENTRANT为我们做三件事情,并且做的非常优雅:

  • 它会对部分函数重新定义它们的可安全重入的版本,这些函数名字一般不会发生改变,只是会在函数名后面添加_r字符串,如函数名gethostbyname变成gethostbyname_r。

  • stdio.h中原来以宏的形式实现的一些函数将变成可安全重入函数。

  • 在error.h中定义的变量error现在将成为一个函数调用,它能够以一种安全的多线程方式来获取真正的errno的值。

3. Linux环境下的多线程函数

3.1 线程创建

在进程被创建时,系统会为其创建一个主线程,而要在进程中创建新的线程,则可以调用pthread_create函数:

#include <pthread.h>
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

参数说明:

  • thread:指向pthread_create类型的指针,用于引用新创建的线程。
  • attr:用于设置线程的属性,一般不需要特殊的属性,所以可以简单地设置为NULL。
  • start_routine:传递新线程所要执行的函数地址。
  • arg:新线程所要执行的函数的参数。

返回值:
调用如果成功,则返回值是0;如果失败则返回错误代码。

每个线程都有自己的线程ID,以便在进程内区分。线程ID在pthread_create调用时回返给创建线程的调用者;一个线程也可以在创建后使用pthread_self()调用获取自己的线程ID:

3.2 线程退出

线程的退出方式有三种:

  • 执行完成后隐式退出;
  • 由线程本身显示调用pthread_exit 函数退出;

pthread_exit (void * retval);

  • 被其他线程用pthread_cance函数终止:

pthread_cancel (pthread_t thread);

如果一个线程要等待另一个线程的终止,可以使用pthread_join函数,该函数的作用是调用pthread_join的线程将被挂起直到线程ID为参数thread的线程终止:

pthread_join (pthread_t thread, void** threadreturn);

3.3 简单的多线程示例

一个简单的Linux多线程示例如下:

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

void *thread_function(void *arg);

char message[] = "Hello World";

int main()
{
    int res;
    pthread_t a_thread;
    void *thread_result;

    res = pthread_create(&a_thread, NULL, thread_function, (void *)message);
    if (res != 0)
    {
        perror("Thread creation failed!");
        exit(EXIT_FAILURE);
    }

    printf("Waiting for thread to finish.../n");
    
    res = pthread_join(a_thread, &thread_result);
    if (res != 0)
    {
        perror("Thread join failed!/n");
        exit(EXIT_FAILURE);
    }

    printf("Thread joined, it returned %s/n", (char *)thread_result);
    printf("Message is now %s/n", message);

    exit(EXIT_FAILURE);
}

void *thread_function(void *arg)
{
    printf("thread_function is running. Argument was %s/n", (char *)arg);
    sleep(3);
    strcpy(message, "Bye!");
    pthread_exit("Thank you for your CPU time!");
}

编译语句如下:

gcc -D_REENTRANT thread1.c -o thread1 -lpthread

输出结果是:

$./thread1[输出]: thread_function is running. Argument was Hello World
Waiting for thread to finish...
Thread joined, it returned Thank you for your CPU time!
Message is now Bye!

在这个例子中,pthread_exit(void *retval)本身返回的就是指向某个对象的指针,因此,pthread_join(pthread_t th, void **thread_return);中的thread_return是二级指针,指向线程返回值的指针。可以看到,我们创建的新线程修改的数组message的值,而原先的线程也可以访问该数组。如果我们调用的是fork而不是pthread_create,就不会有这样的效果了。因为fork创建子进程之后,子进程会拷贝父进程,两者分离,相互不干扰,而线程之间则是共享进程的相关资源。

4. 线程互斥

互斥意味着具有“排它性”,即两个线程不能同时进入被互斥保护的代码。Linux下可以通过pthread_mutex_t 定义互斥体机制完成多线程的互斥操作,该机制的作用是对某个需要互斥的部分,在进入时先得到互斥体,如果没有得到互斥体,表明互斥部分被其它线程拥有,此时欲获取互斥体的线程阻塞,直到拥有该互斥体的线程完成互斥部分的操作为止。 互斥量的操作函数包括:

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t, *mutexattr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destory(pthread_mutex_t *mutex);

与其他函数一样,这些函数成功时返回0,失败时将返回错误代码,但这些函数并不设置errno,所以必须对函数的返回代码进行检查。下面以一个例子来说明用法:

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

#define SIZE 1024
char buffer[SIZE];

void *thread_function(void *arg);
pthread_mutex_t mutex;

int main()
{
    int res;
    pthread_t a_thread;
    void *thread_result;

    res = pthread_mutex_init(&mutex, NULL);
    if (res != 0)
    {
        perror("Mutex init failed!");
        exit(EXIT_FAILURE);
    }

    res = pthread_create(&a_thread, NULL, thread_function, NULL);
    if (res != 0)
    {
        perror("Thread create failed!");
        exit(EXIT_FAILURE);
    }

    printf("Input some text. Enter 'end' to finish/n");

    while (1)
    {
        pthread_mutex_lock(&mutex);
        scanf("%s", buffer);
        pthread_mutex_unlock(&mutex);
        if (strncmp("end", buffer, 3) == 0)
            break;
        sleep(1);
    }

    res = pthread_join(a_thread, &thread_result);
    if (res != 0)
    {
        perror("Thread join failed!");
        exit(EXIT_FAILURE);
    }

    printf("Thread joined/n");

    pthread_mutex_destroy(&mutex);

    exit(EXIT_SUCCESS);
}

void *thread_function(void *arg)
{
    sleep(1);

    while (1)
    {
        pthread_mutex_lock(&mutex);
        printf("You input %d characters/n", strlen(buffer));
        pthread_mutex_unlock(&mutex);
        if (strncmp("end", buffer, 3) == 0)
            break;
        sleep(1);
    }
}

编译语句为:

gcc -D_REENTRANT thread4.c -o thread4 -lpthread

运行结果为:

$ ./thread2
Input some text. Enter 'end' to finish 123 You input 3 characters 1234 You input 4 characters 12345 You >input 5 characters
end
You input 3 characters
Thread joined

5. 读写锁

当有一个线程已经持有互斥锁时,互斥锁将所有试图进入临界区的线程都阻塞住。但是考虑一种情形,当前持有互斥锁的线程只是要读访问共享资源,而同时有其它几个线程也想读取这个共享资源,但是由于互斥锁的排它性,所有其它线程都无法获取锁,也就无法读访问共享资源了,但是实际上多个线程同时读访问共享资源并不会导致问题。

读写锁与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态要么是不加锁状态,一次只能有一个线程对其加锁。而读写锁有三种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。

虽然读写锁的实现有很多种不同的方式,不过当读写锁处于读模式锁住状态时,如果有另外的线程试图加以写模式锁,读写锁通常都会阻塞随后的读模式加锁请求,这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。

读写锁的特点如下:

  • 如果有其它线程读数据,则允许其它线程执行读操作,但不允许写操作。
  • 如果有其它线程写数据,则其它线程都不允许读、写操作。

读写锁分为读锁和写锁,规则如下:

  • 如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁。
  • 如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁。

读写锁也叫共享-独占锁,读写锁非常适合对数据结构读的次数远大于写次数的情况。读写锁的接口函数包括以下几个:

5.1 创建与销毁
#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
5.2 读加锁和写加锁
#include <pthread.h>
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 <pthread.h>
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
5.4 读写锁程序示例

下面是一个使用读写锁来实现 4 个线程读写一段数据是实例。在此示例程序中,共创建了 4 个线程,其中两个线程用来写入数据,两个线程用来读取数据。当某个线程读操作时,其他线程允许读操作,却不允许写操作;当某个线程写操作时,其它线程都不允许读或写操作。

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
 
pthread_rwlock_t rwlock; //读写锁
int num = 1;
 
//读操作,其他线程允许读操作,却不允许写操作
void *fun1(void *arg)
{
    while(1)
    {
        pthread_rwlock_rdlock(&rwlock);
        printf("read num first===%d\n",num);
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
}
 
//读操作,其他线程允许读操作,却不允许写操作
void *fun2(void *arg)
{
    while(1)
    {
        pthread_rwlock_rdlock(&rwlock);
        printf("read num second===%d\n",num);
        pthread_rwlock_unlock(&rwlock);
        sleep(2);
    }
}
 
//写操作,其它线程都不允许读或写操作
void *fun3(void *arg)
{
    while(1)
    {
        
        pthread_rwlock_wrlock(&rwlock);
        num++;
        printf("write thread first\n");
        pthread_rwlock_unlock(&rwlock);
        sleep(2);
    }
}
 
//写操作,其它线程都不允许读或写操作
void *fun4(void *arg)
{
    while(1)
    {
        
        pthread_rwlock_wrlock(&rwlock);
        num++;
        printf("write thread second\n");
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
}
 
int main()
{
    pthread_t ptd1, ptd2, ptd3, ptd4;
    
    pthread_rwlock_init(&rwlock, NULL);//初始化一个读写锁
    
    //创建线程
    pthread_create(&ptd1, NULL, fun1, NULL);
    pthread_create(&ptd2, NULL, fun2, NULL);
    pthread_create(&ptd3, NULL, fun3, NULL);
    pthread_create(&ptd4, NULL, fun4, NULL);
    
    //等待线程结束,回收其资源
    pthread_join(ptd1,NULL);
    pthread_join(ptd2,NULL);
    pthread_join(ptd3,NULL);
    pthread_join(ptd4,NULL);
    
    pthread_rwlock_destroy(&rwlock);//销毁读写锁
    
    return 0;
}

6. 条件变量

7. 信号量

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

推荐阅读更多精彩内容