Number of threads created by GCD?
Asked Answered
F

4

32

Is there any good documention on how many threads are created by GCD? At WWDC, they told us it's modeled around CPU cores. However, if I call this example:

for (int i=1; i<30000; i++) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [NSThread sleepForTimeInterval:100000];
    });
}

it opens 66 threads, even on an iPad1. (It also opens 66 threads when called on Lion natively). Why 66?

Farmland answered 27/8, 2011 at 10:5 Comment(0)
M
35

First, 66 == 64 (the maximum GCD thread pool size) + the main thread + some other random non-GCD thread.

Second, GCD is not magic. It is optimized for keeping the CPU busy with code that is mostly CPU bound. The "magic" of GCD is that it dynamically create more threads than CPUs when work items unintentionally and briefly wait for operations to complete.

Having said that, code can confuse the GCD scheduler by intentionally sleeping or waiting for events instead of using dispatch sources to wait for events. In these scenarios, the block of work is effectively implementing its own scheduler and therefore GCD must assume that the thread has been co-opted from the thread pool.

In short, the thread pool will operate optimally if your code prefers dispatch_after() over "sleep()" like APIs, and dispatch sources over handcrafted event loops (Unix select()/poll(), Cocoa runloops, or POSIX condition variables).

Madame answered 2/9, 2011 at 18:50 Comment(2)
"has been co-opted from the thread pool" What do you mean by co-opted? Do you mean the sleeping or other hang-up will be interpreted as 100% activity so-to-speak and therefore the thread won't be available for use with additional dispatches?Elisavetgrad
Sure would be nice if you could provide a reference for this claim on thread pool size - I am unable to find one anywhere.Sufficient
A
2

The documentation avoids mentioning the number of threads created. Mostly because the optimal number of threads depends heavily on the context.

One issue with Grand Cendral Dispatch is that it will spawn a new thread if a running task blocks. That is, you should avoid blocking when using GCD as having more threads than cores is suboptimal.

In your case, GCD detects that the task is inactive, and spawns a new thread for the next task.

Why 66 is the limit is beyond me.

Amadus answered 27/8, 2011 at 13:20 Comment(4)
I hardly can believe that 66 is an optimal number, if the documentation tells me that each thread eats about half a megabyte of ram. (developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/…) Thus, with 66 threads, we have about 30 megs overhead, that's half of iOS's 64MB limit in total.Farmland
@Farmland Thread also costs context switching. So memory should not be the only factor to decide the limit.Semitropical
@Farmland from the same document "but the actual pages associated with that memory are not created until they are needed.". So, the half meg of stack space is only claimed when used (page by page; i.e. if your thread only needs 2 pages of stack, it only wastes so much). Also, Eonil, we now have multi-core CPU's where the cost of context switching is less of an issue (given that you 'only' run the number of threads equal to or less than the number of cores)Ciliate
@Ciliate You're right about memory. Actual memory space is allocated lazily, so it's unlikely to be a problem in most cases. As memory cost doesn't matter a lot, I believe switching cost is a bigger concern to limit maximum thread count.Semitropical
K
2

The answer should be: 512

There are different cases:

  • All types (Concurrent and serial queues ) can simultaneously create up to 512 threads.

  • All global queues can create up to 64 threads simultaneously.

. All types (Concurrent and serial queues ) All global queues
simultaneously create up to 512 threads 64 threads

As a general rule of thumb,

If the number of threads in an app exceeds 64, it will cause the main thread to lag. In severe cases, it may even trigger a watchdog crash. This is because there is an overhead in creating threads, and the main costs under iOS are: kernel data structure (about 1KB), stack space (512KB for child threads, 1MB for main threads) It can also be set using -setStackSize, but it must be a multiple of 4K, and the minimum is 16K, which takes about 90 ms to create a thread. Opening a large number of threads will reduce the performance of the program. The more threads there are, the more CPU overhead there is to schedule them. The program design is more complex: communication between threads and data sharing among multiple threads.

First of all, GCD has a limit on the number of threads it can create,

The way gcd creates threads is by calling _pthread_workqueue_addthreads, so there is a limit to the number of threads created. Other ways of creating threads do not call this creation method.

Back to the question above:

All global queues can create up to 64 threads at the same time.

The global queue is added via the _pthread_workqueue_addthreads method. However, there is a limit on the number of threads added using this method in the kernel (plugin).

