Unix网络编程-同步

1、互斥锁(量)和条件变量

默认情况下互斥锁和条件变量用于线程间同步,若将它们放在共享内存区,也能用于进程间同步。

1.1 互斥锁

1、概述:

互斥锁(Mutex,也称互斥量),防止多个线程对一个公共资源做读写操作的机制,以保证共享数据的完整性。

用以保护临界区,以保证任何时候只有一个线程(或进程)在访问共享资源(如代码段)。保护临界区的代码形式:

lock_the_mutex(...);
临界区
unlock_the_mutex(...);

任何时刻只有一个线程能够锁住一个给定的互斥锁。

下面的三个函数给一个互斥锁进行上锁和解锁:

#include<pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mptr);
int pthread_mutex_trylock(pthread_mutex_t *mptr);
int pthread_mutex_unlock(pthread_mutex_t *mptr);

以上三个函数,如果调用成功均返回0,失败返回相应的error值。

如果尝试给一个已由某个线程锁住的互斥锁上锁,那么pthread_mutex_lock将阻塞到该互斥锁解锁为止。pthread_mutex_trylock是对应的非阻塞函数,如果该互斥锁已锁住,它就返回一个EBUSY错误。

如果有多个线程阻塞在等待同一个互斥锁上,那么当该互斥锁解锁时,哪一个线程会开始运行:不同线程被赋予不同优先级,同步函数将唤醒优先级最高的被阻塞线程。

2、互斥锁实现的生产者-消费者模型:

生产者-消费者问题也称有界缓冲区问题,若干个生产者和若干个消费者共享使用固定数目的缓冲区,因而带来的同步和通信问题

各种IPC手段本身就是一个生产者-消费者问题的实例。

管道、FIFO和消息队列的同步是隐式同步,使用者只能通过指定的接口来使用这些IPC方式,其中的同步都由内核完成。

共享内存作为IPC,需要使用者进行显式同步,线程间共享全局数据也需要显式同步。

以多生产者,单消费者的模型为例:

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

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

#define MAXNITEMS 1000000
#define MAXNTHREADS 100
int nitems; //(1)生产者存放的条目数,只读(对于生产者或消费者)!!!
/*
*shared结构中的变量是共享数据
*/
struct {//(2)!!!
    pthread_mutex_t mutex;/*同步变量:互斥锁*/
    int buff[MAXNITEMS];//生产者会依次给buff数组存放数据
    int nput;/*nput是buff数组中下一次存放的元素下标*/
    int nval;/*nval是下一次存放的值(0,1,2等)*/
} shared = {
    PTHREAD_MUTEX_INITIALIZER //(3)对用于生产者线程间同步的互斥锁做初始化!!!
};
void *produce(void *), *consume(void *);

int main(int argc,char *argv[])
{
    /*
    *变量说明:tid_produce[]数组中保存每个线程的线程ID
    *count[]是每个线程计数器
    *tid_consume中保存单个的消费者的ID
    */
    int i, nthreads, count[MAXNTHREADS];
    pthread_t tid_produce[MAXNTHREADS], tid_consume;
    /*命令行参数个数判断*/
    if (argc != 3) {
        printf("usage: producer_consumer1 <#iterms> <#threads>\n");
        exit(1);
    }
    /*
    * argv[1]中指定生产者存放的条目数
    * argv[2]中指定待创建的生产者线程的数目
    */
    nitems = min(atoi(argv[1]), MAXNITEMS);//(4)指定生产者存放的条目数!!!
    nthreads = min(atoi(argv[2]), MAXNTHREADS);//(5)创建多少个生产者线程!!!
    /*
    *set_concurrency函数用来告诉线程系统我们希望并发运行多少线程
    *即设置并发级别
    */
    set_concurrency(nthreads);//(6)!!!

    //(7)创建生产者线程:每个线程执行produce!!!
    for (i = 0; i < nthreads; i++) {//依次将buff[i]设置为i
        count[i] = 0;//计数器初始化为0,每个线程每次往缓冲区存放一个条目时给这个计数器加1.
        pthread_create(&tid_produce[i], NULL, produce, &count[i]);
    }

    //(8)等待所有生产者线程终止,并输出每个线程的计数器值!!!
    for (i = 0; i < nthreads; i++) {
        pthread_join(tid_produce[i], NULL);
        printf("count[%d] = %d\n", i, count[i]);
    }
    //(9)然后启动单个消费者线程!!!
    pthread_create(&tid_consume, NULL, consume, NULL);
    //(10)接着等待消费者完成,然后终止进程!!!
    pthread_join(tid_consume, NULL);
    return 0;
}

