线程与线程同步

线程的概念

进程:一个正在执行的程序,是资源分配的最小单元。

线程:有时又称为轻量级进程,程序执行的最小单位,系统独立调度和分派cpu的基本单位,它是进程中的一个实体。一个进程中可以有多个线程,这些线程共享进程的所有资源,线程本身只包含一些必不可少的资源。

并发:指在同一时刻,只能有一条指令执行,但多个进程指令被快速轮换执行,使得宏观上具有多个进程同时执行的效果。

并行:指在同一时刻,有多条指令在多个处理器上同时执行。(真正意义上的同时发生)

同步:彼此有依赖关系的调用不应该“同时发生”,而同步就是要阻止那些“同步发生”的事情。

异步:异步的概念和同步相对,任何两个彼此独立的操作是异步的,它表明事情独立的发生。

多线程的优势:

1、在多处理器中开发程序的并行性

2、在等待慢速IO操作时,程序可以执行其他操作,提高并发性

3、模块化的编程,能更清晰的表达程序中独立事件的关系,结构清晰

4、占用较少的系统资源

线程ID

线程进程
标识符类型pthread_tpid_t
获取idpthread_self()getpid()
创建pthread_create()fork()

线程相关函数

获取当前线程ID

#include <pthread.h>

pthread_t pthread_self(void);

创建线程

#include <pthread.h>

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

第一个参数,新线程的id,如果成功则新线程的id回填到thread指向的内存。

第二个参数,线程属性(调度策略、继承性、分离性…)

第三个参数:回调函数(新线程要执行的函数)

第四个参数:回调函数的参数。(如果需要多个参数,则一般是定义一个结构体指针,然后结构体中传参数)

返回值:成功返回0,失败返回错误码。

编译时需要连接库libpthread , 即带上参数**-lpthread**

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void print_id(char *s)
{
        pid_t pid;
        pthread_t tid;
        pid = getpid();
        tid = pthread_self();
        printf("%s pid=%u, tid=0x%x\n", s, pid, tid);
}

void *thread_fun(void *arg)
{
        print_id(arg);
        return (void *)0;
}

int main()
{
        pthread_t ntid;
        int err;
        err = pthread_create(&ntid, NULL, thread_fun, "new child");
        if (err != 0)
        {
                printf("pthread_create failed\n");
                return -1;
        }
        print_id("main thread");
        sleep(2);

        return 0;
}

运行结果:

main thread pid=21178, tid=0xf4b23740
new child pid=21178, tid=0xf47ff6c0

线程退出

#include <pthread.h>
void pthread_exit(void *retval);

参数:retval。线程退出时携带的数据。注意,不能用它来返回一个指向局部变量的指针,因为线程调用该函数后,这个局部变量就不存在了。

只要调用该函数,当前线程马上退出了,并且不会影响到其他线程的正常运行,不管是在子线程或主线程中都可以使用。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void print_id(char *s)
{
        pid_t pid;
        pthread_t tid;
        pid = getpid();
        tid = pthread_self();
        printf("%s pid=%u, tid=0x%x\n", s, pid, tid);
}

void *thread_fun(void *arg)
{
        print_id(arg);
        sleep(2);
        return (void *)0;
}

int main()
{
        pthread_t ntid;
        int err;
        err = pthread_create(&ntid, NULL, thread_fun, "new child");
        if (err != 0)
        {
                printf("pthread_create failed\n");
                return -1;
        }

        printf("main thread.\n");
        pthread_exit(NULL);
        return 0;
}

线程回收

子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函数是pthread_join()。这是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出时函数解除阻塞进行资源的回收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。

#include <pthread.h>
// 返回值:成功,返回0;失败,返回错误码
int pthread_join(pthread_t thread, void **retval);

参数:

  • thread:要被回收的子线程的线程ID
  • retval:一个传出参数,存储了pthread_exit()传递出的数据,如果不需要这个数据,可以指定为NULL。

线程分离

线程分离函数pthread_detach(),调用这个函数后指定的子线程就可以和主线程分离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后再主线程中使用pthread_join()就回收不了子线程的资源了。(但如果主线程先退出了,分离后的子线程也会退出)

#include <pthread.h>

int pthread_detach(pthread_t thread);

其他函数

线程取消

线程取消是指在某些特定情况下在一个线程中杀死另一个线程。

使用这个函数杀死一个线程需要分两步:

1、在线程A中调用线程取消函数pthread_cancel,指定杀死线程B,这时候B是死不了的。