The specific code for the restriction is shown below:

#define MAX_PTHREAD_SIZE 64*1024

The meaning of this code is as follows:

The total size is limited to 64k, according to Apple-Docs-Threading Programming Guide- Thread creation costs: 1 thread allocates 1k cores, so we can deduce that the result is 64 threads.

https://github.com/ChenYilong

In summary, the global queue can create up to 64 threads, which is written dead in the kernel (plugin).

A more detailed test code is as follows:

  • Test Environment : iOS 14.3
  • Test code

case 1: Global Queue - CPU Busy

In the first test case, we use dispatch_get_global_queue(0, 0) to get a default global queue and simulate CPU busy.


   + (void)printThreadCount {
       kern_return_t kr = { 0 };
       thread_array_t thread_list = { 0 };
       mach_msg_type_number_t thread_count = { 0 };
       kr = task_threads(mach_task_self(), &thread_list, &thread_count);
       if (kr != KERN_SUCCESS) {
           return;
       }
       NSLog(@"threads count:%@", @(thread_count));

       kr = vm_deallocate( mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t) );
       if (kr != KERN_SUCCESS) {
           return;
       }
       return;
   }

   + (void)test1 {
       NSMutableSet<NSThread *> *set = [NSMutableSet set];
       for (int i=0; i < 1000; i++) {
           dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
           dispatch_async(queue, ^{
               NSThread *thread = [NSThread currentThread];
               [set addObject:[NSThread currentThread]];
               dispatch_async(dispatch_get_main_queue(), ^{
                   NSLog(@"start:%@", thread);
                   NSLog(@"GCD threads count:%lu",(unsigned long)set.count);
                   [self printThreadCount];
               });

               NSDate *date = [NSDate dateWithTimeIntervalSinceNow:10];
               long i=0;
               while ([date compare:[NSDate date]]) {
                   i++;
               }
               [set removeObject:thread];
               NSLog(@"end:%@", thread);
           });
       }
   }

Tested: the number of threads is 2

case 2: Global queue - CPU idle

For the second code, we test by [NSThread sleepForTimeInterval:10]; simulating CPU idle

   + (void)test2 {
       NSMutableSet<NSThread *> *set = [NSMutableSet set];
       for (int i=0; i < 1000; i++) {
           dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
           dispatch_async(queue, ^{
               NSThread *thread = [NSThread currentThread];
               [set addObject:[NSThread currentThread]];
               dispatch_async(dispatch_get_main_queue(), ^{
                   NSLog(@"start:%@", thread);
                   NSLog(@"GCD threads count:%lu",(unsigned long)set.count);
                   [self printThreadCount];
               });
               // thread sleep for 10s
               [NSThread sleepForTimeInterval:10];
               [set removeObject:thread];
               NSLog(@"end:%@", thread);
               return;
           });
       }
   }

After testing, the maximum number of threads is 64

All concurrent queues and serial queues can create up to 512 threads simultaneously.

A more detailed test code is as follows:

case 1: Self-built Queues - CPU Busy

Now, let us see the performance of the self-built queue - CPU busy. This example will simulate most APP scenarios, where different business parties create separate queues to manage their tasks.

   + (void)test3 {
       NSMutableSet<NSThread *> *set = [NSMutableSet set];
       for (int i=0; i < 1000; i++) {
           const char *label = [NSString stringWithFormat:@"label-:%d", i].UTF8String;
           NSLog(@"create:%s", label);
           dispatch_queue_t queue = dispatch_queue_create(label, DISPATCH_QUEUE_SERIAL);
           dispatch_async(queue, ^{
               NSThread *thread = [NSThread currentThread];
               [set addObject:[NSThread currentThread]];

               dispatch_async(dispatch_get_main_queue(), ^{
                   static NSInteger lastCount = 0;
                   if (set.count <= lastCount) {
                       return;
                   }
                   lastCount = set.count;
                   NSLog(@"begin:%@", thread);
                   NSLog(@"GCD threads count量:%lu",(unsigned long)set.count);
                   [self printThreadCount];
               });

               NSDate *date = [NSDate dateWithTimeIntervalSinceNow:10];
               long i=0;
               while ([date compare:[NSDate date]]) {
                   i++;
               }
               [set removeObject:thread];
               NSLog(@"end:%@", thread);
           });
       }
   }

