In-depth analysis of workflow thread pool

In-depth analysis of workflow thread pool

Thread pool is a tool commonly used in daily development to manage threads. It is one of the pooling technologies.

The original intention of pooling technology is to reuse some resources to avoid repeated construction and improve execution efficiency. Similar ones include database connection pool, string constant pool, and httpClient connection pool.

This article will share a useful thread pool, which is derived from Sogou’s open source high-performance network framework workflow.

Workflow is a C++ server engine recently released as open source by Sogou. It supports almost all of Sogou's back-end C++ online services, including all search services, cloud input methods, online advertising, etc., and handles over 10 billion requests every day.

Let's learn more about the implementation principle of workflow thread pool by reading the source code.

workflow thread pool

The project address of workflow is located at https://github.com/sogou/workflow.git.

The workflow thread pool is mainly distributed in four files, msgqueue.h/msgqueue.c/thrdpool.h/thrdpool.c.

First we analyze msgqueue.h/msgqueue.c

msgqueue

As you can see from the name, this is a message queue. This message queue contains tasks that need to be executed in the thread pool. The so-called task is an execution function and a corresponding input parameter.

In the workflow, the task definition is thrdpool_task, which has two parameters. The first one is a function pointer routine, and the second parameter is the context context.

struct thrdpool_task
{
    
    
	void (*routine)(void *);
	void *context;
};

Next, take a look at the definition of __msgqueue. __msgqueue is the main body of the message queue, and its definition is as follows:

struct __msgqueue
{
    
    
	size_t msg_max;
	size_t msg_cnt;
	int linkoff;
	int nonblock;
	void *head1;
	void *head2;
	void **get_head;
	void **put_head;
	void **put_tail;
	pthread_mutex_t get_mutex;
	pthread_mutex_t put_mutex;
	pthread_cond_t get_cond;
	pthread_cond_t put_cond;
};

The core idea of ​​msgqueue design is to use two queues. One queue is used to place new tasks, and one queue is used to take new tasks. When the get task queue is empty, the get queue and the put queue are switched. If get and put share a queue, then both placing tasks and retrieving tasks need to be locked. The advantage of using two queues is that locking is only required when switching when the get task queue is empty.

__msgqueueIn the definition of the structure, get_head is the head of the read queue, put_head is the head of the queue, and put_tail is the tail of the queue. head1 and head2 are two head pointers. During initialization, get_head points to head1 and put_head points to head2.

msg_max represents the maximum number of tasks that can be placed. msg_cnt represents the number of messages currently in the put queue.

nonblock represents the blocking mode of the message queue. If it is blocked, then there will be waiting operations for the message queue to obtain and place messages. For example, when the message queue is full, new messages cannot be placed. And if it is non-blocking mode, there is no waiting operation for getting messages and placing messages. Usually the thread pool is set to non-block when the thread pool exits.

linkoff represents the offset of the link pointer. For example, the linkoff value of the msg below is 4.

typedef struct msg_t {
    
    
	int data; 
	struct msg_t* link; 
} msg_t;

With the above preliminary concepts in mind, let's take a look at what methods will be provided in msgqueue. Shown below is the source code of msgqueue.h.

msgqueue.h

#ifndef _MSGQUEUE_H_
#define _MSGQUEUE_H_

#include <stddef.h>

typedef struct __msgqueue msgqueue_t;

#ifdef __cplusplus
extern "C"
{
    
    
#endif

msgqueue_t *msgqueue_create(size_t maxlen, int linkoff);
void msgqueue_put(void *msg, msgqueue_t *queue);
void *msgqueue_get(msgqueue_t *queue);
void msgqueue_set_nonblock(msgqueue_t *queue);
void msgqueue_set_block(msgqueue_t *queue);
void msgqueue_destroy(msgqueue_t *queue);

#ifdef __cplusplus
}
#endif

#endif

Because it is a code written in C language, in order to make it callable by both C and C++ programs, extern "C" is used in the code.