//创建生产者线程
void *produce(void *arg)
{
    for ( ; ; ) {
        pthread_mutex_lock(&shared.mutex);//(1)上锁!!!
        //(2)临界区
        if (shared.nput >= nitems) {
            //说明此时已经生产完毕,解锁
            pthread_mutex_unlock(&shared.mutex);
            return (NULL);
        }
        shared.buff[shared.nput] = shared.nval;
        shared.nput++;
        shared.nval++;
        pthread_mutex_unlock(&shared.mutex);//(3)解锁!!!
        //count元素的增加(通过指针arg)不属于临界区,因为每个线程有各自的计数器
        *((int *) arg) += 1;
    }
}

//等待生产者线程,然后启动消费者线程
void *consume(void *arg)
{
    int i;
    /*
    *消费者只是验证buff中的条目是否正确,如果发现错误则输出一条信息
    *这个函数是只有一个实例在运行,而且是在所有的生产者线程都完成之后
    *因此不需要任何同步
    */
    for (i = 0; i < nitems; i++)
        if (shared.buff[i] != i)
            printf("buff[%d] = %d\n", i, shared.buff[i]);
    return (NULL);
}

3、互斥锁的非正常终止:

若进程在持有互斥锁时终止,内核不会负责自动释放持有的锁。内核自动清理的唯一同步锁类型是fcntl记录锁。

若被锁住的互斥锁的持有进程或线程终止,会造成这个互斥锁无法解锁,因而死锁。线程可以安装线程清理程序,用来在被取消时能释放持有的锁。但这种释放可能会导致共享对象的状态被部分更新,造成不一致。

1.2 条件变量

互斥锁只能用于上锁,实现对某个共享对象的互斥访问,无法用于对某事件的等待。条件变量则用于等待。

#include<pthread.h>
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
int int pthread_cond_signal(pthread_cond_t *cond);

每个条件变量都需要关联一个互斥锁,用来提供对等待条件的互斥访问。

2、读写锁

读写锁可以在读数据与修改数据之间作区分。其规则如下:

1)没有线程持有写锁时,任意多的线程可以持有读锁。

2)仅当没有线程持有读锁或写锁时,才能分配写锁。

简言之,只要没有线程在写,那么所有线程都可以读;但是有线程要想写,必须是既没有线程在读,也没有线程在写。!!!

当已有线程持有读锁时,另一线程申请写锁则会阻塞,若后续还有读锁的申请,此时有两种策略:

1)对后续的读锁请求都通过,可能会造成因读锁不断被分配,写锁申请始终阻塞,“饿死”了写进程。

2)后续读锁请求都阻塞,等当前持有的读锁都结束后优先分配写锁。

与普通互斥锁相比,当被保护数据的读访问比写访问更为频繁时,读写锁能提供更高的并发度。

3、记录上锁

记录上锁是读写锁的一种扩展类型,它可用于有亲缘关系或无亲缘关系的进城之间共享某个文件的读与写。

执行上锁的函数是fcntl,锁由内核维护,其属主由进程ID标识

特点:只用于不同进程间的上锁,而不是同一进程内不同线程间的上锁。

Unix内核没有记录这一概念,对记录的解释是由读写文件的应用进行的。每个记录就是文件中的一个字节范围。

使用fcntl记录上锁时,等待着的读出者优先还是等待着的写入者优先没有保证。

4、信号量

信号量是一种用于不同进程间,或一个给定进程内不同线程间同步手段的原语。

3中信号量类型:

1)Posix有名信号量:使用Posix IPC名字标识,可用于进程或线程间的同步。(可用于彼此无亲缘关系的进程间)

