Detailed explanation of the implementation of producers and consumers under Linux

What is the producer consumer problem?

background

Suppose there is a finite buffer and two threads in the process: the producer and the consumer, which keep putting products into and taking products from the buffer respectively, and a producer must wait when the buffer is full , a consumer must also wait while the buffer is empty. In addition, because the buffer is a critical resource, there must be mutual exclusion between the producer and the consumer.

the heart of the matter

1. Ensure that producers are not allowed to write data to the buffer when the cache is still full.
2. Consumers are not allowed to read data from buffers without data.

It can be seen that producers and consumers have mutually exclusive access to the buffer, and at the same time, producers and consumers are in a cooperative relationship. Only after the producer produces, the consumer can consume, and they are also in a synchronous relationship.

Solutions

For the producer, let him stop for a while if the buffer is full, but be careful not to let him fall asleep. After the consumer takes the data from the buffer, the producer gets up to work and lets him produce the product again. If the consumer finds that the buffer is empty and there is nothing to consume, he will stop and take a rest for a while, and then stand up and work again when he sees that the producer has produced something. But one thing to note is that if the working relationship between the two is not properly handled, then it may happen that both the producer and the consumer are resting and waiting for each other to produce/consume products, which will cause a "deadlock" The phenomenon.

single producer and single consumer

There are only two threads, the producer and the consumer, and it happens that there is a relationship of synchronization and mutual exclusion between these two threads, so what needs to be resolved is the position of mutual exclusion and synchronization of PV operations. Using "inter-thread communication", the problem of waking up can be solved by using signals.

Use of Synchronization Signals

Pseudocode (logic)

pthread_mutex_t mutex = 1;//临界区的互斥信号量
sem_t full = 0;//缓冲区初始化为空
sem_t empty = N;//空闲缓冲区
/*生产者线程*/
void *producer()
{
    
    
	for(;;)
	{
    
    
		produce an product in nextput;//生产者生产数据
		P(empty);//获取空缓冲区单元
		P(mutex);//进入临界区
		add nextput to buffer;//将数据存放到缓冲区里面
		V(mutex);//离开临界区,释放互斥信号量
		V(full);//满缓冲区数加1
	}
}
void *consumer()
{
    
    
	for(;;)
	{
    
    
		P(full);//获取满缓冲区单元
		P(mutex);//进入临界区
		remove an product from buffer;//从缓冲区中取出数据
		V(mutex);//离开临界区,释放互斥信号
		V(empty);//空缓冲区数加1
		consume the product;//消费数据
	}
}
/*需要注意的是:要注意对缓冲区大小为N的处理,当缓冲区中有空时便可对empty变量执行P操作,
一旦取走一个产品便要执行V操作以释放空闲区。对empty和full变量的P操作必须放在对mutex的P操作之前。*/

Interpretation of the above pseudocode

  1. Here I use two signals: full and empty. The mutual exclusion semaphore is mutex, which is used to control mutual exclusion access to the buffer, and the initial value of the mutual exclusion semaphore is 1; the semaphore full is suitable for recording the number of full buffers in the current buffer, and the initial value is 0; the semaphore empty is used to record the number of "empty" buffers in the current buffer, and the initial value is N. After new data is added to the buffer, full is increasing and empty is decreasing. If the producer tries to decrement its value while empty is 0, then the producer will be told to rest. If there is data consumed in the next round, the empty will increase, and the producer will be called to work.
  2. The P operation for empty and full variables must be placed before the P operation for mutex, because if the producer process has filled the buffer, the consumer thread does not take the product, that is, empty=0, when it is still the producer next time When the thread is working, it first executes P(mutex) to block the semaphore, and then it will be blocked when executing P(empty). It is hoped that the consumer will wake it up after taking out the product. When it is the turn of the consumer thread to work, he executes P(mutex) first. However, since the producer thread has blocked the mutex semaphore, the consumer thread will not be able to work and will be blocked. In this way, the producer and consumer The other thread is blocked, and they all hope that the other party will wake them up, so the incarnation of Wangfushi falls into endless waiting.
  3. If the consumer thread has already emptied the buffer, ie full=0, if the consumer runs first next time, a similar deadlock will occur. However, it should be noted that when the producer releases the semaphore, it doesn't matter which one is released first, mutex or full, and it doesn't matter whether the consumer releases mutex or empty first.