The header file of msgqueue provides 6 methods, whose functions are summarized as follows:

  • msgqueue_create: Create msgqueue.
  • msgqueue_put: Place tasks into msgqueue.
  • msgqueue_get: Take out tasks from msgqueue.
  • msgqueue_set_nonblock: Set msgqueue to nonblock.
  • msgqueue_set_block: Set msgqueue to block.
  • msgqueue_destroy: Destroy msgqueu.

The above-mentioned interface is so informative that it is worth learning.

Let's take a look at how these interfaces are implemented.

msgqueue_create

When I first saw this code, it turned out to be a trapezoidal code. It was hard to say that it was beautiful. I wonder if I didn’t understand the essence?

msgqueue_t *msgqueue_create(size_t maxlen, int linkoff)
{
    
    
	msgqueue_t *queue = (msgqueue_t *)malloc(sizeof (msgqueue_t));
	int ret;

	if (!queue)
		return NULL;

	ret = pthread_mutex_init(&queue->get_mutex, NULL);
	if (ret == 0)
	{
    
    
		ret = pthread_mutex_init(&queue->put_mutex, NULL);
		if (ret == 0)
		{
    
    
			ret = pthread_cond_init(&queue->get_cond, NULL);
			if (ret == 0)
			{
    
    
				ret = pthread_cond_init(&queue->put_cond, NULL);
				if (ret == 0)
				{
    
    
					queue->msg_max = maxlen;
					queue->linkoff = linkoff;
					queue->head1 = NULL;
					queue->head2 = NULL;
					queue->get_head = &queue->head1;
					queue->put_head = &queue->head2;
					queue->put_tail = &queue->head2;
					queue->msg_cnt = 0;
					queue->nonblock = 0;
					return queue;
				}

				pthread_cond_destroy(&queue->get_cond);
			}

			pthread_mutex_destroy(&queue->put_mutex);
		}

		pthread_mutex_destroy(&queue->get_mutex);
	}

	errno = ret;
	free(queue);
	return NULL;
}

This ladder's code should be optimized using if-return.

	ret = pthread_mutex_init(&queue->get_mutex, NULL);
	if (ret != 0)
	{
    
    
		//...
	}
	ret = pthread_mutex_init(&queue->put_mutex, NULL);
	if (ret != 0) 
	{
    
    
		//...
	}

The implementation of msgqueue_create is not difficult, the main thing is to initialize some variables. Initialized get_mutex/put_mutex/get_cond/put_cond. After these mutexes and condition variables are successfully created, initialize maxlen/linkoff/msg_cnt/nonblock, and set the queue pointer in msgqueue to null.

msgqueue_put

The source code of msgqueue_put is as follows:

void msgqueue_put(void *msg, msgqueue_t *queue)
{
    
    
	void **link = (void **)((char *)msg + queue->linkoff);//(1)

	*link = NULL; //(2)
	pthread_mutex_lock(&queue->put_mutex);//(3)
	while (queue->msg_cnt > queue->msg_max - 1 && !queue->nonblock)//(4)
		pthread_cond_wait(&queue->put_cond, &queue->put_mutex);//(5)

	*queue->put_tail = link;//(6)
	queue->put_tail = link;//(7)
	queue->msg_cnt++;//(8)
	pthread_mutex_unlock(&queue->put_mutex);//(9)
	pthread_cond_signal(&queue->get_cond);//(10)
}

msgqueue_put is a relatively difficult method to understand. Especially the following two lines have dissuaded many people.

	*queue->put_tail = link;
	queue->put_tail = link;

There are two input parameters of msgqueue_put, the first is msg, and the second is queue. The actual function is to put msg into the queue.

The type of msg here is a void* type, which is considered for universality. Let's take a look at the following code with an actual msg type.

For the thread pool, the type of msg is __thrdpool_task_entry, which has two parameters. The first parameter is link, which is similar to a next pointer used to point to the next task. The second parameter is the actual content of the task.

struct __thrdpool_task_entry
{
    
    
	void *link;
	struct thrdpool_task task;
};

Let’s start looking at the code line by line.

The purpose of lines 1-2 of is actually to retrieve the location of the link pointer in msg. Because the type of msg is void*, and the void* pointer cannot be added or subtracted, msg is first converted to char*. Added queue->linkoff to it, linkoff is actually the offset of the link pointer in the msg type. For __thrdpool_task_entry, the linkoff value is 0. So the first line of code passes the address of the linkoff pointer in msg to link.