2)Posix基于内存的信号量(无名信号量):存放在共享内存区,可用于进程或线程间的同步。(不可用于彼此无亲缘关系的进程间)

3)System V信号量:在内核中维护,可用于进程或线程间的同步。

Posix信号量不必在内核中维护(System V信号量由内核维护),由可能为路径名的名字来标识。

4.1 Posix信号量

1、概述

三种基本操作:

1)创建(create):指定初始值。

2)等待(wait):如果值小于等于0则阻塞,否则将其减一,又称P操作。

3)挂出(post):将信号量的值加1,加后如果值大于0,则唤醒一个阻塞在等待上的线程,又称V操作。

信号量的wait和post与条件变量的wait和signal类似,区别是:因为永久的改变了信号量的值,信号量的操作总被记住(会影响到后续的操作);条件变量的signal如果没有线程在等待,该信号将丢失(对后续操作没有影响)。

互斥锁是为上锁而优化的,条件变量是为等待优化的,信号量既可以上锁也可以等待,因此开销更大。

2、二值信号量

二值信号量,其值为0或1,资源锁住则信号量值为0,若资源可用则信号量值为1。

二值信号量可用于互斥,就像互斥锁一样。但互斥锁必须由锁住它的线程解锁,信号量的挂出却不必由执行过它的等待操作的同一线程执行。

二值信号量用于生产者消费者问题:考虑往某个缓冲区放一个条目的一个生产者,以及取走该条目的一个消费者,这种简化类型。

3、计数信号量

其值在0和某个限制值(32767内)之间,可统计资源数,信号量的值就是可用资源数。等待操作都等待信号量的值变为大于0(表示可用),然后将它减1;挂出操作则只是将信号量值加1(可用资源数增加),唤醒正在等待该信号量值变为大于0的任意线程。

4.2 System V信号量

System V信号量增加了另一级复杂度。

**计数信号量集:一个或多个信号量(构成一个集合),其中每个都是计数信号量。**System V信号量一般指计数信号量集。而Posix信号量一般指单个计数信号量。

5、信号量、互斥锁和条件变量的差异

信号量的意图在于进程间同步,这些进程可能共享也可能不共享内存区;互斥锁和条件变量的意图在于线程间同步,这些线程总是共享内存区;但是信号量也可用于线程间,互斥锁和条件变量也可用于进程间。

1)互斥锁总是由给他上锁的线程解锁,信号量的挂出也可由其他线程执行。

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

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

参考《Unix网络编程:卷2》

时间: 2024-10-10 06:22:24

Unix网络编程-同步的相关文章

UNIX网络编程——网络I/O模型

在学习UNIX网络编程的时候.一開始分不清 同步 和 异步,所以还是总结一下,理清下他们的差别比較好. IO分类 IO依据对IO的调度方式可分为堵塞IO.非堵塞IO.IO复用.信号驱动IO.异步IO. IO操作整个流程分为 可操作推断 和 实际IO操作 两个区间,我们能够称之为两个半程,前半程推断是否可操作,后半程进行实际操作. 当中堵塞IO.非堵塞IO.IO复用.信号驱动IO由于其[实际的IO操作是同步堵塞]的,所以一般把他们归为同步IO,异步IO的实际IO操作是在独立的线程中完毕的,所以称为

UNIX网络编程-send、recv、sendto、recvfrom详解

send.recv和sendto.recvfrom,一般情况下,send.recv在TCP协议下使用,sendto.recvfrom在UDP协议下使用,也可以在TCP协议下使用,不过用的很少. 1.send 这里只描述同步socket的send函数的执行流程. s:套接字  |  buf:存储发送数据  |  len:发送数据长度 当调用该函数时,send先比较待发送数据的长度len和套接字s的发送缓冲的长度,如果len大于s的发送缓冲区的长度,该函数返回SOCKET_ERROR:如果len小于

UNIX网络编程 卷2:进程间通信