Multiple Producers and Multiple Consumers

The difference between single producer and single consumer

Different from single producer and single consumer, when multiple producers and multiple consumers appear at the same time, it will cause you to squeeze me and I squeeze you, which will cause two or more threads to A channel writes or reads data. If a thread has multiple producers executing concurrently, the following situations will occur:

  1. Both producers decrement empty (sem_wait(&empty)).
  2. A producer is judging whether there are channels available in the cache.
  3. The second producer, like the first producer, is judging whether there is an available channel in the cache.
  4. Two producers write data to the same channel at the same time.

Understanding multi-producers and multi-consumers

Multiple producers store data into a buffer, and multiple consumers read data from the buffer. This is a bounded buffer problem, queue rewriting, mutual exclusion between producers and producers, between consumers and consumers, and between producers and consumers. The shared buffer acts as a ring buffer, and starts from the beginning when the data is stored to the end.

Solution

  1. We can use a mutex to protect the producer from storing data into the buffer, but since there are multiple producers, we need to remember where the current producer is storing into the buffer.
  2. We can use another mutex to protect the number of messages in the buffer, and this number of data produced acts as a semaphore for communication between the producer and the consumer.
  3. We can also use a condition variable to wake up the consumer. Also, because there are multiple consumers, consumers also need to remember where to fetch data each time.

Part of the code display

//function:producer thread
void *producer(void *param)
{
    
    
#if MODE == SINGLEROLE
    static int t = 0;
    buffer_item item;
    while (TRUE)
    {
    
    

        // t = rand () % MAX_INTERVAL + 1;
        sleep(1); // sleep for a random period of time
        // item = rand() % MAX_NUM + 1; // produce an item
        item = str[i];
        if (insert_item(item))
            printf("Insert item %c failed\n", item);
        else
            printf("Producer produced %c\n", item);
        i++;
        t++;
        if (t == 31)
            pthread_exit(0);
    }
// exit(0);
#elif MODE == MANYROLE
    //多个生产者
    for (;;)
    {
    
    
        sleep(1);
        pthread_mutex_lock(&put.mutex);
        //检测是否已满
        if (put.nval >= NUMOFNAME)
        {
    
    
            pthread_mutex_unlock(&put.mutex);
            return NULL;
        }
        buffer[in] = str[put.nval];
        printf("Buffer[%d]: ", in);
        in = (in + 1) % BUFFER_SIZE;
        if (str[put.nval] == '2')
            idflag++;
        if (idflag >= 2)
        {
    
    
            idbuffer[idline] = str[put.nval];
            idline++;
        }
        printf("Producer produced %c\n", str[put.nval]);
        //判断是否数组下标溢出
        if (++put.nput >= BUFFER_SIZE)
        {
    
    
            put.nput = 0;
        }
        ++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);
    }
    return NULL;
#endif
}
// function: consumer thread
void *consumer(void *param)
{
    
    
#if MODE == SINGLEROLE
    static int t = 0;
    buffer_item item;

    while (TRUE)
    {
    
    
        // t = rand () % MAX_INTERVAL + 1;
        sleep(1); // sleep for a random period of time
        // if (item == '\n')
        //     pthread_exit(0);
        //  break;
        if (remove_item(&item))
            printf("Remove item failed\n");
        else
            printf("Consumer consumed %c\n", item);
        t++;
        if (t == 31)
        {
    
    
            pthread_exit(0);
        }
    }
// exit(0);
#elif MODE == MANYROLE
    //多个消费者
    for (;;)
    {
    
    
        sleep(1);
        pthread_mutex_lock(&nready.mutex);
        // 确保线程唤醒
        while (nready.nready == 0)
        {
    
    
            pthread_cond_wait(&nready.cond, &nready.mutex);
        }
        if (++nready.nget >= NUMOFNAME)
        {
    
    
            if (nready.nget == NUMOFNAME)
            {
    
    
                printf("Buffer[%d]: Consumer consumed %c\n", nready.nget - 1, buffer[(nready.nget - 1) % BUFFER_SIZE]);
            }
            pthread_cond_signal(&nready.cond);
            pthread_mutex_unlock(&nready.mutex);
            return NULL;
        }
        --nready.nready;
        pthread_mutex_unlock(&nready.mutex);
        if (str[nready.nget] == 'j')
            nameflag++;
        if (nameflag)
        {
    
    
            Consume_Buffer[nameline] = str[nready.nget];
            nameline++;
            if (str[nready.nget] == 'n')
                nameflag++;
            if (nameflag == 3)
                nameflag = 0;
        }
        printf("Buffer[%d]: Consumer consumed %c\n", nready.nget - 1, buffer[(nready.nget - 1) % BUFFER_SIZE]);
    }
    return NULL;
#endif
}