The second line of code is easier to understand, it points the linkoff pointer to NULL. As shown below:

msgqueue_put

	void **link = (void **)((char *)msg + queue->linkoff);//(1)

	*link = NULL;//(2)

Next, look down. The function of the following code is that if the messages in the queue are full and it is in block mode, the message will stop until the messages in the queue are consumed. If it is in non-block mode, messages can be sent all the time without blocking.

The third line of code uses put_mutex to lock the critical section because multiple threads may put at the same time.

Lines 4 and 5 are the conventional way of writing condition variables, that is, loop evaluation until the condition is met.

	pthread_mutex_lock(&queue->put_mutex); //(3)
	while (queue->msg_cnt > queue->msg_max - 1 && !queue->nonblock) //(4)
		pthread_cond_wait(&queue->put_cond, &queue->put_mutex); //(5)

The following two lines are the code to persuade. . .

	*queue->put_tail = link;// (6)
	queue->put_tail = link; //(7)

First of all, review that in the msgqueue_create method, queue->put_tail points to head2. As shown below:

msgqueue_put

Therefore, the function of line 6 is to make head2 point to the link in msg. The effect is shown in the figure below.

msgqueue_put

The function of line 7 is to move put_tail to the end of the message queue, that is, to the link.

The effect is as follows:

msgqueue_put

If other messages come in, the process is similar. For example, if another message is added at this time, the queue situation will be as follows:

msgqueue_put

Comparing the graphics, these two lines of code are clearly visible.

The last three lines are very simple and will not be analyzed in detail. Please refer to the comments.

	queue->msg_cnt++;//队列中的消息数量加1
	pthread_mutex_unlock(&queue->put_mutex);//将put_mutex解锁
	pthread_cond_signal(&queue->get_cond);//发送信号给消费线程
msgqueue_get

The code for msgqueue_get is as follows:

void *msgqueue_get(msgqueue_t *queue)
{
    
    
	void *msg;//(1)

	pthread_mutex_lock(&queue->get_mutex);//(2)
	if (*queue->get_head || __msgqueue_swap(queue) > 0) //(3)
	{
    
    
		msg = (char *)*queue->get_head - queue->linkoff;//(4)
		*queue->get_head = *(void **)*queue->get_head;//(5)
	}
	else
	{
    
    
		msg = NULL; //(6)
		errno = ENOENT; //(7)
	}

	pthread_mutex_unlock(&queue->get_mutex);//(8)
	return msg;//(9)
}

The following is still analyzed line by line.

Line 1 defines the msg pointer, and line 2 uses get_mutex for locking, because the get process may be concurrent.

Line 3, if get_head is not empty, it means that msg can be taken out of it. The purpose of line 4 is to recalculate the starting address of the msg message. Line 5 lets get_head point to the next msg in the get queue.

As shown in the figure below, the msg pointer points to the address of the msg message:

msgqueue_get

The following is to let get_head point to the next element in the queue.

msgqueue_get

Returning to line 3, if get_head is empty, it means that the put queue and the get queue may need to be exchanged. Here refer to the analysis of __msgqueue_swap.

Lines 6 and 7 handle the situation if the put queue element is also empty. Scenes with empty elements will appear on non-block scenes. In the block scenario, if the put queue is empty, it will block and wait.

__msgqueue_swap

The source code of __msgqueue_swap is as follows:

static size_t __msgqueue_swap(msgqueue_t *queue)
{
    
    
	void **get_head = queue->get_head; //(1)
	size_t cnt; //(2)

	queue->get_head = queue->put_head;//(3)
	pthread_mutex_lock(&queue->put_mutex);//(4)
	while (queue->msg_cnt == 0 && !queue->nonblock)//(5)
		pthread_cond_wait(&queue->get_cond, &queue->put_mutex);(6)

	cnt = queue->msg_cnt;//(7)
	if (cnt > queue->msg_max - 1)//(8)
		pthread_cond_broadcast(&queue->put_cond);//(9)

	queue->put_head = get_head;//(10)
	queue->put_tail = get_head;//(11)
	queue->msg_cnt = 0;//(12)
	pthread_mutex_unlock(&queue->put_mutex);//(13)
	return cnt;
}