2、在线程B中进程一次调用系统调用,否则线程B可以一直运行。

#include <pthread.h>

// thread:待取消的线程ID
int pthread_cancel(pthread_t thread);

线程比较函数

#include <pthread.h>

int pthread_equal(pthread_t t1, pthread_t t2);

返回值:如果两个线程相等,则返回非0;如果不相等,则返回0。

线程同步

线程同步的概念

当有一个线程正在对内存中的共享资源进行访问的时候,其他线程不能对这块内存进行操作,直到该线程对这块内存访问完为止,其他线程才能访问这块内存。这就是线程同步。

同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的。

线程同步

对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步。常用的线程同步方式有4种:互斥锁、读写锁、条件变量、信号量。所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也被称为临界资源。

死锁

死锁是指两个或多个进程(线程)在运行过程中因争夺资源而造成的一种僵局,若无外力作用,这些进程(线程)都将无法向前推进。

出现死锁的一般场景:

  • 加锁之后忘记解锁。(或者粗心每解锁,或者在异常分支没解锁,且代码运行到该异常分支)
  • 重复加锁,造成死锁。比如在A函数加锁了,然后进入它的子函数B又加了一遍锁,导致重复加锁
  • 程序中有多个共享资源,因此由多把锁,随意加锁,导致相互被阻塞。(比如线程A拥有了资源a,线程B拥有了资源b。现在线程A想要资源b,线程B想要资源a,它们相互等对方释放资源,但是手上又有对方需要的资源,且不释放,导致死锁)

互斥锁和条件变量

互斥锁

互斥锁指代相互排斥(mutual exclusion),用于保护临界区,以保证任何时刻只有一个线程在执行其中的代码(假设互斥锁由多个线程共享),或者任何时刻只有一个进程在执行其中的代码(假设互斥锁由多个进程共享)。互斥锁是线程同步最常用的一种方式。

互斥锁可以静态分配并静态初始化,也可以动态分配。

如果互斥锁变量是静态分配的,则可以把它初始化为常值PTHREAD_MUTEX_INITALIZER。

static pthread_mutex_t lock = PTHREAD_MUTEX_INITALIZER

在Linux中,互斥锁的数据类型为pthread_mutex_t。

互斥锁的一些函数如下:

#include <pthread.h>

// 初始化互斥锁
// 参数:mutex:互斥锁变量的地址;attr:互斥锁的属性,一般使用默认属性即可,即指定为NULL
int pthread_mutex_init(phtread_mutex_t *restric mutex, const pthread_mutexattr_t 							*restrict attr);

// 释放互斥锁资源
int pthread_mutex_destroy(pthread_mutex *mutex);

// 加锁。如果其他线程已经上锁,则该函数会阻塞
int pthread_mutex_lock(pthread_mutex_t *mutex);

// 加锁的非阻塞版本
int pthread_mutex_trylock(pthread_mutex *mutex);

// 解锁。
// 注意:不是所有的线程都可以对互斥锁解锁的,只有加锁的那个线程才能解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

生产者-消费者问题

同步中有一个称为生产者-消费者(producer-consumer)问题的经典问题,也称为有界缓冲区(bundoed buffer)问题。一个或多个生产者(线程或者进程)创建着一个个的数据条目,然后这些条目由一个或多个消费者(线程或者进程)处理。数据条目在生产者和消费者之间是使用某种类型的IPC传递的。

比如,如下的shell命令行就是一个生产者-消费者问题:

grep pattern chapters.* | wc -l

grep是单个生产者,wc是单个消费者。IPC类型是管道。

举例一:多生产者-单消费者(仅考虑生产者之间的同步)

第一个例子中,我们只关心多个生产者线程之间的同步。直到所有生产者线程都完成工作后,才启动消费者线程。

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <sys/param.h>

// 生产者存放的最大条目数
#define MAXNITEMS 1000000

// 待创建生产者线程的最大数目

#define MAXNTHREADS 100

// 共享的变量
int nitems;
// 将互斥锁和共享数据放到一个结构体中
typedef struct {
        pthread_mutex_t mutex;
        int buff[MAXNITEMS];
        int nput;//buff数组中下一次存放的元素下标
        int nval; // 下一次存放的值
} shared;

shared g_shared;

// 生产者和消费者线程的回调函数
void *produce(void *);
void *consume(void *);

