UNIX环境高级编程学习笔记-线程

本文详细探讨了UNIX环境下线程的创建、共享资源管理,包括互斥量、死锁、读写锁和自旋锁的概念及应用。通过实例展示了线程如何共享进程信息,以及如何通过互斥量防止竞争条件。虽然尝试构造死锁示例未成功,但提出了避免死锁的固定加锁顺序策略。同时,读写锁作为互斥量的优化,提高了并发性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


前面在进程里也说过,父子进程共用一套程序主体(CPU执行的机器指令),不共用数据段、堆、栈(不考虑写时复制)。

线程在创建之后,就与其他线程共享所在进程的所有信息,包括可执行程序代码、程序的全局内存和堆内存、栈、文件描述符

创建线程

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);

Compile and link with -pthread
  • thread,新创建的线程 ID 会被设置成 thread 指向的内存单元。
  • attr,定制线程属性,指向 pthread_attr_t 结构体(由 pthread_attr_init 初始化)

11-4-pthread.c

#include "apue.h"
#include <pthread.h>

pthread_t ntid;

void
printids(const char *s)
{
    pid_t pid;
    pthread_t tid;

    pid = getpid();
    tid = pthread_self();
    printf("%s pid %lu tid %lu (0x%lx) \n", 
           s, (unsigned long)pid, (unsigned long)tid,(unsigned long)tid);
}

void *
thr_fn(void *arg)
{
    printids("new thread: ");
    return ((void *)0);
}

int main(void)
{
    int err;

    err = pthread_create(&ntid, NULL, thr_fn, NULL);
    if (err != 0) {
        err_exit(err, "can't create thread");
    }
    printids("main thread:");
    sleep(1);
    exit(0);
}

编译:

$ gcc 11-4-pthread.c -g -O2 -o thread.out -lapue -lpthread

执行:

$ ./thread.out 
main thread: pid 8272 tid 139793605408576 (0x7f243c313740) 
new thread:  pid 8272 tid 139793596901120 (0x7f243baf6700)

上面的流程如下:

简单的线程

  • 3过程调用 pthread_creade 创建线程之后,新的线程从 thr_fn 开始执行

共享资源

同一进程内的所有线程共用堆、栈会造成竞争,读操作需要一个存储器周期,写操作需要两个存储器周期。

互斥量

互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行设置(加锁),访问结束后释放互斥量(解锁)

动作函数参数描述返回值
创建pthread_mutex_initpthread_mutex_t *mutex
const pthread_mutexattr_t *attr
创建 mutex
加锁pthread_mutex_lockpthread_mutex_t *mutex对 mutex 加锁。若 mutex 已锁,则线程阻塞,直到 mutex 可用
pthread_mutex_trylockpthread_mutex_t *mutex无阻塞加锁成功返回0
解锁pthread_mutex_unlockpthread_mutex_t *mutex解锁成功返回0
删锁pthread_mutex_destroypthread_mutex_t *mutex释放资源

一个使用互斥量的示例:

#include <stdlib.h>
#include <pthread.h>
#include "apue.h"

/*
 * 一个锁对象
 * f_count:每当一个线程成功获取一个锁,
 *        应对该值+1,用于记录锁被引用的次数
 * f_lock:互斥量
 * f_id:读、写操作的资源
 */
struct foo {
    int f_count;
    pthread_mutex_t f_lock;
    int f_id;
};

/*
 *可能造成冲突的资源
 */
struct foo *ziyuan;


/*
 * 初始化一个锁对象
 * - 为锁对象分配内存
 * - f_count = 1
 */
struct foo *
foo_alloc(int id)
{
    struct foo *fp;

    if ((fp = malloc(sizeof(struct foo))) != NULL ) {
        fp->f_count = 1;
        fp->f_id = id;
        if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
            free(fp);
            return(NULL);
        }
    }
    return(fp);
};

/*
 *添加一个到锁对象的引用,f_count + 1
 */
void
foo_hold(struct  foo *fp)
{
    pthread_mutex_lock(&fp->f_lock);
    fp->f_count++;
    pthread_mutex_unlock(&fp->f_lock);
}

/*
 *删除一个到锁对象的引用
 */
void
foo_rele(struct foo *fp)
{
    pthread_mutex_lock(&fp->f_lock);
    if (--fp->f_count == 0) {
        pthread_mutex_unlock(&fp->f_lock);
        pthread_mutex_destroy(&fp->f_lock);
        free(fp);
    } else {
        pthread_mutex_unlock(&fp->f_lock);
    }
}

/*
 * 线程处理函数
 */
void *
thr_fn(void *arg)
{
    //写操作
    foo_hold(ziyuan);
    ziyuan->f_id = 7;
    foo_rele(ziyuan);

    //读操作
    foo_hold(ziyuan);
    printf("child thread: id is %d\n", ziyuan->f_id);
    foo_rele(ziyuan);

    return ((void *)1);
}

int main(void)
{
    int err;
    pthread_t ntid;
    void *tret;

    ziyuan = foo_alloc(5);

    //创建线程
    err = pthread_create(&ntid, NULL, thr_fn, NULL);
    if (err != 0) {
        err_exit(err, "can't create thread");
    }

    //写操作
    foo_hold(ziyuan);
    ziyuan->f_id = 6;
    foo_rele(ziyuan);

    //读操作
    foo_hold(ziyuan);
    printf("main thread: id is %d\n", ziyuan->f_id);
    foo_rele(ziyuan);

    //主线程阻塞
    err = pthread_join(ntid, &tret);
    if (err != 0) {
        err_exit(err, "join thread failed");
    }
    return 0;
}