Lines 1-2 are easier to understand. They take out the address of the head element of the queue. cnt represents the number of messages in the put queue.

Line 3 points get_head to the head of the put queue. Note that get_mutex has been added at this time, so there will be no concurrency issues.

Line 4 adds put_mutex.

Lines 5-6 When the message queue is in block mode, it will be judged whether there are elements in the put queue, and if there are no elements, it will wait. If it is non-block mode, skip it.

The program goes to lines 7-9 to notify the waiting producer that production can continue. Although pthread_cond_broadcast is used here, the producer will not be awakened immediately because queue->put_mutex is held by the current thread at this time. After the producer wakes up and the spin lock fails for a period of time, it will sleep again until queue- >put_mutex is released. So I personally think it might be better to place lines 7-9 after line 12.

Lines 10-12 represent switching the pointers of the two queues.

Line 13 releases queue->put_mutex and the producer can continue production.

msgqueue_set_nonblock

The msgqueue_set_nonblock code is relatively simple, that is, setting the queue in non-block mode.

void msgqueue_set_nonblock(msgqueue_t *queue)
{
    
    
	queue->nonblock = 1;
	pthread_mutex_lock(&queue->put_mutex);
	pthread_cond_signal(&queue->get_cond);
	pthread_cond_broadcast(&queue->put_cond);
	pthread_mutex_unlock(&queue->put_mutex);
}
msgqueue_set_block

The msgqueue_set_block code is relatively simple, that is, setting the queue in block mode.

void msgqueue_set_block(msgqueue_t *queue)
{
    
    
	queue->nonblock = 0;
}
msgqueue_destroy

The msgqueue_destroy method is mainly to destroy the mutex lock and condition variables used in it.

	pthread_cond_destroy(&queue->put_cond);
	pthread_cond_destroy(&queue->get_cond);
	pthread_mutex_destroy(&queue->put_mutex);
	pthread_mutex_destroy(&queue->get_mutex);
	free(queue);

thrdpool

thrpool is the core code of the thread pool. First, take a look at its header file thrdpool.h, as shown below:

#ifndef _THRDPOOL_H_
#define _THRDPOOL_H_

#include <stddef.h>

typedef struct __thrdpool thrdpool_t;

struct thrdpool_task
{
    
    
	void (*routine)(void *);
	void *context;
};

#ifdef __cplusplus
extern "C"
{
    
    
#endif

thrdpool_t *thrdpool_create(size_t nthreads, size_t stacksize);
int thrdpool_schedule(const struct thrdpool_task *task, thrdpool_t *pool);
int thrdpool_increase(thrdpool_t *pool);
int thrdpool_in_pool(thrdpool_t *pool);
void thrdpool_destroy(void (*pending)(const struct thrdpool_task *),
					  thrdpool_t *pool);

#ifdef __cplusplus
}
#endif

#endif

It can be seen that there are not many methods, only 5. Let’s analyze them one by one below.

thrdpool_create

The code for thrdpool_create is as follows.

The form of nested if is used here again, which is not very beautiful.

thrdpool_t *thrdpool_create(size_t nthreads, size_t stacksize)
{
    
    
	thrdpool_t *pool;// (1)
	int ret;// (2)

	pool = (thrdpool_t *)malloc(sizeof (thrdpool_t));// (3)
	if (!pool)// (4)
		return NULL;// (5)

	pool->msgqueue = msgqueue_create((size_t)-1, 0);// (6)
	if (pool->msgqueue)// (7)
	{
    
    
		ret = pthread_mutex_init(&pool->mutex, NULL);// (8)
		if (ret == 0)// (9)
		{
    
    
			ret = pthread_key_create(&pool->key, NULL);// (9)
			if (ret == 0)// (10)
			{
    
    
				pool->stacksize = stacksize;// (11)
				pool->nthreads = 0;// (12)
				memset(&pool->tid, 0, sizeof (pthread_t));// (13)
				pool->terminate = NULL;// (14)
				if (__thrdpool_create_threads(nthreads, pool) >= 0)// (15)
					return pool;// (16)

				pthread_key_delete(pool->key);// (17)
			}

			pthread_mutex_destroy(&pool->mutex);// (18)
		}

		errno = ret;// (19)
		msgqueue_destroy(pool->msgqueue);// (20)
	}

	free(pool);
	return NULL;
}