int main(int argc, int **argv)
{
        int i,nthreads,count[MAXNTHREADS];
        pthread_t tid_produce[MAXNTHREADS],tid_consume;
        // 生产者存放的条目数 以及生产者线程的数目由用户指定
        if (argc != 3)
        {
                printf("parame num error\n");
                return -1;
        }
        // 初始化互斥锁
        pthread_mutex_init(&g_shared.mutex, NULL);
        nitems = MIN(atoi(argv[1]), MAXNITEMS);
        nthreads = MIN(atoi(argv[2]), MAXNTHREADS);

        // 创建生产者线程
        for (i = 0; i < nthreads; i++)
        {
                count[i]=0;
                pthread_create(&tid_produce[i], NULL, produce, &count[i]);
        }

        // 等待所有的生成者线程执行完毕,然后线程回收
        for (i = 0; i < nthreads; i++)
        {
                pthread_join(tid_produce[i], NULL);
                printf("count[%d] = %d\n", i, count[i]);
        }

        // 等所有生产者线程终止后,启动单个消费者
        pthread_create(&tid_consume, NULL, consume, NULL);
        pthread_join(tid_consume, NULL);
    	# 释放互斥锁资源
        pthread_mutex_destroy(&g_shared.mutex);
        return 0;
}

// 生产者回调函数
void *produce(void *arg)
{
        for ( ; ;)
        {
                // 先上锁
                pthread_mutex_lock(&g_shared.mutex);
                if (g_shared.nput >= nitems)
                {
                        // 数组已经满了,直接返回
                        pthread_mutex_unlock(&g_shared.mutex);
                        return NULL;
                }
                // 对共享数据进行操作
                g_shared.buff[g_shared.nput] = g_shared.nval;
                g_shared.nput++;
                g_shared.nval++;
                pthread_mutex_unlock(&g_shared.mutex);
                *((int *)arg) += 1;
        }
}

void *consume(void *arg)
{
        int i;
        for (i = 0; i < nitems; i++)
        {
                if (g_shared.buff[i] != i)
                {
                        printf("buff[%d] = %d\n", i, g_shared.buff[i]);
                }
        }
        return NULL;
}

举例二:多生产者-单消费者(考虑生产者之间的同步、生产者与消费者之间的同步)

在所有生产者线程都启动后立即启动消费者线程。这样在生产者线程产生数据的同时,消费者线程就能处理它,而不必像例一一样,消费者线程直到所有生产者线程都完成后才启动。但现在不仅要考虑生产者之间的同步,而且要考虑生产者与消费者之间的同步,以确保消费者只处理已由生产者存放的数据条目。

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <sys/param.h>

// 生产者存放的最大条目数
#define MAXNITEMS 1000000

// 待创建生产者线程的最大数目

#define MAXNTHREADS 100

// 下面的nitems和shared是各个线程共享的变量
int nitems;
typedef struct {
        pthread_mutex_t mutex;
        int buff[MAXNITEMS];
        int nput;//buff数组中下一次存放的元素下标
        int nval; // 下一次存放的值
} shared;

shared g_shared;

// 生产者和消费者线程的回调函数
void *produce(void *);
void *consume(void *);

int main(int argc, int **argv)
{
        int i,nthreads,count[MAXNTHREADS];
        pthread_t tid_produce[MAXNTHREADS],tid_consume;
        // 生产者存放的条目数 以及生产者线程的数目由用户指定
        if (argc != 3)
        {
                printf("parame num error\n");
                return -1;
        }
        // 初始化互斥锁
        pthread_mutex_init(&g_shared.mutex, NULL);
        nitems = MIN(atoi(argv[1]), MAXNITEMS);
        nthreads = MIN(atoi(argv[2]), MAXNTHREADS);
/*
        if (pthread_setconcurrency(nthreads) != 0)
        {
                printf("setconcurrency error\n");
                pthread_mutex_destroy(g_shared.mutex);
                return -1;
        }
*/
        // 创建生产者线程
        for (i = 0; i < nthreads; i++)
        {
                count[i]=0;
                pthread_create(&tid_produce[i], NULL, produce, &count[i]);
        }

        // 等待所有的生成者线程执行完毕,然后线程回收
        for (i = 0; i < nthreads; i++)
        {
                pthread_join(tid_produce[i], NULL);
                printf("count[%d] = %d\n", i, count[i]);
        }

        // 等所有生产者线程终止后,启动单个消费者
        pthread_create(&tid_consume, NULL, consume, NULL);
        pthread_join(tid_consume, NULL);
        pthread_mutex_destroy(&g_shared.mutex);
        return 0;
}