Multi-threaded use of semaphores

The main function of the semaphore

int sem_init(sem_t *sem,int pshared,unsigned int value);
int sem_wait(sem_t *sem);
int sem_post(sem_t sem);
  • sem_init() : Used to initialize the specified signal, pshared is 0, indicating that the signal is shared among multiple threads of the current process, and value indicates the value of the initialized signal.
  • sem_wait() : It can be used to block the current thread until the value of the semaphore is greater than 0, unblocking. After unblocking, the value of sem is -1, indicating that the execution of public resources has been reduced. For example: If you call sem_wait() on a semaphore with a value of 2, the thread will continue executing and the value of the semaphore will be -1. When value=0 is initialized, using sem_wait() will block this thread, and this thread function will wait for other thread functions to call sem_post() to increase this value so that it is no longer 0 before starting to execute, and then the value value is reduced by 1 .
  • sem_post() : Used to increase the value of the semaphore +1. When a thread is blocked on this semaphore, calling this function will cause one of the threads to not be blocked. The selection mechanism is determined by the thread scheduling strategy.

thread main function

int pthread_create(pthread_t *thread,const pthread_attr_t *attr,void *(*start_routine)(void*),void *arg);
void pthread_exit(void *retval);
int pthread_join(pthread_t thread,void **thread_result);
int pthread_cancel(pthread_t thread)

Generally speaking, we can use the pthread_create() interface to create a thread, and at the same time use the pthread_join() interface to block the execution of the main process until the merged thread execution ends. If the thread is running normally then the return value of pthread_join is 0. During the execution of the thread, if we want to actively exit the execution of the thread, then we can use the pthread_exit() interface function to exit.

Supplement: thread merging and separation

Merging of threads: pthread_join()

Thread merging is a scheme to actively reclaim thread resources. When a process or thread calls the pthread_join() interface for other threads, it is thread merging. This interface blocks the calling process or thread until the merged thread terminates . When the merged thread ends, the pthread_join() interface will reclaim the resources of this thread, and return the return value of this thread to the merger.

Thread separation: pthread_detach()

Thread separation is to hand over the recycling of thread resources to the system to complete automatically , that is to say, when the separated thread ends, the system will automatically reclaim its resources. Because thread detachment is the automatic recovery mechanism of the startup system , the program cannot obtain the return value of the detached thread, which makes the pthread_detach() interface only need to have one parameter, which is the detached thread handle.

mutex

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);//互斥锁解锁

Only one thread can hold a certain mutex at the same time, and the thread that owns the mutex can operate on shared resources . If a thread locks an already locked mutex , the thread will sleep until other threads release the mutex . It can be said that this mutex guarantees that each thread performs atomic operations on shared resources in order.

thread properties

int pthread_attr_init(pthread_attr_t **attr);
int pthread_attr_destory(pthread_attr_t *attr);

Thread attribute objects are initialized by the pthread_attr_init() interface and destroyed by pthread_attr_destory() .

thread binding properties

int pthread_attr_setscope(pthread_attr_t *attr, int scope);

It has two parameters, the first is the pointer of the thread attribute object, and the second is the binding type, which has two values: PTHREAD_SCOPE_SYSTEM (bound) and PTHREAD_SCOPE_PROCESS (unbound) .

设置线程绑定属性
#include <stdio.h>
#include <pthread.h>
……
int main( int argc, char *argv[] )
{
pthread_attr_t attr;
pthread_t th;
……
pthread_attr_init( &attr );
pthread_attr_setscope( &attr, PTHREAD_SCOPE_SYSTEM );
pthread_create( &th, &attr, thread, NULL );
……
}

thread separation property

pthread_attr_setdetachstat(pthread_attr_t *attr, int detachstate); 