The input parameters of thrdpool_create are nthreads and stacksize, which represent the number of threads and the size of the thread stack respectively.

Lines 1-2 declare the pool and ret variables.

Lines 3-5 create the pool object.

Line 6 creates a message queue msgqueue for the pool.

Lines 7-14, when the message queue is successfully created, the mutex and thread key in the pool are initialized. This key is used to set thread private variables. And set the number of threads and stack of the thread pool.

Line 15 calls __thrdpool_create_threads for actual thread creation. This method will be explained later. If the creation is successful, line 16 returns the pool variable.

Lines 17-20 perform some rollback operations when creation fails.

__thrdpool_create_threads

__thrdpool_create_threads is the method that actually creates threads.

static int __thrdpool_create_threads(size_t nthreads, thrdpool_t *pool)
{
    
    
	pthread_attr_t attr;//(1)
	pthread_t tid;//(2)
	int ret;//(3)

	ret = pthread_attr_init(&attr);//(4)
	if (ret == 0)//(5)
	{
    
    
		if (pool->stacksize)//(6)
			pthread_attr_setstacksize(&attr, pool->stacksize);//(7)

		while (pool->nthreads < nthreads)//(8)
		{
    
    
			ret = pthread_create(&tid, &attr, __thrdpool_routine, pool);//(9)
			if (ret == 0)//(10)
				pool->nthreads++;//(11)
			else//(12)
				break;//(13)
		}

		pthread_attr_destroy(&attr);//(14)
		if (pool->nthreads == nthreads)//(15)
			return 0;//(16)

		__thrdpool_terminate(0, pool);//(17)
	}

	errno = ret;//(18)
	return -1;//(19)
}

Lines 1-3 declare three parameters attr/tid/ret.

Line 4 creates attr. If the creation is successful, continue, if the creation fails, an error is returned.

Lines 6-7 set the thread stack size.

Lines 8-13 create threads in a loop. If the creation is successful, the number of threads in the thread pool will be increased. The entry method of the thread is __thrdpool_routine, which will be explained below.

Lines 14-16 When the thread is created, attr must be destroyed. If the number of threads created is equal to the expected number, return, otherwise the creation fails and the thread pool is destroyed.

__thrdpool_routine

__thrdpool_routine is the entry function of the thread. Generally, the entry function of the thread pool will have a loop to continuously accept tasks to run.

static void *__thrdpool_routine(void *arg)
{
    
    
	thrdpool_t *pool = (thrdpool_t *)arg;//(1)
	struct __thrdpool_task_entry *entry;//(2)
	void (*task_routine)(void *);//(3)
	void *task_context;//(4)
	pthread_t tid;//(5)

	pthread_setspecific(pool->key, pool);//(6)
	while (!pool->terminate)//(7)
	{
    
    
		entry = (struct __thrdpool_task_entry *)msgqueue_get(pool->msgqueue);//(8)
		if (!entry)//(9)
			break;//(10)

		task_routine = entry->task.routine;//(11)
		task_context = entry->task.context;//(12)
		free(entry);//(13)
		task_routine(task_context);//(14)

		if (pool->nthreads == 0)//(15)
		{
    
    
			free(pool);//(16)
			return NULL;//(17)
		}
	}

	/* One thread joins another. Don't need to keep all thread IDs. */
	pthread_mutex_lock(&pool->mutex);//(18)
	tid = pool->tid;//(19)
	pool->tid = pthread_self();//(20)
	if (--pool->nthreads == 0)//(21)
		pthread_cond_signal(pool->terminate);//(22)

	pthread_mutex_unlock(&pool->mutex);//(23)
	if (memcmp(&tid, &__zero_tid, sizeof (pthread_t)) != 0)//(24)
		pthread_join(tid, NULL);//(25)

	return NULL;//(23)
}