// 生产者回调函数
void *produce(void *arg)
{
        for ( ; ;)
        {
                // 先上锁
                pthread_mutex_lock(&g_shared.mutex);
                if (g_shared.nput >= nitems)
                {
                        // 数组已经满了,直接返回
                        pthread_mutex_unlock(&g_shared.mutex);
                        return NULL;
                }
                // 对共享数据进行操作
                g_shared.buff[g_shared.nput] = g_shared.nval;
                g_shared.nput++;
                g_shared.nval++;
                pthread_mutex_unlock(&g_shared.mutex);
                *((int *)arg) += 1;
        }
}

void consume_wait(int i)
{
        for ( ; ;)
        {
            	// 生产者与消费者之间的同步,如果生产者已经加锁了,则这里会阻塞,直到解锁为止
                pthread_mutex_lock(&g_shared.mutex);
            	// 生产者还没生产数据,消费者就必须等
                if (i < g_shared.nput)
                {
                        pthread_mutex_unlock(&g_shared.mutex);
                        return;
                }
                pthread_mutex_unlock(&g_shared.mutex);
        }
}

void *consume(void *arg)
{
        int i;
        for (i = 0; i < nitems; i++)
        {
                consume_wait(i);
                if (g_shared.buff[i] != i)
                {
                        printf("buff[%d] = %d\n", i, g_shared.buff[i]);
                }
        }
        return NULL;
}

在上面的consume_wait中,当期待的条目尚未准备好时,会一遍一遍的循环(for死循环),每次给互斥锁解锁又加锁,这称为轮询。这种轮询方式比较浪费CPU资源。在实际编程中,需要尽量避免这种情况,可以引入条件变量。

条件变量

互斥锁用于上锁,而条件变量则是用于等待,一般是互斥锁和条件变量配合使用。

条件变量类型对应的类型为pthread_cond_t。

条件变量也可以静态初始化:

pthread_contd_t cond = PTHREAD_COND_INITIALIZER;

下面是条件变量使用的一些函数:

#include <pthread.h>

// 初始化
// cond:条件变量的地址
// attr:条件变量属性,一般使用默认属性,指定为NULL
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);

// 销毁释放资源
int pthread_cond_destroy(pthread_cond_t *cond);

// 线程阻塞函数,哪个线程调用这个函数,哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

// 将线程阻塞一定的时间长度,时间到达后,线程就解除阻塞了
/*
表示的时间是从1970.1.1到某个时间点的时间,总长度使用秒/纳秒表示
struct timespec {
	time_t tv_sec;
	long tv_nsec;
}
*/
int pthread_cond_timewait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

// 唤醒阻塞在条件变量上的线程,至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);

// 唤醒阻塞在条件变量上的线程,被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);

当我们调用pthread_cond_wait时,该函数会原子的执行以下两个动作:

(1)给对应的互斥锁解锁。这样做是为了避免死锁。

(2)把调用线程置于休眠状态,直到另外某个线程就本条件变量调用pthread_cond_signal解除阻塞,在解除阻塞的时候,函数内部会帮助这个线程再次将这个互斥锁锁上,并继续向下访问临界区。

举例三:多生产者-单消费者(条件变量)

针对举例二轮询的问题,可以用条件变量解决。具体代码如下:

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>
#include <sys/param.h>

// 生产者存放的最大条目数
#define MAXNITEMS 1000000

// 待创建生产者线程的最大数目

#define MAXNTHREADS 100

// 各个线程共享的变量
int nitems;
int buff[MAXNITEMS];
// 用于生产者之间
struct {
        pthread_mutex_t mutex;
        int nput;//buff数组中下一次存放的元素下标
        int nval; // 下一次存放的值
} put = {
        PTHREAD_MUTEX_INITIALIZER
};

// 用于生产者与消费者之间
struct {
        pthread_mutex_t mutex;
        pthread_cond_t cond;
        int nready;
} nready = {
        PTHREAD_MUTEX_INITIALIZER, PTHREAD_COND_INITIALIZER
};

// 生产者和消费者线程的回调函数
void *produce(void *);
void *consume(void *);