编译:16

$ gcc 11-6-mutex.c -g -O2 -o 11-6-mutex.out -lapue -lpthread

执行:

$ ./11-6-mutex.out 
main thread: id is 6
child thread: id is 7

图示演示的一个简化的foo_hold、foo_rele的流程

使用互斥量保护数据结构

死锁

本来想写个简单的程序演示一下,写到一般发现我还没有能力造成那个死锁的点。

设想两个线程A和B,在第一时间线程A已经对资源A加锁成功,线程B对资源B加锁;第二时间,线程A试图锁住资源B,线程B试图锁住资源A。这样就造成了两个线程都阻塞在了加锁这个操作上。

我试图通过下面的例子重现死锁的现象,然而失败了:

#include "apue.h"
#include <pthread.h>

struct foo {
    int f_count;
    pthread_mutex_t f_lock;
    int f_id;
};

/*
 * 造成冲突的资源
 */
struct foo *ziyuan1;
struct foo *ziyuan2;

/*
 * 初始化一个锁对象
 * - 为锁对象分配内存
 * - f_count = 1
 */
struct foo *
foo_alloc(int id)
{
    struct foo *fp;
    if ((fp = malloc(sizeof(struct foo))) != NULL) {
        fp->f_count = 1;
        fp->f_id = id;
        if (pthread_mutex_init(&fp->f_lock, NULL) != 0) {
            free(fp);
            return (NULL);
        }
    }
    return fp;
}

/*
 * 添加一个到锁对象的引用
 */
void *
foo_hold(struct foo *fp)
{
    pthread_mutex_lock(&fp->f_lock);
    fp->f_count++;
    pthread_mutex_unlock(&fp->f_lock);
}

/*
 *删除一个到锁对象的引用
 */
void *
foo_rele(struct foo *fp)
{
    pthread_mutex_lock(&fp->f_lock);
    if (--fp->f_count == 0) {
        pthread_mutex_unlock(&fp->f_lock);
        pthread_mutex_destroy(&fp->f_lock);
        free(fp);
    } else {
        pthread_mutex_unlock(&fp->f_lock);
    }
}

/*
 * 线程处理函数
 */
void *
thr_fn(void *arg)
{
    foo_hold(ziyuan2);
    printf("thild thread: id is %d\n", ziyuan2->f_id);
    sleep(1);
    foo_hold(ziyuan1);
    printf("thild thread: id is %d\n", ziyuan1->f_id);

    foo_rele(ziyuan1);
    foo_rele(ziyuan2);

    return ((void *)1);
}

int
main(void)
{
    int err;
    pthread_t ntid;
    void *tret;

    ziyuan1 = foo_alloc(5);
    ziyuan2 = foo_alloc(7);

    //创建线程
    err = pthread_create(&ntid, NULL, thr_fn, NULL);
    if (err != 0) {
        err_exit(err, "can't create thread");
    }

    //写操作
    foo_hold(ziyuan1);
    printf("main thread: id is %d\n", ziyuan1->f_id);
    sleep(1);
    foo_hold(ziyuan2);
    printf("main thread: id is %d\n", ziyuan2->f_id);

    foo_rele(ziyuan1);
    foo_rele(ziyuan2);

    //主线程阻塞
    err = pthread_join(ntid, &tret);
    if (err != 0) {
        err_exit(err, "join thread failed");
    }
    return 0;
}

编译执行后的结果并没有造成程序的一直进行:

$ gcc 11-6-deadlock.c -g -O2 -o 11-6-deadlock.out -lapue -lpthread
$ ./11-6-deadlock.out 
main thread: id is 5
thild thread: id is 7
main thread: id is 7
thild thread: id is 5

所以目前我只能从理论上来分析这个问题。

一个解决这个问题的方法,当需要两个及以上资源加锁时,永远按照固定的顺序对资源加锁。

读写锁

读写锁是互斥量的一种优化(具有更高的并发性),读的优化。

读一个资源的时候不会对资源更改,当线程A读该资源时,资源处于读加锁状态。线程B也对该资源读加锁,成功;线程C对该资源写加锁,失败。

当线程C对该资源写加锁时,资源处于写加锁状态。线程A对该资源读加锁,失败;线程D对该资源写加锁,失败。

动作函数参数描述返回值
创建pthread_rwlock_initpthread_rwlock_t *restrict rwlock
pthread_rwlockattr_t *restrict attr
初始化读写锁,attr为null指针设置默认属性成功返回0
加锁-读pthread_rwlock_rdlockpthread_rwlock_t *restrict rwlock成功返回0
pthread_rwlock_tryrdlock
加锁-写pthread_rwlock_wrlockpthread_rwlock_t *restrict rwlock成功返回0
pthread_rwlock_tryrdlock
解锁pthread_rwlock_unlockpthread_rwlock_t *restrict rwlock成功返回0
删锁pthread_rwlock_destroypthread_rwlock_t *restrict rwlock释放为读写锁分配的资源成功返回0

自旋锁

屏障

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值