In line 1, the parameters are taken out from arg and forced into thrdpool_t type. Lines 2-5 declare a parameter.

Line 6 sets a pool address value to pool->key. This will be used later to determine whether a thread belongs to a thread pool.

Lines 7 to 17, the loop retrieves tasks from the message queue and executes them. If the retrieved message is empty, it exits. Because the message queue will be set to non-block when the thread pool exits, the received messages may be empty. In line 7, pool->terminate has a value of NULL when the thread pool does not exit. When the thread pool is destroyed, it will be assigned a value. The function of lines 15 to 17 needs special explanation. This is when the thread inside the thread pool calls destroy. The thread pool will go here. The thread calling destroy will wait for other threads in the thread pool to exit. When it reaches lines 15-17, , you need to destroy the thread pool. The principle of workflow thread pool destruction is that whoever calls destroy destroys it. When an external thread calls destroy, the external thread destroys the thread pool. When an internal thread calls destroy, the internal thread destroys the thread pool.

Lines 18-23 mean that the thread has exited, and pool->mutex will be mounted here first. Because threads may exit at the same time. The design idea here is to let the threads exit one by one and let the next thread join the previous thread. I will mention this again when explaining destruction later.

thrdpool_schedule

The actual function of thrdpool_schedule is to push a task to the thread pool. Internally, msgqueue_put is called to insert a task into the message queue. The implementation of this method is relatively simple and does not require too much analysis.

int thrdpool_schedule(const struct thrdpool_task *task, thrdpool_t *pool)
{
    
    
	void *buf = malloc(sizeof (struct __thrdpool_task_entry));

	if (buf)
	{
    
    
		__thrdpool_schedule(task, buf, pool);
		return 0;
	}

	return -1;
}

void __thrdpool_schedule(const struct thrdpool_task *task, void *buf,
						 thrdpool_t *pool)
{
    
    
	((struct __thrdpool_task_entry *)buf)->task = *task;
	msgqueue_put(buf, pool->msgqueue);
}
thrdpool_increase

The function of thrdpool_increase is to add a thread. The function of this function is relatively clear, so we won’t do too much analysis here.

int thrdpool_increase(thrdpool_t *pool)
{
    
    
	pthread_attr_t attr;
	pthread_t tid;
	int ret;

	ret = pthread_attr_init(&attr);
	if (ret == 0)
	{
    
    
		if (pool->stacksize)
			pthread_attr_setstacksize(&attr, pool->stacksize);

		pthread_mutex_lock(&pool->mutex);
		ret = pthread_create(&tid, &attr, __thrdpool_routine, pool);
		if (ret == 0)
			pool->nthreads++;

		pthread_mutex_unlock(&pool->mutex);
		pthread_attr_destroy(&attr);
		if (ret == 0)
			return 0;
	}

	errno = ret;
	return -1;
}
thrdpool_in_pool

The function of this function is to determine whether a thread belongs to the thread pool. When a thread belonging to the thread pool starts, the thread private variable key will be filled with the address of the pool, so you can use pthread_getspecific(pool->key) == pool for judgment.

int thrdpool_in_pool(thrdpool_t *pool)
{
    
    
	return pthread_getspecific(pool->key) == pool;
}
thrdpool_destroy

The thrdpool_destroy function input parameter contains a variable part. One part is a pending function. This function is used to process some tasks that have been submitted but have not yet been executed.


void thrdpool_destroy(void (*pending)(const struct thrdpool_task *),
					  thrdpool_t *pool)
{
    
    
	int in_pool = thrdpool_in_pool(pool);//(1)
	struct __thrdpool_task_entry *entry;//(2)

	__thrdpool_terminate(in_pool, pool);//(3)
	while (1)//(4)
	{
    
    
		entry = (struct __thrdpool_task_entry *)msgqueue_get(pool->msgqueue);//(4)
		if (!entry)//(5)
			break;//(6)

		if (pending)//(7)
			pending(&entry->task);//(8)

		free(entry);//(9)
	}

	pthread_key_delete(pool->key);//(10)
	pthread_mutex_destroy(&pool->mutex);//(11)
	msgqueue_destroy(pool->msgqueue);//(12)
	if (!in_pool)//(13)
		free(pool);//(14)
}