int main(int argc, int **argv)
{
        int i,nthreads,count[MAXNTHREADS];
        pthread_t tid_produce[MAXNTHREADS],tid_consume;
        // 生产者存放的条目数 以及生产者线程的数目由用户指定
        if (argc != 3)
        {
                printf("parame num error\n");
                return -1;
        }

        nitems = MIN(atoi(argv[1]), MAXNITEMS);
        nthreads = MIN(atoi(argv[2]), MAXNTHREADS);

        // 创建生产者线程
        for (i = 0; i < nthreads; i++)
        {
                count[i]=0;
                pthread_create(&tid_produce[i], NULL, produce, &count[i]);
        }

        pthread_create(&tid_consume, NULL, consume, NULL);

        for (i = 0; i < nthreads; i++)
        {
                pthread_join(tid_produce[i], NULL);
                printf("count[%d] = %d\n", i, count[i]);
        }


        pthread_join(tid_consume, NULL);
        return 0;
}

// 生产者回调函数
void *produce(void *arg)
{
        for ( ; ;)
        {
                // 先上锁:生产者与生产者之间的锁
                pthread_mutex_lock(&put.mutex);
                if (put.nput >= nitems)
                {
                        // 数组已经满了,直接返回
                        pthread_mutex_unlock(&put.mutex);
                        return NULL;
                }
                // 对共享数据进行操作
                buff[put.nput] = put.nval;
                put.nput++;
                put.nval++;
                // 释放生产者与生产者之间的锁
                pthread_mutex_unlock(&put.mutex);

                // 上锁:生产者与消费者之间的锁
                pthread_mutex_lock(&nready.mutex);
                if (nready.nready == 0)
                {       // 唤醒消费者线程
                        pthread_cond_signal(&nready.cond);
                }
                nready.nready++;
                pthread_mutex_unlock(&nready.mutex);
                *((int *)arg) += 1;
        }
}


void *consume(void *arg)
{
        int i;
        for (i = 0; i < nitems; i++)
        {
                // 加锁:生产者与消费者之间的锁
                pthread_mutex_lock(&nready.mutex);
                while (nready.nready == 0)
                {
                        // 阻塞等待
                        pthread_cond_wait(&nready.cond, &nready.mutex);
                }
                nready.nready--;
                pthread_mutex_unlock(&nready.mutex);
                if (buff[i] != i)
                {
                        printf("buff[%d] = %d\n", i, buff[i]);
                }
        }
        return NULL;
}

读写锁

读写锁是互斥锁的升级版,在做读操作的时候可以提高程序的执行效率,如果所有的线程都是读操作,那么读是并行的。

读写锁的分配规则如下:

1、只要没有线程持有某个给定的读写锁用于写,那么任意数目的线程可以持有该读写锁用于读。

2、仅当没有线程持有某个给定的读写锁用于读或用于写时,才能分配该读写锁用于写。

换一句话说,只要没有线程在修改某个给定的数据,那么任意数量的线程都可以拥有该数据的读访问权。仅当没有其他线程在读或修改某个给定的数据时,当前线程才可以修改它

读写锁的数据类型为pthread_rwlock_t。如果这个类型的某个变量是静态分配的,那么可通过给它赋常值

PTHREAD_RWLOCK_INITIALIZER来初始化它。

#include <pthread.h>

// 初始化读写锁。attr:读写锁属性,一般使用默认属性,即NULL。
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);

Posix信号量

信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A、B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类的。

信号量主要阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。

信号量的类型为sem_t ,对应的头文件为<semaphore.h>

// 初始化无名信号量
// sem:信号量变量地址
// pshared:0:线程同步;非0:进程同步
// value:初始化当前信号量拥有的资源数(>=0),如果资源数为0,线程就会被阻塞
int sem_init(sem *sem, int pshared, 	unsigned int value);

// 无名信号量的资源释放
int sem_destroy(sem_t *sem);

// 当线程调用这个函数,并且sem的资源数>0,线程不会被阻塞,线程会占用sem中的一个资源,因此资源数减1,直到sem中的资源数减为0,资源被耗尽,则线程会被阻塞。
int sem_wait(sem *sem);

// 当线程调用这个函数,并且sem中的资源数 >0,线程不会阻塞,线程会占用sem中的一个资源,因此资源数减1,直到sem中的资源数减为0时,资源被耗尽,但是线程不会被阻塞,直接返回错误号,因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况
int sem_trywait(sem_t *sem);
    
// 当线程调用这个函数,并且sem的资源数>0,线程不会被阻塞,线程会占用sem中的一个资源,因此资源数减1,直到sem中的资源数减为0,资源被耗尽,线程会被阻塞,但是阻塞是有时长的,当阻塞指定的时长之后,线程解除阻塞。
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);