Its second parameter has two values: PTHREAD_CREATE_DETACHED (detached) and PTHREAD_CREATE_JOINABLE (mergeable, also the default attribute) .

设置线程分离属性
#include <stdio.h>
#include <pthread.h>
……
int main( int argc, char *argv[] )
{
pthread_attr_t attr;
pthread_t th;
……
pthread_attr_init( &attr );
pthread_attr_setscope( &attr, PTHREAD_SCOPE_SYSTEM );
pthread_create( &th, &attr, thread, NULL );
……
}

Scheduling properties

There are three thread scheduling attributes provided by Linux: algorithm , priority and inheritance

//线程调度算法的接口
pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
/*它的第二个参数有三个取值:SCHED_RR(轮询)、SCHED_FIFO(先进先出)和SCHED_OTHER(其它)。*/

//设置线程优先级的接口
struct sched_param {
    
      
    int sched_priority;  
}  
int pthread_attr_setschedparam(pthread_attr_t *attr, struct sched_param *param); 
//设置线程的继承权
int pthread_attr_setinheritsched(pthread_attr_t *attr, int inheritsched);
/*它的第二个参数有两个取值:PTHREAD_INHERIT_SCHED(拥有继承权)和PTHREAD_EXPLICIT_SCHED(放弃继承权)。
新线程在默认情况下是拥有继承权。*/

Use of shared memory and message queues

Commonly used interface functions

shmget() : Create shared memory
shmat() : Map the created shared memory to a specific process space.
shmdt() : Undo mapping.
insert image description here
msgget() : create or open a message queue (the number of message queues will be limited by the number of system message queues)
msgsnd() : add a message to use (add to the end of the message queue)
msgrcv() : read a message (put the message from the message queue take away)

Message queue and shared memory code display

/*
* set_shm 函数建立一个具有n 个字节的共享内存区
* 如果建立成功,返回一个指向该内存区首地址的指针shm_buf
* 输入参数:
* shm_key 共享内存的键值
* shm_val 共享内存字节的长度
* shm_flag 共享内存的存取权限
*/
/*实现过程:创建共享内存->映射到进程的指针并返回*/
char * set_shm(key_t shm_key,int shm_num,int shm_flg)
{
    
    
    int i,shm_id;
    char * shm_buf;
    //测试由shm_key 标识的共享内存区是否已经建立
    if((shm_id = get_ipc_id("/proc/sysvipc/shm",shm_key)) < 0 )
    {
    
    
    //shmget 新建一个长度为shm_num 字节的共享内存,其标号返回到shm_id
        if((shm_id = shmget(shm_key,shm_num,shm_flg)) <0)
        {
    
    
            perror("shareMemory set error");
            exit(EXIT_FAILURE);
        }
    //shmat 将由shm_id 标识的共享内存附加给指针shm_buf
        if((shm_buf = (char *)shmat(shm_id,0,0)) < (char *)0)
        {
    
    
            perror("get shareMemory error");
            exit(EXIT_FAILURE);
        }
        /*共享内存区初始化*/
        for(i=0; i<shm_num; i++)
        shm_buf[i] = 0; //初始为0
    }
    //shm_key 标识的共享内存区已经建立,将由shm_id 标识的共享内存附加给指针shm_buf
    if((shm_buf = (char *)shmat(shm_id,0,0)) < (char *)0)
    {
    
    
        perror("get shareMemory error");
        exit(EXIT_FAILURE);
    }
    return shm_buf;
}
/*
* set_msq 函数建立一个消息队列
* 如果建立成功,返回一个消息队列的标识符msq_id
* 输入参数:
* msq_key 消息队列的键值
* msq_flag 消息队列的存取权限
*/
/*实现过程:创建消息队列->返回ID*/
int set_msq(key_t msq_key,int msq_flg)
{
    
    
    int msq_id;
    //测试由msq_key 标识的消息队列是否已经建立
    if((msq_id = get_ipc_id("/proc/sysvipc/msg",msq_key)) < 0 )
    {
    
    
        //msgget 新建一个消息队列,其标号返回到msq_id
        if((msq_id = msgget(msq_key,msq_flg)) < 0)
        {
    
    
            perror("messageQueue set error");
            exit(EXIT_FAILURE);
        }
    }
    return msq_id;
}

Guess you like

Origin blog.csdn.net/dbqwcl/article/details/124986692