Line 1 in_pool is used to determine whether the thread calling thrdpool_destroy belongs to the thread in the thread pool. The destroy thread can be a thread in the thread pool or an external thread. The implementation of the two will be different.

Line 3 will call__thrdpool_terminate to destroy the thread pool, which will be explained later.

Lines 4-13 process some messages that have not yet been processed in the message queue.

__thrdpool_terminate

The source code of __thrdpool_terminate is as follows:

static void __thrdpool_terminate(int in_pool, thrdpool_t *pool)
{
    
    
	pthread_cond_t term = PTHREAD_COND_INITIALIZER;//(1)

	pthread_mutex_lock(&pool->mutex);//(2)
	msgqueue_set_nonblock(pool->msgqueue);//(3)
	pool->terminate = &term;//(4)

	if (in_pool)//(5)
	{
    
    
		pthread_detach(pthread_self());//(6)
		pool->nthreads--;//(7)
	}

	while (pool->nthreads > 0)//(8)
		pthread_cond_wait(&term, &pool->mutex);//(9)

	pthread_mutex_unlock(&pool->mutex);//(10)
	if (memcmp(&pool->tid, &__zero_tid, sizeof (pthread_t)) != 0)//(11)
		pthread_join(pool->tid, NULL);//(12)
}

Lines 1 and 4 set the initial value for the condition variable pool->termincate. Set the message queue to non-block so that threads waiting for messages quickly enter the exit process. If a thread in the thread pool issues a destroy request, it will set itself to detach and subtract 1 from the number of threads in the thread pool. (Note that this echoes lines 15-17 of __thrdpool_routine).

Lines 8-9 wait for all threads to exit. As mentioned above, __thrdpool_routine will join the previous thread and send a signal to pool->termincate.

Finally, if an external thread initiates the destruction, you also need help to destroy the last thread in the thread pool.

If a thread in the thread pool initiates the destroy, it has already been set to detach, so it does not need to join itself.

The difference between the two initiation modes can be seen in the following two figures:

The initiator is an external thread:

thrdpool_terminate

The initiator is the thread pool internal thread:

thrdpool_terminate

demo

The thread pool of workflow can be used basically out of the box and does not depend on other codes. You only need to include masqueue.c/msgqueue.h/thrdpool.h/thrdpool.c and add the test code main.cpp.

The file structure of demo is as follows:

[root@localhost workflow-thread]# tree .
.
├── main.cpp
├── msgqueue.c
├── msgqueue.h
├── thrdpool.c
└── thrdpool.h

The following is the content of main.cpp:

//gcc -c msgqueue.c 
//gcc -c thrdpool.c
//g++ main.cpp msgqueue.o thrdpool.o 

#include <iostream>
#include <unistd.h>
#include "thrdpool.h"
#include "msgqueue.h"


struct Context{
    
    
	int val;
};

void my_func(void *context) // 我们要执行的函数  
{
    
     
	printf("task-%d start.\n", ((Context*)context)->val);
	sleep(1);
} 

void my_pending(const struct thrdpool_task *task) // 线程池销毁后,没执行的任务会到这里
{
    
    
	printf("pending task-%d.\n",  ((Context*)task->context)->val);  
} 

int main() 
{
    
    
	thrdpool_t *thrd_pool = thrdpool_create(3, 1024); // 创建  
	struct thrdpool_task task;
	int i;
	
	Context *p_context[5];
	for (i = 0; i < 5; i++)
	{
    
    
		p_context[i] = new Context();
		p_context[i]->val = i;
		task.routine = &my_func; 
		task.context = (void *)(p_context[i]); 
		thrdpool_schedule(&task, thrd_pool); // 调用
	}
	getchar(); 

	std::cout << "start_destroy" << std::endl;
	thrdpool_destroy(&my_pending, thrd_pool); // 结束
	for (i = 0; i < 5; i++)
	{
    
    
		delete p_context[i];
	}
	
	return 0; 
} 

The running results are as follows:

demo

Guess you like

Origin blog.csdn.net/qq_31442743/article/details/131727500