// 调用该函数给sem中的资源数加1
// 如果有线程调用sem_wait、sem_trywait、sem_timedwait时因为sem中的资源数为0时被阻塞了,这时这些线程会解除阻塞,获取到资源之后继续向下运行
int sem_post(sem_t *sem);

// 查看sem中现在拥有的资源个数,并将这个值传给sval
int sem_getvalue(sem_t *sem, int *sval);

Posix信号量是计数信号量,它提供以下三种基本操作:

(1)创建(create)一个信号量。这还要求调用者指定初始值。

(2)等待(wait)一个信号量(也叫P操作)。该操作会测试这个信号量的值,如果其值小于或等于0,那就等待(阻塞),一旦其值大于0,就将它的值减1。这个过程可以用下面的伪代码表示:

while(semaphore_value <=0)
{
	// wait
	;
}
semaphore_value--;

(3)挂出(post)一个信号量(也称为V操作)。该操作信号量的值加1,并唤醒等待该信号量的任意线程。

二值信号量可以用于互斥,就像互斥锁一样:

/* 互斥锁 */
初始化互斥锁;
pthread_mutex_lock(&mutex);
临界区
pthread_mutex_unlock(&mutex);


/* 二值信号量 */
/* 我们把信号量初始化为1,sem_wait调用等待其值变为大于0,然后将它减1,sem_post调用则将其值加1(从0变成1),然后唤醒阻塞在sem_wait调用中等待该信号量的任何线程。
*/
初始化信号量为1sem_wait(&sem);
临界区
sem_post(&sem);

信号量、互斥变量、条件变量的差异:

1、互斥锁必须总是由锁住它的线程解锁,但号量的挂出却不必由执行过它的等待操作的同一线程执行

2、互斥锁要么被锁住,要么被解开(二值状态,类似于二值信号量)

3、因为信号量有一个与之关联的状态(它的计数值),因此信号量挂出操作总是被记住。而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号将丢失。

有名(named)信号量和无名信号量(或者称基于内存的信号量)

有名信号量和基于内存的信号量使用的函数如下:
在这里插入图片描述

有名信号量总是能够在不同进程间共享,基于内存的信号量则必须在创建时指定是否在进程间共享。这两类信号量的持续性也有差别:有名信号量至少有随内核的持续性,基于内存的信号量则具有随进程的持续性。

有名信号量

#include <semaphore.h>

// 创建或打开一个命名信号量
sem_t *sem_open(const char *name, int oflag, .../* mode_t mode, unsigned int value*/);

参数:

  • name:信号量的名称。通常以斜杠/开头,如果以斜杠开头,则不能再含有任何其他斜杠符
  • oflag:用于控制函数的行为。可以是0、O_CREAT、O_CREAT|O_EXCEL。如果指定了O_CREAT标志,则必须要有第三个mode和第四个value参数。
    • O_CREAT:如果信号量不存在,则创建它。
    • O_EXCL:与O_CREAT一起使用时,如果信号量已存在,则sem_open会失败。
  • mode:指定新创建的信号量的权限。
  • value:信号量的初始值。该值必须是非负的。

返回值:

成功时,返回一个指向sem_t类型的指针。失败时,返回SEM_FAILED,并设置errno。

#include <semaphore.h>

// 关闭一个有名的信号量
// 返回值:成功,返回0;出错,返回-1
int sem_close(sem_t *sem);

关闭一个有名信号量并没有将它从系统中删除,因此Posix有名信号量至少是随内核持续的:即使当前没有进程打开着某个信号量,它的值仍然保持。

有名信号量使用sem_unlink从系统中删除:

#include <semaphore.h>

// 删除一个有名的信号量
// 返回值:成功,返回0;出错,返回-1
int sem_unlink(const char *name);

生产者-消费者问题

和之前的生产者-消费者问题相比,本例还对此有一个扩展:把共享缓冲区用作一个环绕缓冲区,生产者填写最后一项后,可以回过头来填写第一项,消费者也同样可以这么做。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <semaphore.h>
#include <pthread.h>

#define NBUFF 10
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

// 以斜杠开头,而且不能再含有任何其他的斜杠(我试了下我的环境,会造成异常退出)
#define SEM_MUTEX "/mutex"
#define SEM_NEMPTY "/nempty"
#define SEM_NSTORED "/nstored"

// 可存放NBUFF个条目的缓冲区一级三个信号量指针是生产者线程和消费者线程共享的全局变量
int nitems;
struct {
        int buff[NBUFF];
        sem_t *mutex, *nempty, *nstored;
}shared;