这篇是计算机类的优质预售推荐>>>><UNIX网络编程 卷2:进程间通信(第2版)> UNIX和网络专家W. Richard Stevens的传世之作 编辑推荐 两卷本的<UNIX网络编程>是已故著名技术作家W. Richard Stevens的传世之作.卷2着重讨论怎样让应用程序与在其它机器上的应用程序进行对话. 良好的进程间通信(IPC)机制是提高UNIX程序性能的关键. 本书全面深入地解说了各种进程间通信形式,包括消息传递.同步.共享内存及远程过程调用

【unix网络编程第三版】阅读笔记(五):I/O复用:select和poll函数

本博文主要针对UNP一书中的第六章内容来聊聊I/O复用技术以及其在网络编程中的实现 1. I/O复用技术 I/O多路复用是指内核一旦发现进程指定的一个或者多个I/O条件准备就绪,它就通知该进程.I/O复用适用于以下场合: (1) 当客户处理多个描述符(一般是交互式输入或网络套接字),必须适用I/O复用 (2) 当一个客户处理多个套接字时,这种情况很少见,但也可能出现 (3) 当一个TCP服务器既要处理监听套接字,又要处理已连接套接字,一般就要使用I/O复用 (4) 如果一个服务器既要适用TCP,

UNIX网络编程卷1 回射客户程序 TCP客户程序设计范式

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 下面我会介绍同一个使用 TCP 协议的客户端程序的几个不同版本,分别是停等版本.select 加阻塞式 I/O 版本. 非阻塞式 I/O 版本.fork 版本.线程化版本.它们都由同一个 main 函数调用来实现同一个功能,即回射程序客户端. 它从标准输入读入一行文本,写到服务器上,读取服务器对该行的回射,并把回射行写到标准输出上. 其中,非阻塞式 I/O 版本是所有版本中执行速度最快的,

Unix网络编程中的五种I/O模型_转

转自:Unix网络编程中的的五种I/O模型 下面主要是把unp第六章介绍的五种I/O模型. 1. 阻塞I/O模型 例如UDP函数recvfrom的内核到应用层.应用层到内核的调用过程是这样的:首先把描述符.接受数据缓冲地址.大小传递给内核,但是如果此时 该与该套接口相应的缓冲区没有数据,这个时候就recvfrom就会卡(阻塞)在这里,知道数据到来的时候,再把数据拷贝到应用层,也就是传进来的地址空 间,如果没有数据到来,就会使该函数阻塞在那里,这就叫做阻塞I/O模型,如下图: 2. 非阻塞I/O模

unix网络编程环境搭建

unix网络编程环境搭建 新建 模板 小书匠 1.点击下载源代码 可以通过下列官网中的源代码目录下载最新代码: http://www.unpbook.com/src.html 2.解压文件 tar -xzvf upv13e.tar.gz 3.上传至阿里云 本人本地已经配置好,这次实验是将环境搭建至云服务器中. scp -r unpv13e [email protected]120.76.140.119:/root/program/unp // -r 上传文件夹  4.编译文件 cd unpv13

unix网络编程代码(2)

继续贴<unix网络编程>上的示例代码.这次是一个反射程序,反射是客户端讲用户输入的文本发送到服务器端,服务器端读取客户端发过来的文本消息,然后原封不动的把文本消息返回给客户端.使用tcp协议连接客户端和服务端,我已经在我的阿里云服务器上测试过了,能够完美运行. 首先是头文件wrap.h,在该头文件中,声明了封装部分网络编程套接字api的包裹函数,以及某些宏定义. 1 #ifndef WRAP_H_ 2 #define WRAP_H_ 3 4 #include <stdio.h>

【LINUX/UNIX网络编程】之简单多线程服务器(多人群聊系统)

RT,Linux下使用c实现的多线程服务器.这个真是简单的不能再简单的了,有写的不好的地方,还希望大神轻拍.(>﹏<) 本学期Linux.unix网络编程的第四个作业. 先上实验要求: [实验目的] 1.熟练掌握线程的创建与终止方法: 2.熟练掌握线程间通信同步方法: 3.应用套接字函数完成多线程服务器,实现服务器与客户端的信息交互. [实验内容] 通过一个服务器实现最多5个客户之间的信息群发. 服务器显示客户的登录与退出: 客户连接后首先发送客户名称,之后发送群聊信息: 客户输入bye代表退