After testing, the maximum number of threads created by GCD is 512

case 2: Self-built queues - CPU idle

+ (void)test4 {
       NSMutableSet<NSThread *> *set = [NSMutableSet set];
       for (int i=0; i < 10000; i++) {
           const char *label = [NSString stringWithFormat:@"label-:%d", i].UTF8String;
           NSLog(@"create:%s", label);
           dispatch_queue_t queue = dispatch_queue_create(label, DISPATCH_QUEUE_SERIAL);
           dispatch_async(queue, ^{
               NSThread *thread = [NSThread currentThread];

               dispatch_async(dispatch_get_main_queue(), ^{
                   [set addObject:thread];
                   static NSInteger lastCount = 0;
                   if (set.count <= lastCount) {
                       return;
                   }
                   lastCount = set.count;
                   NSLog(@"begin:%@", thread);
                   NSLog(@"GCD threads count:%lu",(unsigned long)set.count);
                   [self printThreadCount];
               });

               [NSThread sleepForTimeInterval:10];
               dispatch_async(dispatch_get_main_queue(), ^{
                   [set removeObject:thread];
                   NSLog(@"end:%@", thread);
               });
           });
       }
   }

The maximum number of threads created by the self-built queue - CPU idle is 512

Other test code

__block int index = 0;
   
// one concurrent  queue test
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; ++i) {
   dispatch_async(queue, ^{
       id name = nil;
       @synchronized (self) {
           name = [NSString stringWithFormat:@"gcd-limit-test-global-concurrent-%d", index];
           index += 1;
       }
       NSThread.currentThread.name = name;
       NSLog(@"%@", name);
       sleep(100000);
   });
}
// some concurrent queues test
for (int i = 0; i < 1000; ++i) {
   char buffer[256] = {};
   sprintf(buffer, "gcd-limit-test-concurrent-%d", i);
   dispatch_queue_t queue = dispatch_queue_create(buffer, DISPATCH_QUEUE_CONCURRENT);
   dispatch_async(queue, ^{
       id name = nil;
       @synchronized (self) {
           name = [NSString stringWithFormat:@"gcd-limit-test-concurrent-%d", index];
           index += 1;
       }
       NSThread.currentThread.name = name;
       NSLog(@"%@", name);
       sleep(100000);
   });
}
// some serial queues test
for (int i = 0; i < 1000; ++i) {
   char buffer[256] = {};
   sprintf(buffer, "gcd-limit-test-%d", i);
   dispatch_queue_t queue = dispatch_queue_create(buffer, 0);
   dispatch_async(queue, ^{
       id name = nil;
       @synchronized (self) {
           name = [NSString stringWithFormat:@"gcd-limit-test-%d", index];
           index += 1;
       }
       NSThread.currentThread.name = name;
       NSLog(@"%@", name);
       sleep(100000);
   });
}

https://github.com/ChenYilong

https://github.com/ChenYilong

https://github.com/ChenYilong

Special note here:

The 512 mentioned is the limit of gcd. After the 512 gcd threads are opened, you can still open them with NSThread.

So the chart above

Currently it should be 512, 516 = 512(max) + main thread + js thread + web thread + uikit event thread"

Conclusion

After testing, GCD's global queue automatically limits the number of threads to a reasonable number. Compared to this, the number of threads created by the self-built queue is large.

Considering that the number of threads is too large, the CPU scheduling cost will increase.

Therefore, it is recommended that small APPs use a global queue to manage tasks as much as possible; large APPs can decide the suitable solution according to their actual situation.

Krefetz answered 16/11, 2021 at 10:13 Comment(1)
All concurrent queues and serial queues can create up to 512 threads simultaneously seems not true. creating concurrent queues using test3 and test4 only get 64 thread max.Arrowwood
J
0

Number of busy threads is equal to CPU cores. Blocked threads (you block them with sleepForTimeInterval) are not counted.

If you change your code to this (Swift):

for _ in 1..<30000 {
    DispatchQueue.global().async {
        while true {}
     }
}

you'll see that there are just 2 threads created (on iPhone SE):

enter image description here

There is a thread limit so that your app is not killed because of big memory consumption if you have problem with blocked threads (usually because of deadlock).

Never block threads and you'll have them as much as your cores.

Jeffryjeffy answered 1/6, 2019 at 17:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.