void *produce(void *), *consume(void *);

int main(int argc, char **argv)
{
        pthread_t tid_produce, tid_consume;
        if (argc != 2)
        {
                printf("param error\n");
                return -1;
        }
        nitems = atoi(argv[1]);
        sem_unlink(SEM_MUTEX);
        sem_unlink(SEM_NEMPTY);
        sem_unlink(SEM_NSTORED);
        // 创建三个信号量
        shared.mutex = sem_open(SEM_MUTEX, O_CREAT | O_EXCL, FILE_MODE, 1);
        shared.nempty = sem_open(SEM_NEMPTY, O_CREAT | O_EXCL, FILE_MODE, NBUFF);// nempty初始化为NBUFF,即有NBUFF个空槽
        shared.nstored = sem_open(SEM_NSTORED, O_CREAT | O_EXCL, FILE_MODE, 0); // nstored初始化为0,即缓冲区已填写的槽位数为0

        // 创建生产者和消费者线程
        pthread_create(&tid_produce, NULL, produce, NULL);
        pthread_create(&tid_consume, NULL, consume, NULL);

        // 主线程等待这两个线程的终止,接着删除一开始创建的三个信号量
        pthread_join(tid_produce, NULL);
        pthread_join(tid_consume, NULL);

        sem_unlink(SEM_MUTEX);
        sem_unlink(SEM_NEMPTY);
        sem_unlink(SEM_NSTORED);
        return 0;

}

void *produce(void *arg)
{
        int i;
        for (i = 0; i < nitems; i++)
        {
                // 生产者在nempty信号量上调用sem_wait,一直等待,直到缓冲区有可用空间,则解除阻塞。
                // 首次执行时,缓冲区的可用空间从NBUFF变成NBUFF-1
                sem_wait(shared.nempty);
                // 在往缓冲区存放新条目之前,生产者必须先获取mutex信号量
                sem_wait(shared.mutex);
                shared.buff[i % NBUFF] = i;
                // 往缓冲区存放当前条目后,释放mutex信号量
                sem_post(shared.mutex);
                // 挂出nstored信号量,告诉消费者缓冲区有条目待处理。第一次执行时,nstored的值从初始值0变成1
                sem_post(shared.nstored);
        }
        return NULL;
}

void *consume(void *arg)
{
        int i;
        for (i = 0; i < nitems; i++)
        {
                //首先判断nstored是否大于0,如果大于0,说明缓冲区中已经有条目待处理
                sem_wait(shared.nstored);
                // 从缓冲区取出一个信号量前,先获取mutex信号量
                sem_wait(shared.mutex);
                if (shared.buff[i % NBUFF] != i)
                {
                        printf("buff[%d] = %d\n", i, shared.buff[i % NBUFF]);
                }
                // 取走缓冲区数据后,释放mutex信号量
                sem_post(shared.mutex);
                // 取走之后,nempty的数量又多了一个,告诉生产者又有一个空槽位可用了
                sem_post(shared.nempty);
        }
        return NULL;
}

无名信号量

Posix也提供基于内存的信号量,它们由应用程序分配信号量的内存空间,然后由系统初始化它们的值。

// 初始化无名信号量
// sem:信号量变量地址
// pshared:0:线程同步;非0:进程同步
// value:初始化当前信号量拥有的资源数(>=0),如果资源数为0,线程就会被阻塞
int sem_init(sem *sem, int pshared, unsigned int value);

// 无名信号量的资源释放
int sem_destroy(sem_t *sem);

// 上面两个函数的返回值都是:成功,返回0;失败,返回-1,并设置errno

无名信号量的持续性:

  • 如果某个基于内存的信号量是由单个进程内的各个线程共享的,那么该信号量具有随进程的持续性,当该进程终止时它也消失。
  • 如果某个基于内存的信号量是在不同进程间共享的,则该信号量必须存放在共享内存区中,因而只要该共享内存区仍然存在,该信号量也就继续存在。

基于内存的信号量的生产者-消费者问题的代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <semaphore.h>
#include <pthread.h>

#define NBUFF 10
#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)


// 可存放NBUFF个条目的缓冲区一级三个信号量指针是生产者线程和消费者线程共享的全局变量
int nitems;
struct {
        int buff[NBUFF];
        sem_t mutex, nempty, nstored;
}shared;

void *produce(void *), *consume(void *);

int main(int argc, char **argv)
{
        pthread_t tid_produce, tid_consume;
        if (argc != 2)
        {
                printf("param error\n");
                return -1;
        }
        nitems = atoi(argv[1]);

        // 初始化基于内存的信号量
        sem_init(&shared.mutex, 0, 1);
        sem_init(&shared.nempty, 0, NBUFF);
        sem_init(&shared.nstored, 0, 0);

        // 创建生产者和消费者线程
        pthread_create(&tid_produce, NULL, produce, NULL);
        pthread_create(&tid_consume, NULL, consume, NULL);

        // 主线程等待这两个线程的终止,接着删除一开始创建的三个信号量
        pthread_join(tid_produce, NULL);
        pthread_join(tid_consume, NULL);

        sem_destroy(&shared.mutex);
        sem_destroy(&shared.nempty);
        sem_destroy(&shared.nstored);
        return 0;

}

void *produce(void *arg)
{
        int i;
        for (i = 0; i < nitems; i++)
        {
                // 生产者在nempty信号量上调用sem_wait,一直等待,直到缓冲区有可用空间,则解除阻塞。
                // 首次执行时,缓冲区的可用空间从NBUFF变成NBUFF-1
                sem_wait(&shared.nempty);
                // 在往缓冲区存放新条目之前,生产者必须先获取mutex信号量
                sem_wait(&shared.mutex);
                shared.buff[i % NBUFF] = i;
                // 往缓冲区存放当前条目后,释放mutex信号量
                sem_post(&shared.mutex);
                // 挂出nstored信号量,告诉消费者缓冲区有条目待处理。第一次执行时,nstored的值从初始值0变成1
                sem_post(&shared.nstored);
        }
        return NULL;
}

void *consume(void *arg)
{
        int i;
        for (i = 0; i < nitems; i++)
        {
                //首先判断nstored是否大于0,如果大于0,说明缓冲区中已经有条目待处理
                sem_wait(&shared.nstored);
                // 从缓冲区取出一个信号量前,先获取mutex信号量
                sem_wait(&shared.mutex);
                if (shared.buff[i % NBUFF] != i)
                {
                        printf("buff[%d] = %d\n", i, shared.buff[i % NBUFF]);
                }
                // 取走缓冲区数据后,释放mutex信号量
                sem_post(&shared.mutex);
                // 取走之后,nempty的数量又多了一个,告诉生产者又有一个空槽位可用了
                sem_post(&shared.nempty);
        }
        return NULL;
}

"producer_consumer5.c" 91L, 2482B written                                                                 
zld@zld:~/Codes/thread-test$ 
zld@zld:~/Codes/thread-test$ 
zld@zld:~/Codes/thread-test$ gcc -lpthread producer_consumer5.c -o app1
zld@zld:~/Codes/thread-test$ ./app1 66
zld@zld:~/Codes/thread-test$ 
zld@zld:~/Codes/thread-test$ 
zld@zld:~/Codes/thread-test$ 
zld@zld:~/Codes/thread-test$ 
zld@zld:~/Codes/thread-test$ 
zld@zld:~/Codes/thread-test$ 
zld@zld:~/Codes/thread-test$ vim producer_consumer5.c 
                // 在往缓冲区存放新条目之前,生产者必须先获取mutex信号量
                sem_wait(&shared.mutex);
                shared.buff[i % NBUFF] = i;
                // 往缓冲区存放当前条目后,释放mutex信号量
                sem_post(&shared.mutex);
                // 挂出nstored信号量,告诉消费者缓冲区有条目待处理。第一次执行时,nstored的值从初始值0变成1
                sem_post(&shared.nstored);
        }
        return NULL;
}

void *consume(void *arg)
{
        int i;
        for (i = 0; i < nitems; i++)
        {
                //首先判断nstored是否大于0,如果大于0,说明缓冲区中已经有条目待处理
                sem_wait(&shared.nstored);
                // 从缓冲区取出一个信号量前,先获取mutex信号量
                sem_wait(&shared.mutex);
                if (shared.buff[i % NBUFF] != i)
                {
                        printf("buff[%d] = %d\n", i, shared.buff[i % NBUFF]);
                }
                // 取走缓冲区数据后,释放mutex信号量
                sem_post(&shared.mutex);
                // 取走之后,nempty的数量又多了一个,告诉生产者又有一个空槽位可用了
                sem_post(&shared.nempty);
        }
        return NULL;
}

本文参考自:《UNIX网络编程,卷2:进程间通信》

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值